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