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