xref: /plugin/strata/syntax/entry.php (revision 0847ebd29a490ea4bc8c536bb9f6dda8b6bbaa1a)
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