1<?php 2/** 3 * DokuWiki Plugin strata (Helper Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Brend Wanders <b.wanders@utwente.nl> 7 */ 8 9if (!defined('DOKU_INC')) die('meh.'); 10 11/** 12 * Helper to construct and handle syntax fragments. 13 */ 14class helper_plugin_strata_syntax_RegexHelper { 15 /** 16 * Regular expression fragment table. This is used for interpolation of 17 * syntax patterns, and should be without captures. Do not assume any 18 * specific delimiter. 19 */ 20 var $regexFragments = array( 21 'variable' => '(?:\?[^\s:\(\)\[\]\{\}\<\>\|\~\!\@\#\$\%\^\&\*\?\="]+)', 22 'predicate' => '(?:[^:\(\)\[\]\{\}\<\>\|\~\!\@\#\$\%\^\&\*\?\="]+)', 23 'reflit' => '(?:\[\[[^]]*\]\])', 24 'type' => '(?:\[\s*[a-z0-9]+\s*(?:::[^\]]*)?\])', 25 'aggregate' => '(?:@\s*[a-z0-9]+(?:\([^\)]*\))?)', 26 'operator' => '(?:!=|>=|<=|>|<|=|!~>|!~|!\^~|!\$~|\^~|\$~|~>|~)', 27 'any' => '(?:.+?)' 28 ); 29 30 /** 31 * Patterns used to extract information from captured fragments. These patterns 32 * are used with '/' as delimiter, and should contain at least one capture group. 33 */ 34 var $regexCaptures = array( 35 'variable' => array('\?(.*)', array('name')), 36 'aggregate' => array('@\s*([a-z0-9]+)(?:\(([^\)]*)\))?', array('aggregate','hint')), 37 'type' => array('\[\s*([a-z0-9]+)\s*(?:::([^\]]*))?\]', array('type', 'hint')), 38 'reflit' => array('\[\[(.*)\]\]',array('reference')) 39 ); 40 41 /** 42 * Grabs the syntax fragment. 43 */ 44 function __get($name) { 45 if(array_key_exists($name, $this->regexFragments)) { 46 return $this->regexFragments[$name]; 47 } else { 48 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 49 trigger_error("Undefined syntax fragment '$name' on {$trace[0]['file']}:{$trace[0]['line']}", E_USER_NOTICE); 50 } 51 } 52 53 /** 54 * Extracts information from a fragment, based on the type. 55 */ 56 function __call($name, $arguments) { 57 if(array_key_exists($name, $this->regexCaptures)) { 58 list($pattern, $names) = $this->regexCaptures[$name]; 59 $result = preg_match("/^{$pattern}$/", $arguments[0], $match); 60 if($result === 1) { 61 array_shift($match); 62 $shortest = min(count($names), count($match)); 63 return new helper_plugin_strata_syntax_RegexHelperCapture(array_combine(array_slice($names,0,$shortest), array_slice($match, 0, $shortest))); 64 } else { 65 return null; 66 } 67 } else { 68 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 69 trigger_error("Undefined syntax capture '$name' on {$trace[0]['file']}:{$trace[0]['line']}", E_USER_NOTICE); 70 } 71 } 72} 73 74/** 75 * A single capture. Used as a return value for the RegexHelper's 76 * capture methods. 77 */ 78class helper_plugin_strata_syntax_RegexHelperCapture implements ArrayAccess { 79 function __construct($values) { 80 $this->values = $values; 81 } 82 83 function __get($name) { 84 if(array_key_exists($name, $this->values)) { 85 return $this->values[$name]; 86 } else { 87 return null; 88 } 89 } 90 91 function offsetExists($offset) { 92 // the index is valid iff: 93 // it is an existing field name 94 // it is a correct nummeric index (with 0 being the first name and count-1 the last) 95 return isset($this->values[$offset]) || ($offset >= 0 && $offset < count($this->values)); 96 } 97 98 function offsetGet($offset) { 99 // return the correct offset 100 if (isset($this->values[$offset])) { 101 return $this->values[$offset]; 102 } else { 103 // or try the numeric offsets 104 if(is_numeric($offset) && $offset >= 0 && $offset < count($this->values)) { 105 // translate numeric offset to key 106 $keys = array_keys($this->values); 107 return $this->values[$keys[intval($offset)]]; 108 } else { 109 // offset unknown, return without value 110 return; 111 } 112 } 113 } 114 115 function offsetSet($offset, $value) { 116 // noop 117 $trace = debug_backtrace(); 118 trigger_error("Syntax fragment fields are read-only on {$trace[0]['file']}:{$trace[0]['line']}", E_USER_NOTICE); 119 } 120 121 function offsetUnset($offset) { 122 // noop 123 $trace = debug_backtrace(); 124 trigger_error("Syntax fragment fields are read-only on {$trace[0]['file']}:{$trace[0]['line']}", E_USER_NOTICE); 125 } 126} 127 128/** 129 * Helper plugin for common syntax parsing. 130 */ 131class helper_plugin_strata_syntax extends DokuWiki_Plugin { 132 public static $patterns; 133 134 /** 135 * Static initializer called directly after class declaration. 136 * 137 * This static method exists because we want to keep the static $patterns 138 * and its initialization close together. 139 */ 140 static function initialize() { 141 self::$patterns = new helper_plugin_strata_syntax_RegexHelper(); 142 } 143 144 /** 145 * Constructor. 146 */ 147 function __construct() { 148 $this->util =& plugin_load('helper', 'strata_util'); 149 $this->error = ''; 150 $this->regions = array(); 151 } 152 153 /** 154 * Returns an object describing the pattern fragments. 155 */ 156 function getPatterns() { 157 return self::$patterns; 158 } 159 160 /** 161 * Determines whether a line can be ignored. 162 */ 163 function ignorableLine($line) { 164 $line = utf8_trim($line); 165 return $line == '' || utf8_substr($line,0,2) == '--'; 166 } 167 168 /** 169 * Updates the given typemap with new information. 170 * 171 * @param typemap array a typemap 172 * @param var string the name of the variable 173 * @param type string the type of the variable 174 * @param hint string the type hint of the variable 175 */ 176 function updateTypemap(&$typemap, $var, $type, $hint=null) { 177 if(empty($typemap[$var]) && $type) { 178 $typemap[$var] = array('type'=>$type,'hint'=>$hint); 179 return true; 180 } 181 182 return false; 183 } 184 185 /** 186 * Constructs a literal with the given text. 187 */ 188 function literal($val) { 189 return array('type'=>'literal', 'text'=>$val); 190 } 191 192 /** 193 * Constructs a variable with the given name. 194 */ 195 function variable($var) { 196 if($var[0] == '?') $var = substr($var,1); 197 return array('type'=>'variable', 'text'=>$var); 198 } 199 200 function _fail($message, $regions=array()) { 201 msg($message,-1); 202 203 if($this->isGroup($regions) || $this->isText($regions)) { 204 $regions = array($regions); 205 } 206 207 $lines = array(); 208 foreach($regions as $r) $lines[] = array('start'=>$r['start'], 'end'=>$r['end']); 209 throw new strata_exception($message, $lines); 210 } 211 212 /** 213 * Constructs a query from the give tree. 214 * 215 * @param root array the tree to transform 216 * @param typemap array the type information collected so far 217 * @param projection array the variables to project 218 * @return a query structure 219 */ 220 function constructQuery(&$root, &$typemap, $projection) { 221 $p = $this->getPatterns(); 222 223 $result = array( 224 'type'=>'select', 225 'group'=>array(), 226 'projection'=>$projection, 227 'ordering'=>array(), 228 'grouping'=>false, 229 'considering'=>array() 230 ); 231 232 // extract sort groups 233 $ordering = $this->extractGroups($root, 'sort'); 234 235 // extract grouping groups 236 $grouping = $this->extractGroups($root, 'group'); 237 238 // extract additional projection groups 239 $considering = $this->extractGroups($root, 'consider'); 240 241 // transform actual group 242 $where = $this->extractGroups($root, 'where'); 243 $tree = null; 244 if(count($where)==0) { 245 $tree =& $root; 246 } elseif(count($where)==1) { 247 $tree =& $where[0]; 248 if(count($root['cs'])) { 249 $this->_fail($this->getLang('error_query_outofwhere'), $root['cs']); 250 } 251 } else { 252 $this->_fail($this->getLang('error_query_singlewhere'), $where); 253 } 254 255 list($group, $scope) = $this->transformGroup($tree, $typemap); 256 $result['group'] = $group; 257 if(!$group) return false; 258 259 // handle sort groups 260 if(count($ordering)) { 261 if(count($ordering) > 1) { 262 $this->_fail($this->getLang('error_query_multisort'), $ordering); 263 } 264 265 // handle each line in the group 266 foreach($ordering[0]['cs'] as $line) { 267 if($this->isGroup($line)) { 268 $this->_fail($this->getLang('error_query_sortblock'), $line); 269 } 270 271 if(preg_match("/^({$p->variable})\s*(?:\((asc|desc)(?:ending)?\))?$/S",utf8_trim($line['text']),$match)) { 272 $var = $p->variable($match[1]); 273 if(!in_array($var->name, $scope)) { 274 $this->_fail(sprintf($this->getLang('error_query_sortvar'),utf8_tohtml(hsc($var->name))), $line); 275 } 276 277 $result['ordering'][] = array('variable'=>$var->name, 'direction'=>($match[2]?:'asc')); 278 } else { 279 $this->_fail(sprintf($this->getLang('error_query_sortline'), utf8_tohtml(hsc($line['text']))), $line); 280 } 281 } 282 } 283 284 //handle grouping 285 if(count($grouping)) { 286 if(count($grouping) > 1) { 287 $this->_fail($this->getLang('error_query_multigrouping'), $grouping); 288 } 289 290 // we have a group, so we want grouping 291 $result['grouping'] = array(); 292 293 foreach($grouping[0]['cs'] as $line) { 294 if($this->isGroup($line)) { 295 $this->_fail($this->getLang('error_query_groupblock'), $line); 296 } 297 298 if(preg_match("/({$p->variable})$/",utf8_trim($line['text']),$match)) { 299 $var = $p->variable($match[1]); 300 if(!in_array($var->name, $scope)) { 301 $this->_fail(sprintf($this->getLang('error_query_groupvar'),utf8_tohtml(hsc($var->name))), $line); 302 } 303 304 $result['grouping'][] = $var->name; 305 } else { 306 $this->_fail(sprintf($this->getLang('error_query_groupline'), utf8_tohtml(hsc($line['text']))), $line); 307 } 308 } 309 } 310 311 //handle considering 312 if(count($considering)) { 313 if(count($considering) > 1) { 314 $this->_fail($this->getLang('error_query_multiconsidering'), $considering); 315 } 316 317 foreach($considering[0]['cs'] as $line) { 318 if($this->isGroup($line)) { 319 $this->_fail($this->getLang('error_query_considerblock'), $line); 320 } 321 322 if(preg_match("/^({$p->variable})$/",utf8_trim($line['text']),$match)) { 323 $var = $p->variable($match[1]); 324 if(!in_array($var->name, $scope)) { 325 $this->_fail(sprintf($this->getLang('error_query_considervar'),utf8_tohtml(hsc($var->name))), $line); 326 } 327 328 $result['considering'][] = $var->name; 329 } else { 330 $this->_fail(sprintf($this->getLang('error_query_considerline'), utf8_tohtml(hsc($line['text']))), $line); 331 } 332 } 333 } 334 335 foreach($projection as $var) { 336 if(!in_array($var, $scope)) { 337 $this->_fail(sprintf($this->getLang('error_query_selectvar'), utf8_tohtml(hsc($var)))); 338 } 339 } 340 341 // return final query structure 342 return array($result, $scope); 343 } 344 345 /** 346 * Transforms a full query group. 347 * 348 * @param root array the tree to transform 349 * @param typemap array the type information 350 * @return the transformed group and a list of in-scope variables 351 */ 352 function transformGroup(&$root, &$typemap) { 353 // extract patterns and split them in triples and filters 354 $patterns = $this->extractText($root); 355 356 // extract union groups 357 $unions = $this->extractGroups($root, 'union'); 358 359 // extract minus groups 360 $minuses = $this->extractGroups($root,'minus'); 361 362 // extract optional groups 363 $optionals = $this->extractGroups($root,'optional'); 364 365 // check for leftovers 366 if(count($root['cs'])) { 367 $this->_fail(sprintf($this->getLang('error_query_group'),( isset($root['cs'][0]['tag']) ? sprintf($this->getLang('named_group'), utf8_tohtml(hsc($root['cs'][0]['tag']))) : $this->getLang('unnamed_group'))), $root['cs']); 368 } 369 370 // split patterns into triples and filters 371 list($patterns, $filters, $scope) = $this->transformPatterns($patterns, $typemap); 372 373 // convert each union into a pattern 374 foreach($unions as $union) { 375 list($u, $s) = $this->transformUnion($union, $typemap); 376 $scope = array_merge($scope, $s); 377 $patterns[] = $u; 378 } 379 380 if(count($patterns) == 0) { 381 $this->_fail(sprintf($this->getLang('error_query_grouppattern')), $root); 382 } 383 384 // chain all patterns with ANDs 385 $result = array_shift($patterns); 386 foreach($patterns as $pattern) { 387 $result = array( 388 'type'=>'and', 389 'lhs'=>$result, 390 'rhs'=>$pattern 391 ); 392 } 393 394 // apply all optionals 395 if(count($optionals)) { 396 foreach($optionals as $optional) { 397 // convert eacfh optional 398 list($optional, $s) = $this->transformGroup($optional, $typemap); 399 $scope = array_merge($scope, $s); 400 $result = array( 401 'type'=>'optional', 402 'lhs'=>$result, 403 'rhs'=>$optional 404 ); 405 } 406 } 407 408 409 // add all filters; these are a bit weird, as only a single FILTER is really supported 410 // (we have defined multiple filters as being a conjunction) 411 if(count($filters)) { 412 foreach($filters as $f) { 413 $line = $f['_line']; 414 unset($f['_line']); 415 if($f['lhs']['type'] == 'variable' && !in_array($f['lhs']['text'], $scope)) { 416 $this->_fail(sprintf($this->getLang('error_query_filterscope'),utf8_tohtml(hsc($f['lhs']['text']))), $line); 417 } 418 if($f['rhs']['type'] == 'variable' && !in_array($f['rhs']['text'], $scope)) { 419 $this->_fail(sprintf($this->getLang('error_query_filterscope'),utf8_tohtml(hsc($f['rhs']['text']))), $line); 420 } 421 } 422 423 $result = array( 424 'type'=>'filter', 425 'lhs'=>$result, 426 'rhs'=>$filters 427 ); 428 } 429 430 // apply all minuses 431 if(count($minuses)) { 432 foreach($minuses as $minus) { 433 // convert each minus, and discard their scope 434 list($minus, $s) = $this->transformGroup($minus, $typemap); 435 $result = array( 436 'type'=>'minus', 437 'lhs'=>$result, 438 'rhs'=>$minus 439 ); 440 } 441 } 442 443 return array($result, $scope); 444 } 445 446 /** 447 * Transforms a union group with multiple subgroups 448 * 449 * @param root array the union group to transform 450 * @param typemap array the type information 451 * @return the transformed group and a list of in-scope variables 452 */ 453 function transformUnion(&$root, &$typemap) { 454 // fetch all child patterns 455 $subs = $this->extractGroups($root,null); 456 457 // do sanity checks 458 if(count($root['cs'])) { 459 $this->_fail($this->getLang('error_query_unionblocks'), $root['cs']); 460 } 461 462 if(count($subs) < 2) { 463 $this->_fail($this->getLang('error_query_unionreq'), $root); 464 } 465 466 // transform the first group 467 list($result,$scope) = $this->transformGroup(array_shift($subs), $typemap); 468 469 // transform each subsequent group 470 foreach($subs as $sub) { 471 list($rhs, $s) = $this->transformGroup($sub, $typemap); 472 $scope = array_merge($scope, $s); 473 $result = array( 474 'type'=>'union', 475 'lhs'=>$result, 476 'rhs'=>$rhs 477 ); 478 } 479 480 return array($result, $scope); 481 } 482 483 /** 484 * Transforms a list of patterns into a list of triples and a 485 * list of filters. 486 * 487 * @param lines array a list of lines to transform 488 * @param typemap array the type information 489 * @return a list of triples, a list of filters and a list of in-scope variables 490 */ 491 function transformPatterns(&$lines, &$typemap) { 492 // we need this to resolve things 493 global $ID; 494 495 // we need patterns 496 $p = $this->getPatterns(); 497 498 // result holders 499 $scope = array(); 500 $triples = array(); 501 $filters = array(); 502 503 foreach($lines as $lineNode) { 504 $line = trim($lineNode['text']); 505 506 // [grammar] TRIPLEPATTERN := (VARIABLE|REFLIT) ' ' (VARIABLE|PREDICATE) TYPE? : ANY 507 if(preg_match("/^({$p->variable}|{$p->reflit})\s+({$p->variable}|{$p->predicate})\s*({$p->type})?\s*:\s*({$p->any})$/S",$line,$match)) { 508 list(, $subject, $predicate, $type, $object) = $match; 509 510 $subject = utf8_trim($subject); 511 if($subject[0] == '?') { 512 $subject = $this->variable($subject); 513 $scope[] = $subject['text']; 514 $this->updateTypemap($typemap, $subject['text'], 'ref'); 515 } else { 516 global $ID; 517 $subject = $p->reflit($subject)->reference; 518 $subject = $this->util->loadType('ref')->normalize($subject,null); 519 $subject = $this->literal($subject); 520 } 521 522 $predicate = utf8_trim($predicate); 523 if($predicate[0] == '?') { 524 $predicate = $this->variable($predicate); 525 $scope[] = $predicate['text']; 526 $this->updateTypemap($typemap, $predicate['text'], 'text'); 527 } else { 528 $predicate = $this->literal($this->util->normalizePredicate($predicate)); 529 } 530 531 $object = utf8_trim($object); 532 if($object[0] == '?') { 533 // match a proper type variable 534 if(preg_match("/^({$p->variable})\s*({$p->type})?$/",$object,$captures)!=1) { 535 $this->_fail($this->getLang('error_pattern_garbage'),$lineNode); 536 } 537 list(, $var, $vtype) = $captures; 538 539 // create the object node 540 $object = $this->variable($var); 541 $scope[] = $object['text']; 542 543 // try direct type first, implied type second 544 $vtype = $p->type($vtype); 545 $type = $p->type($type); 546 $this->updateTypemap($typemap, $object['text'], $vtype->type, $vtype->hint); 547 $this->updateTypemap($typemap, $object['text'], $type->type, $type->hint); 548 } else { 549 // check for empty string token 550 if($object == '[[]]') { 551 $object=''; 552 } 553 if(!$type) { 554 list($type, $hint) = $this->util->getDefaultType(); 555 } else { 556 $type = $p->type($type); 557 $hint = $type->hint; 558 $type = $type->type; 559 } 560 $type = $this->util->loadType($type); 561 $object = $this->literal($type->normalize($object,$hint)); 562 } 563 564 $triples[] = array('type'=>'triple','subject'=>$subject, 'predicate'=>$predicate, 'object'=>$object); 565 566 // [grammar] FILTER := VARIABLE TYPE? OPERATOR VARIABLE TYPE? 567 } elseif(preg_match("/^({$p->variable})\s*({$p->type})?\s*({$p->operator})\s*({$p->variable})\s*({$p->type})?$/S",$line, $match)) { 568 list(,$lhs, $ltype, $operator, $rhs, $rtype) = $match; 569 570 $lhs = $this->variable($lhs); 571 $rhs = $this->variable($rhs); 572 573 if($operator == '~>' || $operator == '!~>') $operator = str_replace('~>','^~',$operator); 574 575 // do type information propagation 576 $rtype = $p->type($rtype); 577 $ltype = $p->type($ltype); 578 579 if($ltype) { 580 // left has a defined type, so update the map 581 $this->updateTypemap($typemap, $lhs['text'], $ltype->type, $ltype->hint); 582 583 // and propagate to right if possible 584 if(!$rtype) { 585 $this->updateTypemap($typemap, $rhs['text'], $ltype->type, $lhint->hint); 586 } 587 } 588 if($rtype) { 589 // right has a defined type, so update the map 590 $this->updateTypemap($typemap, $rhs['text'], $rtype->type, $rtype->hint); 591 592 // and propagate to left if possible 593 if(!$ltype) { 594 $this->updateTypemap($typemap, $lhs['text'], $rtype->type, $rtype->hint); 595 } 596 } 597 598 $filters[] = array('type'=>'filter', 'lhs'=>$lhs, 'operator'=>$operator, 'rhs'=>$rhs, '_line'=>$lineNode); 599 600 // [grammar] FILTER := VARIABLE TYPE? OPERATOR ANY 601 } elseif(preg_match("/^({$p->variable})\s*({$p->type})?\s*({$p->operator})\s*({$p->any})$/S",$line, $match)) { 602 603 // filter pattern 604 list(, $lhs,$ltype,$operator,$rhs) = $match; 605 606 $lhs = $this->variable($lhs); 607 608 // update typemap if a type was defined 609 list($type,$hint) = $p->type($ltype); 610 if($type) { 611 $this->updateTypemap($typemap, $lhs['text'],$type,$hint); 612 } else { 613 // use the already declared type if no type was defined 614 if(!empty($typemap[$lhs['text']])) { 615 extract($typemap[$lhs['text']]); 616 } else { 617 list($type, $hint) = $this->util->getDefaultType(); 618 } 619 } 620 621 // check for empty string token 622 if($rhs == '[[]]') { 623 $rhs = ''; 624 } 625 626 // special case: the right hand side of the 'in' operator always normalizes with the 'text' type 627 if($operator == '~>' || $operator == '!~>') { 628 $operator = str_replace('~>','^~', $operator); 629 $type = 'text'; 630 unset($hint); 631 } 632 633 // normalize 634 $type = $this->util->loadType($type); 635 $rhs = $this->literal($type->normalize($rhs,$hint)); 636 637 $filters[] = array('type'=>'filter','lhs'=>$lhs, 'operator'=>$operator, 'rhs'=>$rhs, '_line'=>$lineNode); 638 639 // [grammar] FILTER := ANY OPERATOR VARIABLE TYPE? 640 } elseif(preg_match("/^({$p->any})\s*({$p->operator})\s*({$p->variable})\s*({$p->type})?$/S",$line, $match)) { 641 list(, $lhs,$operator,$rhs,$rtype) = $match; 642 643 $rhs = $this->variable($rhs); 644 645 // update typemap if a type was defined 646 list($type, $hint) = $p->type($rtype); 647 if($type) { 648 $this->updateTypemap($typemap, $rhs['text'],$type,$hint); 649 } else { 650 // use the already declared type if no type was defined 651 if(!empty($typemap[$rhs['text']])) { 652 extract($typemap[$rhs['text']]); 653 } else { 654 list($type, $hint) = $this->util->getDefaultType(); 655 } 656 } 657 658 // check for empty string token 659 if($lhs == '[[]]') { 660 $lhs = ''; 661 } 662 663 // special case: the left hand side of the 'in' operator always normalizes with the 'page' type 664 if($operator == '~>' || $operator == '!~>') { 665 $operator = str_replace('~>','^~', $operator); 666 $type = 'page'; 667 unset($hint); 668 } 669 670 // normalize 671 $type = $this->util->loadType($type); 672 $lhs = $this->literal($type->normalize($lhs,$hint)); 673 674 $filters[] = array('type'=>'filter','lhs'=>$lhs, 'operator'=>$operator, 'rhs'=>$rhs, '_line'=>$lineNode); 675 } else { 676 // unknown lines are fail 677 $this->_fail(sprintf($this->getLang('error_query_pattern'),utf8_tohtml(hsc($line))), $lineNode); 678 } 679 } 680 681 return array($triples, $filters, $scope); 682 } 683 684 function getFields(&$tree, &$typemap) { 685 $fields = array(); 686 687 // extract the projection information in 'long syntax' if available 688 $fieldsGroups = $this->extractGroups($tree, 'fields'); 689 690 // parse 'long syntax' if we don't have projection information yet 691 if(count($fieldsGroups)) { 692 if(count($fieldsGroups) > 1) { 693 $this->_fail($this->getLang('error_query_fieldsgroups'), $fieldsGroups); 694 } 695 696 $fieldsLines = $this->extractText($fieldsGroups[0]); 697 if(count($fieldsGroups[0]['cs'])) { 698 $this->_fail(sprintf($this->getLang('error_query_fieldsblock'),( isset($fieldsGroups[0]['cs'][0]['tag']) ? sprintf($this->getLang('named_group'),hsc($fieldsGroups[0]['cs'][0]['tag'])) : $this->getLang('unnamed_group'))), $fieldsGroups[0]['cs']); 699 } 700 $fields = $this->parseFieldsLong($fieldsLines, $typemap); 701 if(!$fields) return array(); 702 } 703 704 return $fields; 705 } 706 707 /** 708 * Parses a projection group in 'long syntax'. 709 */ 710 function parseFieldsLong($lines, &$typemap) { 711 $p = $this->getPatterns(); 712 $result = array(); 713 714 foreach($lines as $lineNode) { 715 $line = trim($lineNode['text']); 716 // FIELDLONG := VARIABLE AGGREGATE? TYPE? (':' ANY)? 717 if(preg_match("/^({$p->variable})\s*({$p->aggregate})?\s*({$p->type})?(?:\s*(:)\s*({$p->any})?\s*)?$/S",$line, $match)) { 718 list(, $var, $vaggregate, $vtype, $nocaphint, $caption) = $match; 719 $variable = $p->variable($var)->name; 720 if(!$nocaphint || (!$nocaphint && !$caption)) $caption = ucfirst($variable); 721 722 list($type,$hint) = $p->type($vtype); 723 list($agg,$agghint) = $p->aggregate($vaggregate); 724 725 $this->updateTypemap($typemap, $variable, $type, $hint); 726 $result[] = array('variable'=>$variable,'caption'=>$caption, 'aggregate'=>$agg, 'aggregateHint'=>$agghint, 'type'=>$type, 'hint'=>$hint); 727 } else { 728 $this->_fail(sprintf($this->getLang('error_query_fieldsline'),utf8_tohtml(hsc($line))), $lineNode); 729 } 730 } 731 732 return $result; 733 } 734 735 /** 736 * Parses a projection group in 'short syntax'. 737 */ 738 function parseFieldsShort($line, &$typemap) { 739 $p = $this->getPatterns(); 740 $result = array(); 741 742 // FIELDSHORT := VARIABLE AGGREGATE? TYPE? CAPTION? 743 if(preg_match_all("/\s*({$p->variable})\s*({$p->aggregate})?\s*({$p->type})?\s*(?:(\")([^\"]*)\")?/",$line,$match, PREG_SET_ORDER)) { 744 foreach($match as $m) { 745 list(, $var, $vaggregate, $vtype, $caption_indicator, $caption) = $m; 746 $variable = $p->variable($var)->name; 747 list($type, $hint) = $p->type($vtype); 748 list($agg, $agghint) = $p->aggregate($vaggregate); 749 if(!$caption_indicator) $caption = ucfirst($variable); 750 $this->updateTypemap($typemap, $variable, $type, $hint); 751 $result[] = array('variable'=>$variable,'caption'=>$caption, 'aggregate'=>$agg, 'aggregateHint'=>$agghint, 'type'=>$type, 'hint'=>$hint); 752 } 753 } 754 755 return $result; 756 } 757 758 /** 759 * Returns the regex pattern used by the 'short syntax' for projection. This methods can 760 * be used to get a dokuwiki-lexer-safe regex to embed into your own syntax pattern. 761 * 762 * @param captions boolean Whether the pattern should include caption matching (defaults to true) 763 */ 764 function fieldsShortPattern($captions = true) { 765 $p = $this->getPatterns(); 766 return "(?:\s*{$p->variable}\s*{$p->aggregate}?\s*{$p->type}?".($captions?'\s*(?:"[^"]*")?':'').")"; 767 } 768 769 /** 770 * Constructs a tagged tree from the given list of lines. 771 * 772 * @return a tagged tree 773 */ 774 function constructTree($lines, $what) { 775 $root = array( 776 'tag'=>'', 777 'cs'=>array(), 778 'start'=>1, 779 'end'=>1 780 ); 781 782 $stack = array(); 783 $stack[] =& $root; 784 $top = count($stack)-1; 785 $lineCount = 0; 786 787 foreach($lines as $line) { 788 $lineCount++; 789 if($this->ignorableLine($line)) continue; 790 791 if(preg_match('/^([^\{]*) *{$/',utf8_trim($line),$match)) { 792 list(, $tag) = $match; 793 $tag = utf8_trim($tag); 794 795 $stack[$top]['cs'][] = array( 796 'tag'=>$tag?:null, 797 'cs'=>array(), 798 'start'=>$lineCount, 799 'end'=>0 800 ); 801 $stack[] =& $stack[$top]['cs'][count($stack[$top]['cs'])-1]; 802 $top = count($stack)-1; 803 804 } elseif(preg_match('/^}$/',utf8_trim($line))) { 805 $stack[$top]['end'] = $lineCount; 806 array_pop($stack); 807 $top = count($stack)-1; 808 809 } else { 810 $stack[$top]['cs'][] = array( 811 'text'=>$line, 812 'start'=>$lineCount, 813 'end'=>$lineCount 814 ); 815 } 816 } 817 818 if(count($stack) != 1 || $stack[0] != $root) { 819 msg(sprintf($this->getLang('error_syntax_braces'),$what),-1); 820 } 821 822 $root['end'] = $lineCount; 823 824 return $root; 825 } 826 827 /** 828 * Renders a debug display of the syntax. 829 * 830 * @param lines array the lines that form the syntax 831 * @param region array the region to highlight 832 * @return a string with markup 833 */ 834 function debugTree($lines, $regions) { 835 $result = ''; 836 $lineCount = 0; 837 $count = 0; 838 839 foreach($lines as $line) { 840 $lineCount++; 841 842 foreach($regions as $region) { 843 if($lineCount == $region['start']) { 844 if($count == 0) $result .= '<div class="strata-debug-highlight">'; 845 $count++; 846 } 847 848 if($lineCount == $region['end']+1) { 849 $count--; 850 851 if($count==0) $result .= '</div>'; 852 } 853 } 854 855 if($line != '') { 856 $result .= '<div class="strata-debug-line">'.hsc($line).'</div>'."\n"; 857 } else { 858 $result .= '<div class="strata-debug-line"><br/></div>'."\n"; 859 } 860 } 861 862 if($count > 0) { 863 $result .= '</div>'; 864 } 865 866 return '<div class="strata-debug">'.$result.'</div>'; 867 } 868 869 /** 870 * Extract all occurences of tagged groups from the given tree. 871 * This method does not remove the tagged groups from subtrees of 872 * the given root. 873 * 874 * @param root array the tree to operate on 875 * @param tag string the tag to remove 876 * @return an array of groups 877 */ 878 function extractGroups(&$root, $tag) { 879 $result = array(); 880 $to_remove = array(); 881 foreach($root['cs'] as $i=>&$tree) { 882 if(!$this->isGroup($tree)) continue; 883 if($tree['tag'] == $tag || (($tag=='' || $tag==null) && $tree['tag'] == null) ) { 884 $result[] =& $tree; 885 $to_remove[] = $i; 886 } 887 } 888 // invert order of to_remove to always remove higher indices first 889 rsort($to_remove); 890 foreach($to_remove as $i) { 891 array_splice($root['cs'],$i,1); 892 } 893 return $result; 894 } 895 896 /** 897 * Extracts all text elements from the given tree. 898 * This method does not remove the text elements from subtrees 899 * of the root. 900 * 901 * @param root array the tree to operate on 902 * @return array an array of text elements 903 */ 904 function extractText(&$root) { 905 $result = array(); 906 $to_remove = array(); 907 foreach($root['cs'] as $i=>&$tree) { 908 if(!$this->isText($tree)) continue; 909 $result[] =& $tree; 910 $to_remove[] = $i; 911 } 912 // invert order of to_remove to always remove higher indices first 913 rsort($to_remove); 914 foreach($to_remove as $i) { 915 array_splice($root['cs'],$i,1); 916 } 917 return $result; 918 } 919 920 /** 921 * Returns whether the given node is a line. 922 */ 923 function isText(&$node) { 924 return array_key_exists('text', $node); 925 } 926 927 /** 928 * Returns whether the given node is a group. 929 */ 930 function isGroup(&$node) { 931 return array_key_exists('tag', $node); 932 } 933 934 /** 935 * Sets all properties given as '$properties' to the values parsed from '$trees'. 936 * 937 * The property array has as keys all possible properties, which are specified by its 938 * values. Such specification is an array that may have the following keys, with the 939 * described values: 940 * - choices: array of possible values, where the keys are the internally used values 941 * and the values specify synonyms for the choice, of which the first listed one 942 * is most common. For example: 'true' => array('yes', 'yeah') specifies that the 943 * user can choose 'yes' or 'yeah' (of which 'yes' is the commonly used value) and 944 * that the return value will contain 'true' if this choice was chosen. 945 * - pattern: regular expression that defines all possible values. 946 * - pattern_desc: description used for errors when a pattern is specified. 947 * - minOccur: positive integer specifying the minimum number of values, defaults to 1. 948 * - maxOccur: integer greater than or equal to minOccur, which specifies the maximum 949 * number of values, defaults to minOccur. 950 * - default: the default value (which must be a value the user is allowed to set). 951 * When default is given, this method guarantees that the property is always set, 952 * otherwise the property may not be set since all properties are optional. 953 * Either 'choices' or 'pattern' must be set (not both), all other values are optional. 954 * 955 * An example property array is as follows: 956 * array( 957 * 'example boolean' => array( 958 * 'choices' => array('y' => array('yes', 'yeah'), 'n' => array('no', 'nay')), 959 * 'minOccur' => 1, 960 * 'maxOccur' => 3, 961 * 'default' => 'yes' 962 * ), 963 * 'example natural number' => array( 964 * 'pattern' => '/^[0-9]+$/', 965 * 'pattern_desc' => $this->getLang('property_Z*') 966 * ) 967 * ) 968 * 969 * @param $properties The properties that can be set. 970 * @param $trees The trees that contain the values for these properties. 971 * @return An array with as indices the property names and as value a list of all values given for that property. 972 */ 973 function setProperties($properties, $trees) { 974 $propertyValues = array(); 975 $p = $this->getPatterns(); 976 977 foreach ($trees as $tree) { 978 $text = $this->extractText($tree); 979 foreach($text as $lineNode) { 980 $line = utf8_trim($lineNode['text']); 981 if (preg_match('/^('.$p->predicate.')(\*)?\s*:\s*('.$p->any.')$/', $line, $match)) { 982 list(, $variable, $multi, $value) = $match; 983 $this->_setPropertyValue($properties, $tree['tag'], $lineNode, $variable, !empty($multi), $value, $propertyValues); 984 } else { 985 $this->emitError($lineNode, 'error_property_weirdgroupline', hsc($tree['tag']), hsc($line)); 986 } 987 } 988 // Warn about unknown groups 989 foreach ($tree['cs'] as $group) { 990 $this->emitError($group, 'error_property_unknowngroup', hsc($trees[0]['tag']), hsc($group['tag'])); 991 } 992 } 993 994 // Set property defaults 995 foreach ($properties as $name => $p) { 996 if (!isset($propertyValues[$name]) && isset($p['default'])) { 997 $this->_setPropertyValue($properties, 'default value', null, $name, false, $p['default'], $propertyValues); 998 } 999 } 1000 1001 // Show errors, if any 1002 $this->showErrors(); 1003 1004 return $propertyValues; 1005 } 1006 1007 function _setPropertyValue($properties, $group, $region, $variable, $isMulti, $value, &$propertyValues) { 1008 if (!isset($properties[$variable])) { 1009 // Unknown property: show error 1010 $property_title_values = $this->getLang('property_title_values'); 1011 $propertyList = implode(', ', array_map(function ($n, $p) use ($property_title_values) { 1012 $values = implode(', ', array_map(function ($c) { 1013 return $c[0]; 1014 }, $p['choices'])); 1015 $title = sprintf($property_title_values, $values); 1016 return '\'<code title="' . hsc($title) . '">' . hsc($n) . '</code>\''; 1017 }, array_keys($properties), $properties)); 1018 $this->emitError($region, 'error_property_unknownproperty', hsc($group), hsc($variable), $propertyList); 1019 } else if (isset($propertyValues[$variable])) { 1020 // Property is specified more than once: show error 1021 $this->emitError($region, 'error_property_multi', hsc($group), hsc($variable)); 1022 } else { 1023 $p = $properties[$variable]; 1024 $minOccur = isset($p['minOccur']) ? $p['minOccur'] : 1; 1025 $maxOccur = isset($p['maxOccur']) ? $p['maxOccur'] : $minOccur; 1026 1027 if ($isMulti) { 1028 $values = array_map('utf8_trim', explode(',', $value)); 1029 } else if ($minOccur == 1 || $minOccur == $maxOccur) { 1030 // Repeat the given value as often as we expect it 1031 $values = array_fill(0, $minOccur, $value); 1032 } else { 1033 // A single value was given, but multiple were expected 1034 $this->emitError($region, 'error_property_notmulti', hsc($group), hsc($variable), $minOccur); 1035 return; 1036 } 1037 1038 if (count($values) < $minOccur || count($values) > $maxOccur) { 1039 // Number of values given differs from expected number 1040 if ($minOccur == $maxOccur) { 1041 $this->emitError($region, 'error_property_occur', hsc($group), hsc($variable), $minOccur, count($values)); 1042 } else { 1043 $this->emitError($region, 'error_property_occurrange', hsc($group), hsc($variable), $minOccur, $maxOccur, count($values)); 1044 } 1045 } else if (isset($p['choices'])) { // Check whether the given property values are valid choices 1046 // Create a mapping from choice to normalized value of the choice 1047 $choices = array(); 1048 $choicesInfo = array(); // For nice error messages 1049 foreach ($p['choices'] as $nc => $c) { 1050 if (is_array($c)) { 1051 $choices = array_merge($choices, array_fill_keys($c, $nc)); 1052 $title = sprintf($this->getLang('property_title_synonyms'), implode(', ', $c)); 1053 $choicesInfo[] = '\'<code title="' . hsc($title) . '">' . hsc($c[0]) . '</code>\''; 1054 } else { 1055 $choices[$c] = $c; 1056 $choicesInfo[] = '\'<code>' . hsc($c) . '</code>\''; 1057 } 1058 } 1059 if (!isset($choices['']) && isset($p['default'])) { 1060 $choices[''] = $choices[$p['default']]; 1061 } 1062 1063 $incorrect = array_diff($values, array_keys($choices)); // Find all values that are not a valid choice 1064 if (count($incorrect) > 0) { 1065 unset($choices['']); 1066 foreach (array_unique($incorrect) as $v) { 1067 $this->emitError($region, 'error_property_invalidchoice', hsc($group), hsc($variable), hsc($v), implode(', ', $choicesInfo)); 1068 } 1069 } else { 1070 $propertyValues[$variable] = array_map(function($v) use ($choices) { return $choices[$v]; }, $values); 1071 } 1072 } else if (isset($p['pattern'])) { // Check whether the given property values match the pattern 1073 $incorrect = array_filter($values, function($v) use ($p) { return !preg_match($p['pattern'], $v); }); 1074 if (count($incorrect) > 0) { 1075 foreach (array_unique($incorrect) as $v) { 1076 if (isset($p['pattern_desc'])) { 1077 $this->emitError($region, 'error_property_patterndesc', hsc($group), hsc($variable), hsc($v), $p['pattern_desc']); 1078 } else { 1079 $this->emitError($region, 'error_property_pattern', hsc($group), hsc($variable), hsc($v), hsc($p['pattern'])); 1080 } 1081 } 1082 } else { 1083 $propertyValues[$variable] = $values; 1084 } 1085 } else { // Property value has no requirements 1086 $propertyValues[$variable] = $values; 1087 } 1088 } 1089 } 1090 1091 /** 1092 * Generates a html error message, ensuring that all utf8 in arguments is escaped correctly. 1093 * The generated messages might be accumulated until showErrors is called. 1094 * 1095 * @param region The region at which the error occurs. 1096 * @param msg_id The id of the message in the language file. 1097 */ 1098 function emitError($region, $msg_id) { 1099 $args = func_get_args(); 1100 array_shift($args); 1101 array_shift($args); 1102 $args = array_map('strval', $args); // convert everything to strings first 1103 $args = array_map('utf8_tohtml', $args); // Escape args 1104 $msg = vsprintf($this->getLang($msg_id), $args); 1105 msg($msg, -1); 1106 $this->error .= "<br />\n" . $msg; 1107 $this->regions[] = $region; 1108 } 1109 1110 /** 1111 * Ensures that all emitted errors are shown. 1112 */ 1113 function showErrors() { 1114 if (!empty($this->error)) { 1115 $error = $this->error; 1116 $regions = $this->regions; 1117 $this->error = ''; 1118 $this->regions = array(); 1119 throw new strata_exception($error, $regions); 1120 } 1121 } 1122} 1123 1124// call static initiliazer (PHP doesn't offer this feature) 1125helper_plugin_strata_syntax::initialize(); 1126