15153720fSfkaag71<?php 25153720fSfkaag71/** 35153720fSfkaag71 * Strata, data entry plugin 45153720fSfkaag71 * 55153720fSfkaag71 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 65153720fSfkaag71 * @author Brend Wanders <b.wanders@utwente.nl> 75153720fSfkaag71 */ 85153720fSfkaag71 95153720fSfkaag71if (!defined('DOKU_INC')) die('Meh.'); 105153720fSfkaag71 115153720fSfkaag71/** 125153720fSfkaag71 * Data entry syntax for dedicated data blocks. 135153720fSfkaag71 */ 145153720fSfkaag71class syntax_plugin_strata_entry extends DokuWiki_Syntax_Plugin { 155153720fSfkaag71 protected static $previewMetadata = array(); 165153720fSfkaag71 175153720fSfkaag71 function __construct() { 185153720fSfkaag71 $this->syntax =& plugin_load('helper', 'strata_syntax'); 195153720fSfkaag71 $this->util =& plugin_load('helper', 'strata_util'); 205153720fSfkaag71 $this->triples =& plugin_load('helper', 'strata_triples'); 215153720fSfkaag71 } 225153720fSfkaag71 235153720fSfkaag71 function getType() { 245153720fSfkaag71 return 'substition'; 255153720fSfkaag71 } 265153720fSfkaag71 275153720fSfkaag71 function getPType() { 285153720fSfkaag71 return 'block'; 295153720fSfkaag71 } 305153720fSfkaag71 315153720fSfkaag71 function getSort() { 325153720fSfkaag71 return 450; 335153720fSfkaag71 } 345153720fSfkaag71 355153720fSfkaag71 function connectTo($mode) { 365153720fSfkaag71 if($this->getConf('enable_entry')) { 375153720fSfkaag71 $this->Lexer->addSpecialPattern('<data(?: +[^#>]+?)?(?: *#[^>]*?)?>\s*?\n(?:.*?\n)*?\s*?</data>',$mode, 'plugin_strata_entry'); 385153720fSfkaag71 } 395153720fSfkaag71 } 405153720fSfkaag71 415153720fSfkaag71 function handle($match, $state, $pos, Doku_Handler $handler) { 425153720fSfkaag71 $result = array( 435153720fSfkaag71 'entry'=>'', 445153720fSfkaag71 'data'=> array( 455153720fSfkaag71 $this->util->getIsaKey(false) => array(), 465153720fSfkaag71 $this->util->getTitleKey(false) => array() 475153720fSfkaag71 ) 485153720fSfkaag71 ); 495153720fSfkaag71 505153720fSfkaag71 // allow for preprocessing by a subclass 515153720fSfkaag71 $match = $this->preprocess($match, $state, $pos, $handler, $result); 525153720fSfkaag71 535153720fSfkaag71 $lines = explode("\n",$match); 545153720fSfkaag71 $header = trim(array_shift($lines)); 555153720fSfkaag71 $footer = trim(array_pop($lines)); 565153720fSfkaag71 575153720fSfkaag71 585153720fSfkaag71 // allow subclasses to mangle header 595153720fSfkaag71 $header = $this->handleHeader($header, $result); 605153720fSfkaag71 615153720fSfkaag71 // extract header, and match it to get classes and fragment 625153720fSfkaag71 preg_match('/^( +[^#>]+)?(?: *#([^>]*?))?$/', $header, $header); 635153720fSfkaag71 645153720fSfkaag71 // process the classes into triples 65*0847ebd2SFKaag if (isset($header[1])) 66*0847ebd2SFKaag { 675153720fSfkaag71 foreach(preg_split('/\s+/',trim($header[1])) as $class) { 685153720fSfkaag71 if($class == '') continue; 695153720fSfkaag71 $result['data'][$this->util->getIsaKey(false)][] = array('value'=>$class,'type'=>'text', 'hint'=>null); 705153720fSfkaag71 } 71*0847ebd2SFKaag } 725153720fSfkaag71 735153720fSfkaag71 // process the fragment if necessary 74*0847ebd2SFKaag $result['entry'] = $header[2]??null; 755153720fSfkaag71 $result['position'] = $pos; 765153720fSfkaag71 if($result['entry'] != '') { 775153720fSfkaag71 $result['title candidate'] = array('value'=>$result['entry'], 'type'=>'text', 'hint'=>null); 785153720fSfkaag71 } 795153720fSfkaag71 805153720fSfkaag71 // parse tree 815153720fSfkaag71 $tree = $this->syntax->constructTree($lines,'data entry'); 825153720fSfkaag71 835153720fSfkaag71 // allow subclasses first pick in the tree 845153720fSfkaag71 $this->handleBody($tree, $result); 855153720fSfkaag71 865153720fSfkaag71 // fetch all lines 875153720fSfkaag71 $lines = $this->syntax->extractText($tree); 885153720fSfkaag71 895153720fSfkaag71 // sanity check 905153720fSfkaag71 if(count($tree['cs'])) { 915153720fSfkaag71 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); 925153720fSfkaag71 return array(); 935153720fSfkaag71 } 945153720fSfkaag71 955153720fSfkaag71 $p = $this->syntax->getPatterns(); 965153720fSfkaag71 975153720fSfkaag71 // now handle all lines 985153720fSfkaag71 foreach($lines as $line) { 995153720fSfkaag71 $line = $line['text']; 1005153720fSfkaag71 // match a "property_type(hint)*: value" pattern 1015153720fSfkaag71 // (the * is only used to indicate that the value is actually a comma-seperated list) 1025153720fSfkaag71 // [grammar] ENTRY := PREDICATE TYPE? '*'? ':' ANY 1035153720fSfkaag71 if(preg_match("/^({$p->predicate})\s*({$p->type})?\s*(\*)?\s*:\s*({$p->any}?)$/",$line,$parts)) { 1045153720fSfkaag71 // assign useful names 1055153720fSfkaag71 list(, $property, $ptype, $multi, $values) = $parts; 1065153720fSfkaag71 list($type,$hint) = $p->type($ptype); 1075153720fSfkaag71 1085153720fSfkaag71 // trim property so we don't get accidental 'name ' keys 1095153720fSfkaag71 $property = utf8_trim($property); 1105153720fSfkaag71 1115153720fSfkaag71 // lazy create key bucket 1125153720fSfkaag71 if(!isset($result['data'][$property])) { 1135153720fSfkaag71 $result['data'][$property] = array(); 1145153720fSfkaag71 } 1155153720fSfkaag71 1165153720fSfkaag71 // determine values, splitting on commas if necessary 1175153720fSfkaag71 $values = ($multi == '*') ? explode(',',$values) : array($values); 1185153720fSfkaag71 1195153720fSfkaag71 // generate triples from the values 1205153720fSfkaag71 foreach($values as $v) { 1215153720fSfkaag71 $v = utf8_trim($v); 1225153720fSfkaag71 if($v == '') continue; 1235153720fSfkaag71 // replace the [[]] quasi-magic token with the empty string 1245153720fSfkaag71 if($v == '[[]]') $v = ''; 1255153720fSfkaag71 if(!isset($type) || $type == '') { 1265153720fSfkaag71 list($type, $hint) = $this->util->getDefaultType(); 1275153720fSfkaag71 } 1285153720fSfkaag71 $result['data'][$property][] = array('value'=>$v,'type'=>$type,'hint'=>($hint?:null)); 1295153720fSfkaag71 } 1305153720fSfkaag71 } else { 1315153720fSfkaag71 msg(sprintf($this->syntax->getLang('error_entry_line'), utf8_tohtml(hsc($line))),-1); 1325153720fSfkaag71 } 1335153720fSfkaag71 } 1345153720fSfkaag71 1355153720fSfkaag71 // normalize data: 1365153720fSfkaag71 // - Normalize all values 1375153720fSfkaag71 $buckets = $result['data']; 1385153720fSfkaag71 $result['data'] = array(); 1395153720fSfkaag71 1405153720fSfkaag71 foreach($buckets as $property=>&$bucket) { 1415153720fSfkaag71 // normalize the predicate 1425153720fSfkaag71 $property = $this->util->normalizePredicate($property); 1435153720fSfkaag71 1445153720fSfkaag71 // process all triples 1455153720fSfkaag71 foreach($bucket as &$triple) { 1465153720fSfkaag71 // normalize the value 1475153720fSfkaag71 $type = $this->util->loadType($triple['type']); 1485153720fSfkaag71 $triple['value'] = $type->normalize($triple['value'], $triple['hint']); 1495153720fSfkaag71 1505153720fSfkaag71 // lazy create property bucket 1515153720fSfkaag71 if(!isset($result['data'][$property])) { 1525153720fSfkaag71 $result['data'][$property] = array(); 1535153720fSfkaag71 } 1545153720fSfkaag71 1555153720fSfkaag71 $result['data'][$property][] = $triple; 1565153720fSfkaag71 } 1575153720fSfkaag71 } 1585153720fSfkaag71 1595153720fSfkaag71 1605153720fSfkaag71 // normalize title candidate 1615153720fSfkaag71 if(!empty($result['title candidate'])) { 1625153720fSfkaag71 $type = $this->util->loadType($result['title candidate']['type']); 1635153720fSfkaag71 $result['title candidate']['value'] = $type->normalize($result['title candidate']['value'], $result['title candidate']['hint']); 1645153720fSfkaag71 } 1655153720fSfkaag71 1665153720fSfkaag71 $footer = $this->handleFooter($footer, $result); 1675153720fSfkaag71 1685153720fSfkaag71 return $result; 1695153720fSfkaag71 } 1705153720fSfkaag71 1715153720fSfkaag71 /** 1725153720fSfkaag71 * Handles the whole match. This method is called before any processing 1735153720fSfkaag71 * is done by the actual class. 1745153720fSfkaag71 * 1755153720fSfkaag71 * @param match string the complete match 1765153720fSfkaag71 * @param state the parser state 1775153720fSfkaag71 * @param pos the position in the source 1785153720fSfkaag71 * @param the handler object 1795153720fSfkaag71 * @param result array the result array passed to the render method 1805153720fSfkaag71 * @return a preprocessed string 1815153720fSfkaag71 */ 1825153720fSfkaag71 function preprocess($match, $state, $pos, &$handler, &$result) { 1835153720fSfkaag71 return $match; 1845153720fSfkaag71 } 1855153720fSfkaag71 1865153720fSfkaag71 /** 1875153720fSfkaag71 * Handles the header of the syntax. This method is called before 1885153720fSfkaag71 * the header is handled. 1895153720fSfkaag71 * 1905153720fSfkaag71 * @param header string the complete header 1915153720fSfkaag71 * @param result array the result array passed to the render method 1925153720fSfkaag71 * @return a string containing the unhandled parts of the header 1935153720fSfkaag71 */ 1945153720fSfkaag71 function handleHeader($header, &$result) { 1955153720fSfkaag71 // remove prefix and suffix 1965153720fSfkaag71 return preg_replace('/(^<data)|( *>$)/','',$header); 1975153720fSfkaag71 } 1985153720fSfkaag71 1995153720fSfkaag71 /** 2005153720fSfkaag71 * Handles the body of the syntax. This method is called before any 2015153720fSfkaag71 * of the body is handled. 2025153720fSfkaag71 * 2035153720fSfkaag71 * @param tree array the parsed tree 2045153720fSfkaag71 * @param result array the result array passed to the render method 2055153720fSfkaag71 */ 2065153720fSfkaag71 function handleBody(&$tree, &$result) { 2075153720fSfkaag71 } 2085153720fSfkaag71 2095153720fSfkaag71 /** 2105153720fSfkaag71 * Handles the footer of the syntax. This method is called after the 2115153720fSfkaag71 * data has been parsed and normalized. 2125153720fSfkaag71 * 2135153720fSfkaag71 * @param footer string the footer string 2145153720fSfkaag71 * @param result array the result array passed to the render method 2155153720fSfkaag71 * @return a string containing the unhandled parts of the footer 2165153720fSfkaag71 */ 2175153720fSfkaag71 function handleFooter($footer, &$result) { 2185153720fSfkaag71 return ''; 2195153720fSfkaag71 } 2205153720fSfkaag71 2215153720fSfkaag71 2225153720fSfkaag71 protected function getPositions($data) { 2235153720fSfkaag71 global $ID; 2245153720fSfkaag71 2255153720fSfkaag71 // determine positions of other data entries 2265153720fSfkaag71 // (self::$previewMetadata is only filled if a preview_metadata was run) 2275153720fSfkaag71 if(isset(self::$previewMetadata[$ID])) { 2285153720fSfkaag71 $positions = self::$previewMetadata[$ID]['strata']['positions']; 2295153720fSfkaag71 } else { 2305153720fSfkaag71 $positions = p_get_metadata($ID, 'strata positions'); 2315153720fSfkaag71 } 2325153720fSfkaag71 2335153720fSfkaag71 // only read positions if we have them 2345153720fSfkaag71 if(is_array($positions) && isset($positions[$data['entry']])) { 2355153720fSfkaag71 $positions = $positions[$data['entry']]; 2365153720fSfkaag71 $currentPosition = array_search($data['position'],$positions); 2375153720fSfkaag71 $previousPosition = isset($positions[$currentPosition-1])?'data_fragment_'.$positions[$currentPosition-1]:null; 2385153720fSfkaag71 $nextPosition = isset($positions[$currentPosition+1])?'data_fragment_'.$positions[$currentPosition+1]:null; 2395153720fSfkaag71 $currentPosition = 'data_fragment_'.$positions[$currentPosition]; 2405153720fSfkaag71 } 2415153720fSfkaag71 2425153720fSfkaag71 return array($currentPosition, $previousPosition, $nextPosition); 2435153720fSfkaag71 } 2445153720fSfkaag71 2455153720fSfkaag71 function render($mode, Doku_Renderer $R, $data) { 2465153720fSfkaag71 global $ID; 2475153720fSfkaag71 2485153720fSfkaag71 if($data == array()) { 2495153720fSfkaag71 return false; 2505153720fSfkaag71 } 2515153720fSfkaag71 2525153720fSfkaag71 if($mode == 'xhtml' || $mode == 'odt') { 2535153720fSfkaag71 list($currentPosition, $previousPosition, $nextPosition) = $this->getPositions($data); 2545153720fSfkaag71 // render table header 2555153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '<div class="strata-entry" '.(isset($currentPosition)?'id="'.$currentPosition.'"':'').'>'; } 2565153720fSfkaag71 if($mode == 'odt' && isset($currentPosition) && method_exists ($R, 'insertBookmark')) { 2575153720fSfkaag71 $R->insertBookmark($currentPosition, false); 2585153720fSfkaag71 } 2595153720fSfkaag71 $R->table_open(); 2605153720fSfkaag71 $R->tablerow_open(); 2615153720fSfkaag71 $R->tableheader_open(2); 2625153720fSfkaag71 2635153720fSfkaag71 // determine actual header text 2645153720fSfkaag71 $heading = ''; 2655153720fSfkaag71 if(isset($data['data'][$this->util->getTitleKey()])) { 2665153720fSfkaag71 // use title triple if possible 2675153720fSfkaag71 $heading = $data['data'][$this->util->getTitleKey()][0]['value']; 2685153720fSfkaag71 } elseif (!empty($data['title candidate'])) { 2695153720fSfkaag71 // use title candidate if possible 2705153720fSfkaag71 $heading = $data['title candidate']['value']; 2715153720fSfkaag71 } else { 2725153720fSfkaag71 if(useHeading('content')) { 2735153720fSfkaag71 // fall back to page title, depending on wiki configuration 2745153720fSfkaag71 $heading = p_get_first_heading($ID); 2755153720fSfkaag71 } 2765153720fSfkaag71 2775153720fSfkaag71 if(!$heading) { 2785153720fSfkaag71 // use page id if all else fails 2795153720fSfkaag71 $heading = noNS($ID); 2805153720fSfkaag71 } 2815153720fSfkaag71 } 2825153720fSfkaag71 $R->cdata($heading); 2835153720fSfkaag71 2845153720fSfkaag71 // display a comma-separated list of classes if the entry has classes 2855153720fSfkaag71 if(isset($data['data'][$this->util->getIsaKey()])) { 2865153720fSfkaag71 $R->emphasis_open(); 2875153720fSfkaag71 $R->cdata(' ('); 2885153720fSfkaag71 $values = $data['data'][$this->util->getIsaKey()]; 2895153720fSfkaag71 $this->util->openField($mode, $R, $this->util->getIsaKey()); 2905153720fSfkaag71 for($i=0;$i<count($values);$i++) { 2915153720fSfkaag71 $triple =& $values[$i]; 2925153720fSfkaag71 if($i!=0) $R->cdata(', '); 2935153720fSfkaag71 $type = $this->util->loadType($triple['type']); 2945153720fSfkaag71 $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $type, $triple['hint']); 2955153720fSfkaag71 } 2965153720fSfkaag71 $this->util->closeField($mode, $R); 2975153720fSfkaag71 $R->cdata(')'); 2985153720fSfkaag71 $R->emphasis_close(); 2995153720fSfkaag71 } 3005153720fSfkaag71 $R->tableheader_close(); 3015153720fSfkaag71 $R->tablerow_close(); 3025153720fSfkaag71 3035153720fSfkaag71 // render a row for each key, displaying the values as comma-separated list 3045153720fSfkaag71 foreach($data['data'] as $key=>$values) { 3055153720fSfkaag71 // skip isa and title keys 3065153720fSfkaag71 if($key == $this->util->getTitleKey() || $key == $this->util->getIsaKey()) continue; 3075153720fSfkaag71 3085153720fSfkaag71 // render row header 3095153720fSfkaag71 $R->tablerow_open(); 3105153720fSfkaag71 $R->tableheader_open(); 3115153720fSfkaag71 $this->util->renderPredicate($mode, $R, $this->triples, $key); 3125153720fSfkaag71 $R->tableheader_close(); 3135153720fSfkaag71 3145153720fSfkaag71 // render row content 3155153720fSfkaag71 $R->tablecell_open(); 3165153720fSfkaag71 $this->util->openField($mode, $R, $key); 3175153720fSfkaag71 for($i=0;$i<count($values);$i++) { 3185153720fSfkaag71 $triple =& $values[$i]; 3195153720fSfkaag71 if($i!=0) $R->cdata(', '); 3205153720fSfkaag71 $this->util->renderValue($mode, $R, $this->triples, $triple['value'], $triple['type'], $triple['hint']); 3215153720fSfkaag71 } 3225153720fSfkaag71 $this->util->closeField($mode, $R); 3235153720fSfkaag71 $R->tablecell_close(); 3245153720fSfkaag71 $R->tablerow_close(); 3255153720fSfkaag71 } 3265153720fSfkaag71 3275153720fSfkaag71 if($previousPosition || $nextPosition) { 3285153720fSfkaag71 $R->tablerow_open(); 3295153720fSfkaag71 $R->tableheader_open(2); 3305153720fSfkaag71 if($previousPosition) { 3315153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-previous">'; } 3325153720fSfkaag71 $R->locallink($previousPosition, $this->util->getLang('data_entry_previous')); 3335153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '</span>'; } 3345153720fSfkaag71 } 3355153720fSfkaag71 $R->cdata(' '); 3365153720fSfkaag71 if($nextPosition) { 3375153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '<span class="strata-data-fragment-link-next">'; } 3385153720fSfkaag71 $R->locallink($nextPosition, $this->util->getLang('data_entry_next')); 3395153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '</span>'; } 3405153720fSfkaag71 } 3415153720fSfkaag71 $R->tableheader_close(); 3425153720fSfkaag71 $R->tablerow_close(); 3435153720fSfkaag71 } 3445153720fSfkaag71 3455153720fSfkaag71 $R->table_close(); 3465153720fSfkaag71 if($mode == 'xhtml') { $R->doc .= '</div>'; } 3475153720fSfkaag71 3485153720fSfkaag71 return true; 3495153720fSfkaag71 3505153720fSfkaag71 } elseif($mode == 'metadata' || $mode == 'preview_metadata') { 3515153720fSfkaag71 $triples = array(); 3525153720fSfkaag71 $subject = $ID.'#'.$data['entry']; 3535153720fSfkaag71 3545153720fSfkaag71 // resolve the subject to normalize everything 3555153720fSfkaag71 resolve_pageid(getNS($ID),$subject,$exists); 3565153720fSfkaag71 3575153720fSfkaag71 $titleKey = $this->util->getTitleKey(); 3585153720fSfkaag71 3595153720fSfkaag71 $fixTitle = false; 3605153720fSfkaag71 3615153720fSfkaag71 // we only use the title determination if no explicit title was given 3625153720fSfkaag71 if(empty($data['data'][$titleKey])) { 3635153720fSfkaag71 if(!empty($data['title candidate'])) { 3645153720fSfkaag71 // we have a candidate from somewhere 3655153720fSfkaag71 $data['data'][$titleKey][] = $data['title candidate']; 3665153720fSfkaag71 } else { 3675153720fSfkaag71 if(!empty($R->meta['title'])) { 3685153720fSfkaag71 // we do not have a candidate, so we use the page title 3695153720fSfkaag71 // (this is possible because fragments set the candidate) 3705153720fSfkaag71 $data['data'][$titleKey][] = array( 3715153720fSfkaag71 'value'=>$R->meta['title'], 3725153720fSfkaag71 'type'=>'text', 3735153720fSfkaag71 'hint'=>null 3745153720fSfkaag71 ); 3755153720fSfkaag71 } else { 3765153720fSfkaag71 // we were added before the page title is known 3775153720fSfkaag71 // however, we do require a page title (iff we actually store data) 3785153720fSfkaag71 $fixTitle = true; 3795153720fSfkaag71 } 3805153720fSfkaag71 } 3815153720fSfkaag71 } 3825153720fSfkaag71 3835153720fSfkaag71 // store positions information 3845153720fSfkaag71 if($mode == 'preview_metadata') { 3855153720fSfkaag71 self::$previewMetadata[$ID]['strata']['positions'][$data['entry']][] = $data['position']; 3865153720fSfkaag71 } else { 3875153720fSfkaag71 $R->meta['strata']['positions'][$data['entry']][] = $data['position']; 3885153720fSfkaag71 } 3895153720fSfkaag71 3905153720fSfkaag71 // process triples 3915153720fSfkaag71 foreach($data['data'] as $property=>$bucket) { 3925153720fSfkaag71 $this->util->renderPredicate($mode, $R, $this->triples, $property); 3935153720fSfkaag71 3945153720fSfkaag71 foreach($bucket as $triple) { 3955153720fSfkaag71 // render values for things like backlinks 3965153720fSfkaag71 $type = $this->util->loadType($triple['type']); 3975153720fSfkaag71 $type->render($mode, $R, $this->triples, $triple['value'], $triple['hint']); 3985153720fSfkaag71 3995153720fSfkaag71 // prepare triples for storage 4005153720fSfkaag71 $triples[] = array('subject'=>$subject, 'predicate'=>$property, 'object'=>$triple['value']); 4015153720fSfkaag71 } 4025153720fSfkaag71 } 4035153720fSfkaag71 4045153720fSfkaag71 // we're done if nodata is flagged. 4055153720fSfkaag71 if(!isset($R->info['data']) || $R->info['data']==true) { 4065153720fSfkaag71 // batch-store triples if we're allowed to store 4075153720fSfkaag71 $this->triples->addTriples($triples, $ID); 4085153720fSfkaag71 4095153720fSfkaag71 // set flag for title addendum 4105153720fSfkaag71 if($fixTitle) { 4115153720fSfkaag71 $R->meta['strata']['fixTitle'] = true; 4125153720fSfkaag71 } 4135153720fSfkaag71 } 4145153720fSfkaag71 4155153720fSfkaag71 return true; 4165153720fSfkaag71 } 4175153720fSfkaag71 4185153720fSfkaag71 return false; 4195153720fSfkaag71 } 4205153720fSfkaag71} 421