1<?php 2/** 3 * Strata, data entry plugin 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Brend Wanders <b.wanders@utwente.nl> 7 */ 8 9if (!defined('DOKU_INC')) die('Meh.'); 10 11/** 12 * Data entry syntax for dedicated data blocks. 13 */ 14class syntax_plugin_strata_entry extends DokuWiki_Syntax_Plugin { 15 protected static $previewMetadata = array(); 16 17 function __construct() { 18 $this->syntax =& plugin_load('helper', 'strata_syntax'); 19 $this->util =& plugin_load('helper', 'strata_util'); 20 $this->triples =& plugin_load('helper', 'strata_triples'); 21 } 22 23 function getType() { 24 return 'substition'; 25 } 26 27 function getPType() { 28 return 'block'; 29 } 30 31 function getSort() { 32 return 450; 33 } 34 35 function connectTo($mode) { 36 if($this->getConf('enable_entry')) { 37 $this->Lexer->addSpecialPattern('<data(?: +[^#>]+?)?(?: *#[^>]*?)?>\s*?\n(?:.*?\n)*?\s*?</data>',$mode, 'plugin_strata_entry'); 38 } 39 } 40 41 function handle($match, $state, $pos, Doku_Handler $handler) { 42 $result = array( 43 'entry'=>'', 44 'data'=> array( 45 $this->util->getIsaKey(false) => array(), 46 $this->util->getTitleKey(false) => array() 47 ) 48 ); 49 50 // allow for preprocessing by a subclass 51 $match = $this->preprocess($match, $state, $pos, $handler, $result); 52 53 $lines = explode("\n",$match); 54 $header = trim(array_shift($lines)); 55 $footer = trim(array_pop($lines)); 56 57 58 // allow subclasses to mangle header 59 $header = $this->handleHeader($header, $result); 60 61 // extract header, and match it to get classes and fragment 62 preg_match('/^( +[^#>]+)?(?: *#([^>]*?))?$/', $header, $header); 63 64 // process the classes into triples 65 foreach(preg_split('/\s+/',trim($header[1])) as $class) { 66 if($class == '') continue; 67 $result['data'][$this->util->getIsaKey(false)][] = array('value'=>$class,'type'=>'text', 'hint'=>null); 68 } 69 70 // process the fragment if necessary 71 $result['entry'] = $header[2]; 72 $result['position'] = $pos; 73 if($result['entry'] != '') { 74 $result['title candidate'] = array('value'=>$result['entry'], 'type'=>'text', 'hint'=>null); 75 } 76 77 // parse tree 78 $tree = $this->syntax->constructTree($lines,'data entry'); 79 80 // allow subclasses first pick in the tree 81 $this->handleBody($tree, $result); 82 83 // fetch all lines 84 $lines = $this->syntax->extractText($tree); 85 86 // sanity check 87 if(count($tree['cs'])) { 88 msg(sprintf($this->syntax->getLang('error_entry_block'), ($tree['cs'][0]['tag']?sprintf($this->syntax->getLang('named_group'),utf8_tohtml(hsc($tree['cs'][0]['tag']))):$this->syntax->getLang('unnamed_group')), utf8_tohtml(hsc($result['entry']))),-1); 89 return array(); 90 } 91 92 $p = $this->syntax->getPatterns(); 93 94 // now handle all lines 95 foreach($lines as $line) { 96 $line = $line['text']; 97 // match a "property_type(hint)*: value" pattern 98 // (the * is only used to indicate that the value is actually a comma-seperated list) 99 // [grammar] ENTRY := PREDICATE TYPE? '*'? ':' ANY 100 if(preg_match("/^({$p->predicate})\s*({$p->type})?\s*(\*)?\s*:\s*({$p->any}?)$/",$line,$parts)) { 101 // assign useful names 102 list(, $property, $ptype, $multi, $values) = $parts; 103 list($type,$hint) = $p->type($ptype); 104 105 // trim property so we don't get accidental 'name ' keys 106 $property = utf8_trim($property); 107 108 // lazy create key bucket 109 if(!isset($result['data'][$property])) { 110 $result['data'][$property] = array(); 111 } 112 113 // determine values, splitting on commas if necessary 114 $values = ($multi == '*') ? explode(',',$values) : array($values); 115 116 // generate triples from the values 117 foreach($values as $v) { 118 $v = utf8_trim($v); 119 if($v == '') continue; 120 // replace the [[]] quasi-magic token with the empty string 121 if($v == '[[]]') $v = ''; 122 if(!isset($type) || $type == '') { 123 list($type, $hint) = $this->util->getDefaultType(); 124 } 125 $result['data'][$property][] = array('value'=>$v,'type'=>$type,'hint'=>($hint?:null)); 126 } 127 } else { 128 msg(sprintf($this->syntax->getLang('error_entry_line'), utf8_tohtml(hsc($line))),-1); 129 } 130 } 131 132 // normalize data: 133 // - Normalize all values 134 $buckets = $result['data']; 135 $result['data'] = array(); 136 137 foreach($buckets as $property=>&$bucket) { 138 // normalize the predicate 139 $property = $this->util->normalizePredicate($property); 140 141 // process all triples 142 foreach($bucket as &$triple) { 143 // normalize the value 144 $type = $this->util->loadType($triple['type']); 145 $triple['value'] = $type->normalize($triple['value'], $triple['hint']); 146 147 // lazy create property bucket 148 if(!isset($result['data'][$property])) { 149 $result['data'][$property] = array(); 150 } 151 152 $result['data'][$property][] = $triple; 153 } 154 } 155 156 157 // normalize title candidate 158 if(!empty($result['title candidate'])) { 159 $type = $this->util->loadType($result['title candidate']['type']); 160 $result['title candidate']['value'] = $type->normalize($result['title candidate']['value'], $result['title candidate']['hint']); 161 } 162 163 $footer = $this->handleFooter($footer, $result); 164 165 return $result; 166 } 167 168 /** 169 * Handles the whole match. This method is called before any processing 170 * is done by the actual class. 171 * 172 * @param match string the complete match 173 * @param state the parser state 174 * @param pos the position in the source 175 * @param the handler object 176 * @param result array the result array passed to the render method 177 * @return a preprocessed string 178 */ 179 function preprocess($match, $state, $pos, &$handler, &$result) { 180 return $match; 181 } 182 183 /** 184 * Handles the header of the syntax. This method is called before 185 * the header is handled. 186 * 187 * @param header string the complete header 188 * @param result array the result array passed to the render method 189 * @return a string containing the unhandled parts of the header 190 */ 191 function handleHeader($header, &$result) { 192 // remove prefix and suffix 193 return preg_replace('/(^<data)|( *>$)/','',$header); 194 } 195 196 /** 197 * Handles the body of the syntax. This method is called before any 198 * of the body is handled. 199 * 200 * @param tree array the parsed tree 201 * @param result array the result array passed to the render method 202 */ 203 function handleBody(&$tree, &$result) { 204 } 205 206 /** 207 * Handles the footer of the syntax. This method is called after the 208 * data has been parsed and normalized. 209 * 210 * @param footer string the footer string 211 * @param result array the result array passed to the render method 212 * @return a string containing the unhandled parts of the footer 213 */ 214 function handleFooter($footer, &$result) { 215 return ''; 216 } 217 218 219 protected function getPositions($data) { 220 global $ID; 221 222 // determine positions of other data entries 223 // (self::$previewMetadata is only filled if a preview_metadata was run) 224 if(isset(self::$previewMetadata[$ID])) { 225 $positions = self::$previewMetadata[$ID]['strata']['positions']; 226 } else { 227 $positions = p_get_metadata($ID, 'strata positions'); 228 } 229 230 // only read positions if we have them 231 if(is_array($positions) && isset($positions[$data['entry']])) { 232 $positions = $positions[$data['entry']]; 233 $currentPosition = array_search($data['position'],$positions); 234 $previousPosition = isset($positions[$currentPosition-1])?'data_fragment_'.$positions[$currentPosition-1]:null; 235 $nextPosition = isset($positions[$currentPosition+1])?'data_fragment_'.$positions[$currentPosition+1]:null; 236 $currentPosition = 'data_fragment_'.$positions[$currentPosition]; 237 } 238 239 return array($currentPosition, $previousPosition, $nextPosition); 240 } 241 242 function render($mode, Doku_Renderer $R, $data) { 243 global $ID; 244 245 if($data == array()) { 246 return false; 247 } 248 249 if($mode == 'xhtml' || $mode == 'odt') { 250 list($currentPosition, $previousPosition, $nextPosition) = $this->getPositions($data); 251 // render table header 252 if($mode == 'xhtml') { $R->doc .= '<div class="strata-entry" '.(isset($currentPosition)?'id="'.$currentPosition.'"':'').'>'; } 253 if($mode == 'odt' && isset($currentPosition) && method_exists ($R, 'insertBookmark')) { 254 $R->insertBookmark($currentPosition, false); 255 } 256 $R->table_open(); 257 $R->tablerow_open(); 258 $R->tableheader_open(2); 259 260 // determine actual header text 261 $heading = ''; 262 if(isset($data['data'][$this->util->getTitleKey()])) { 263 // use title triple if possible 264 $heading = $data['data'][$this->util->getTitleKey()][0]['value']; 265 } elseif (!empty($data['title candidate'])) { 266 // use title candidate if possible 267 $heading = $data['title candidate']['value']; 268 } else { 269 if(useHeading('content')) { 270 // fall back to page title, depending on wiki configuration 271 $heading = p_get_first_heading($ID); 272 } 273 274 if(!$heading) { 275 // use page id if all else fails 276 $heading = noNS($ID); 277 } 278 } 279 $R->cdata($heading); 280 281 // display a comma-separated list of classes if the entry has classes 282 if(isset($data['data'][$this->util->getIsaKey()])) { 283 $R->emphasis_open(); 284 $R->cdata(' ('); 285 $values = $data['data'][$this->util->getIsaKey()]; 286 $this->util->openField($mode, $R, $this->util->getIsaKey()); 287 for($i=0;$i<count($values);$i++) { 288 $triple =& $values[$i]; 289 if($i!=0) $R->cdata(', '); 290 $type = $this->util->loadType($triple['type']); 291 $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $type, $triple['hint']); 292 } 293 $this->util->closeField($mode, $R); 294 $R->cdata(')'); 295 $R->emphasis_close(); 296 } 297 $R->tableheader_close(); 298 $R->tablerow_close(); 299 300 // render a row for each key, displaying the values as comma-separated list 301 foreach($data['data'] as $key=>$values) { 302 // skip isa and title keys 303 if($key == $this->util->getTitleKey() || $key == $this->util->getIsaKey()) continue; 304 305 // render row header 306 $R->tablerow_open(); 307 $R->tableheader_open(); 308 $this->util->renderPredicate($mode, $R, $this->triples, $key); 309 $R->tableheader_close(); 310 311 // render row content 312 $R->tablecell_open(); 313 $this->util->openField($mode, $R, $key); 314 for($i=0;$i<count($values);$i++) { 315 $triple =& $values[$i]; 316 if($i!=0) $R->cdata(', '); 317 $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $triple['hint']); 318 } 319 $this->util->closeField($mode, $R); 320 $R->tablecell_close(); 321 $R->tablerow_close(); 322 } 323 324 if($previousPosition || $nextPosition) { 325 $R->tablerow_open(); 326 $R->tableheader_open(2); 327 if($previousPosition) { 328 if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-previous">'; } 329 $R->locallink($previousPosition, $this->util->getLang('data_entry_previous')); 330 if($mode == 'xhtml') { $R->doc .= '</span>'; } 331 } 332 $R->cdata(' '); 333 if($nextPosition) { 334 if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-next">'; } 335 $R->locallink($nextPosition, $this->util->getLang('data_entry_next')); 336 if($mode == 'xhtml') { $R->doc .= '</span>'; } 337 } 338 $R->tableheader_close(); 339 $R->tablerow_close(); 340 } 341 342 $R->table_close(); 343 if($mode == 'xhtml') { $R->doc .= '</div>'; } 344 345 return true; 346 347 } elseif($mode == 'metadata' || $mode == 'preview_metadata') { 348 $triples = array(); 349 $subject = $ID.'#'.$data['entry']; 350 351 // resolve the subject to normalize everything 352 resolve_pageid(getNS($ID),$subject,$exists); 353 354 $titleKey = $this->util->getTitleKey(); 355 356 $fixTitle = false; 357 358 // we only use the title determination if no explicit title was given 359 if(empty($data['data'][$titleKey])) { 360 if(!empty($data['title candidate'])) { 361 // we have a candidate from somewhere 362 $data['data'][$titleKey][] = $data['title candidate']; 363 } else { 364 if(!empty($R->meta['title'])) { 365 // we do not have a candidate, so we use the page title 366 // (this is possible because fragments set the candidate) 367 $data['data'][$titleKey][] = array( 368 'value'=>$R->meta['title'], 369 'type'=>'text', 370 'hint'=>null 371 ); 372 } else { 373 // we were added before the page title is known 374 // however, we do require a page title (iff we actually store data) 375 $fixTitle = true; 376 } 377 } 378 } 379 380 // store positions information 381 if($mode == 'preview_metadata') { 382 self::$previewMetadata[$ID]['strata']['positions'][$data['entry']][] = $data['position']; 383 } else { 384 $R->meta['strata']['positions'][$data['entry']][] = $data['position']; 385 } 386 387 // process triples 388 foreach($data['data'] as $property=>$bucket) { 389 $this->util->renderPredicate($mode, $R, $this->triples, $property); 390 391 foreach($bucket as $triple) { 392 // render values for things like backlinks 393 $type = $this->util->loadType($triple['type']); 394 $type->render($mode, $R, $this->triples, $triple['value'], $triple['hint']); 395 396 // prepare triples for storage 397 $triples[] = array('subject'=>$subject, 'predicate'=>$property, 'object'=>$triple['value']); 398 } 399 } 400 401 // we're done if nodata is flagged. 402 if(!isset($R->info['data']) || $R->info['data']==true) { 403 // batch-store triples if we're allowed to store 404 $this->triples->addTriples($triples, $ID); 405 406 // set flag for title addendum 407 if($fixTitle) { 408 $R->meta['strata']['fixTitle'] = true; 409 } 410 } 411 412 return true; 413 } 414 415 return false; 416 } 417} 418