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