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