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