1<?php
2/**
3 * DokuWiki Plugin json (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Janez Paternoster <janez.paternoster@siol.net>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14class syntax_plugin_json_extract extends DokuWiki_Syntax_Plugin
15{
16    /**
17     * @return string Syntax mode type
18     */
19    public function getType() {
20        return 'substition';
21    }
22
23    /**
24     * @return string Paragraph type
25     */
26    public function getPType() {
27        return 'normal';
28    }
29
30    /**
31     * @return int Sort order - Low numbers go before high numbers
32     */
33    public function getSort() {
34        return 150;
35    }
36
37    /**
38     * Connect lookup pattern to lexer.
39     *
40     * @param string $mode Parser mode
41     */
42    public function connectTo($mode) {
43        // %$link.to.0.var{"header 1[ß]":link.to.varx; "header2" : link2}%
44        $this->Lexer->addSpecialPattern('\%\$.*?\%', $mode, 'plugin_json_extract');
45    }
46
47
48    /**
49     * Handle matches of the json syntax
50     *
51     * @param string       $match   The match of the syntax
52     * @param int          $state   The state of the handler
53     * @param int          $pos     The position in the document
54     * @param Doku_Handler $handler The handler
55     *
56     * @return array Data for the renderer - tokens(always), (one or none from) code or list or table.
57     *     [[tokens] => [tok1, tok2, ...],
58     *      [code] => boolean,
59     *      [list] => [
60     *          name1 => [tok3, tok4, ...],
61     *          name2 => [tok5, tok6, ...],
62     *          ...
63     *      ]
64     *      [table] => [
65     *          name1 => [tok3, tok4, ...],
66     *          name2 => [tok5, tok6, ...],
67     *          ...
68     *      ]]
69     */
70    public function handle($match, $state, $pos, Doku_Handler $handler) {
71        $json_o = $this->loadHelper('json');
72
73        //Return value
74        $data = array('match' => $match);
75
76        //Replace #@macro_name@# patterns with strings defined by textinsert Plugin.
77        $match = $json_o->preprocess($match);
78
79        /* match %$path [(row_filter)] {header} #format# (filter)%
80         *
81         * path - path.to.variable
82         * [] - print table, optionally use (row_filter)
83         * {header} - header description for table or links
84         * #format# - format specifier for variable
85         * (filter) - render variable only, if filter is evaluated to true
86         *
87         * for help on PCRE see: https://regexr.com/  */
88        preg_match('/^%\$([^[\]{}#\(\)]*)(\[(?:\(.*?\))?\s*\])?\s*(\{.*?\})?\s*(#.*?#)?\s*(\(.*?\))?\s*%$/', $match, $mt);
89        list(, $path, $table, $header, $format, $filter) = array_pad($mt, 6, '');
90
91        // remove # from format
92        if($format) {
93            $format = substr($format, 1, -1);
94        }
95
96        $data['tokens'] = $json_o->parse_tokens($path);
97
98        if($filter) {
99            $data['filter'] = $json_o->parse_filter(substr($filter, 1, -1));
100        }
101
102
103        //table
104        if($table) {
105            $data['type'] = 'table_without_header';
106            $table_filter = trim(substr($table, 1, -1)); //remove []
107            if(strlen($table_filter) > 0) {
108                $table_filter = substr(trim($table_filter), 1, -1); //remove ()
109                $data['table_filter'] = $json_o->parse_filter($table_filter);
110            }
111            if($header) {
112                $table_header = $json_o->parse_links(substr($header, 1, -1));
113                if($table_header !== false) {
114                    $data['type'] = 'table_with_header';
115                    $data['table_header'] = $table_header;
116                    $data['format'] = array();
117                    if($format) {
118                        $format_pairs = $json_o->parse_key_val($format, ':', ',');
119                        if(is_array($format_pairs)) {
120                            foreach ($format_pairs as &$val) {
121                                $val = $this->handle_format($val);
122                            }
123                            $data['format'] = $format_pairs;
124                        }
125                    }
126                }
127            }
128        }
129
130        //list
131        else if($header) {
132            $list_header = $json_o->parse_links(substr($header, 1, -1));
133            if($list_header !== false) {
134                $data['type'] = 'list';
135                $data['list_header'] = $list_header;
136                $data['format'] = array();
137                if($format) {
138                    $format_pairs = $json_o->parse_key_val($format, ':', ',');
139                    if(is_array($format_pairs)) {
140                        foreach ($format_pairs as &$val) {
141                            $val = $this->handle_format($val);
142                        }
143                        $data['format'] = $format_pairs;
144                    }
145                }
146            }
147        }
148
149        //variable
150        else {
151            $data['type'] = 'variable';
152            $data['format'] = $format ? $this->handle_format($format) : '';
153        }
154
155        return $data;
156    }
157
158
159    /**
160     * Render xhtml output or metadata
161     *
162     * @param string        $mode     Renderer mode (supported modes: xhtml)
163     * @param Doku_Renderer $renderer The renderer
164     * @param array         $data     The data from the handler() function
165     *
166     * @return bool If rendering was successful.
167     */
168    public function render($mode, Doku_Renderer $renderer, $data) {
169        if ($mode === 'xhtml') {
170            $json_o = $this->loadHelper('json');
171
172            if(isset($data['tokens'])) {
173                if(!isset($data['filter']) || $json_o->filter($json_o->get(), $data['filter'])) {
174                    $var = $json_o->get($data['tokens']);
175                }
176                else {
177                    $data['type'] = 'filtered';
178                }
179            }
180
181            switch($data['type']) {
182                case 'filtered':
183                    break;
184
185
186                case 'variable':
187                    $this->render_var($renderer, $var, $data['format'], 1);
188                    break;
189
190
191                case 'list':
192                    $tooltips = array();
193                    //get data list
194                    if(is_array($data['list_header'])) {
195                        $list = array();
196                        foreach($data['list_header'] as $key => $tokens) {
197                            $v = $var;
198                            foreach($tokens as $tok) {
199                                if(is_array($v) && isset($v[$tok])) {
200                                    $v = $v[$tok];
201                                }
202                                else {
203                                    $v = null;
204                                    break;
205                                }
206                            }
207                            //If $name begins with '_tooltip_', it will display only as tooltip
208                            if(strpos($key, '_tooltip_') === 0) {
209                                $tooltips[substr($key, 9)] = $v;
210                            }
211                            else {
212                                $list[$key] = $v;
213                            }
214                        }
215                    }
216                    else {
217                        if(is_array($var)) {
218                            $list = array();
219                            foreach($var as $key => $v) {
220                                $list[$key] = $v;
221                            }
222                        }
223                        else {
224                            $list = array($this->getConf('null_str') => $var);
225                        }
226                    }
227
228                    //render data list
229                    $renderer->table_open(2);
230                    $renderer->tabletbody_open();
231                    foreach($list as $name => $value) {
232                        if(isset($tooltips[$name])) {
233                            $renderer->doc .= DOKU_TAB.'<tr title="'.htmlspecialchars($tooltips[$name]).'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
234                        }
235                        else {
236                            $renderer->tablerow_open();
237                        }
238                        $renderer->tableheader_open();
239                        $renderer->cdata($name);
240                        $renderer->tableheader_close();
241                        $renderer->tablecell_open();
242                        $this->render_var($renderer, $value, isset($data['format'][$name]) ? $data['format'][$name] : '', 1);
243                        $renderer->tablecell_close();
244                        $renderer->tablerow_close();
245                    }
246                    $renderer->tabletbody_close();
247                    $renderer->table_close();
248                    break;
249
250
251                case 'table_without_header':
252                    if(!is_array($var)) {
253                        $var = array($var);
254                    }
255                    //render table
256                    $renderer->table_open();
257                    foreach($var as $set) {
258                        //apply filter
259                        if(isset($data['table_filter']) && !$json_o->filter($set, $data['table_filter'])) {
260                            continue;
261                        }
262                        $renderer->tablerow_open();
263                        if(!is_array($set)) {
264                            $set = array($set);
265                        }
266                        foreach($set as $value) {
267                            $renderer->tablecell_open();
268                            $this->render_var($renderer, $value, null, 1);
269                            $renderer->tablecell_close();
270                        }
271                        $renderer->tablerow_close();
272                    }
273                    $renderer->tabletbody_close();
274                    $renderer->table_close();
275                    break;
276
277
278                case 'table_with_header':
279                    if(!is_array($var)) {
280                        $var = array($var);
281                    }
282                    $table = array();
283                    //get table rows
284                    foreach($var as $set) {
285                        //apply filter
286                        if(isset($data['table_filter']) && !$json_o->filter($set, $data['table_filter'])) {
287                            continue;
288                        }
289
290                        //get table header on the first pass
291                        if(!isset($header)) {
292                            $header = array();
293                            // if not specified, generate header automatically
294                            if($data['table_header'] === '') {
295                                $table_header = array();
296                                if(is_array($set)) {
297                                    foreach($set as $key => $dummy) {
298                                        $table_header[$key] = array($key);
299                                    }
300                                }
301                                $data['table_header'] = $table_header;
302                            }
303                            foreach($data['table_header'] as $key => $tokens) {
304                                if(strpos($key, '_tooltip_') !== 0) {
305                                    $header[] = $key;
306                                }
307                            }
308                        }
309
310                        //get cells for one row
311                        $row = array();
312                        $tooltips = array();
313                        foreach($data['table_header'] as $name => $tokens) {
314                            $v = $set;
315                            //get value of the variable
316                            foreach($tokens as $tok) {
317                                if(is_array($v) && isset($v[$tok])) {
318                                    $v = $v[$tok];
319                                }
320                                else {
321                                    $v = null;
322                                    break;
323                                }
324                            }
325
326                            //If $name begins with '_tooltip_', it will display only as tooltip
327                            if(strpos($name, '_tooltip_') === 0) {
328                                $tooltips[substr($name, 9)] = $v;
329                            }
330                            else {
331                                $row[$name] = $v;
332                            }
333                        }
334                        $row['_tooltip_'] = $tooltips;
335                        $table[] = $row;
336                    }
337
338                    //render table
339                    $renderer->table_open();
340
341                    $renderer->tablethead_open();
342                    $renderer->tablerow_open();
343                    foreach($header as $value) {
344                        $renderer->tableheader_open();
345                        $this->render_var($renderer, $value, null, 1);
346                        $renderer->tableheader_close();
347                    }
348                    $renderer->tablerow_close();
349                    $renderer->tablethead_close();
350
351                    $renderer->tabletbody_open();
352                    foreach($table as $set) {
353                        $tooltips = $set['_tooltip_'];
354                        unset($set['_tooltip_']);
355
356                        //open table row, optionally with tooltip
357                        if(isset($tooltips[''])) {
358                            $renderer->doc .= DOKU_TAB.'<tr title="'.htmlspecialchars($tooltips['']).'">'.DOKU_LF.DOKU_TAB.DOKU_TAB;
359                        }
360                        else {
361                            $renderer->tablerow_open();
362                        }
363
364                        //render all cells in row, some may have tooltips, some may have custom format
365                        foreach($set as $name => $value) {
366                            if(isset($tooltips[$name])) {
367                                $renderer->doc .= '<td title="'.htmlspecialchars($tooltips[$name]).'">';
368                            }
369                            else {
370                                $renderer->tablecell_open();
371                            }
372                            $this->render_var($renderer,
373                                              $value,
374                                              isset($data['format'][$name]) ? $data['format'][$name] : null,
375                                              1);
376                            $renderer->tablecell_close();
377                        }
378                        $renderer->tablerow_close();
379                    }
380                    $renderer->tabletbody_close();
381
382                    $renderer->table_close();
383                    break;
384
385
386                default:
387                    $renderer->cdata($data['match']);
388                    break;
389            }
390            return true;
391        }
392
393        return false;
394    }
395
396
397    /**
398     * Render the variable
399     *
400     * @param Doku_Renderer $renderer The renderer
401     * @param mixed $var variable to be rendered. If array, then members will be rendered.
402     * @param array $format render variable in specific format.
403     * @param integer $recursive if >1 and $var is array, then array elements will be rendered
404     *
405     * @return integer number of elements rendered
406     */
407    private function render_var(Doku_Renderer $renderer, $var, $format=null, $recursive=0, $print_separator=false) {
408        $i = $iprev = 0;
409        if(is_array($format) && ($format['func'] === 'format_code' || $format['func'] === 'format_ejs')) {
410            call_user_func(array($this, $format['func']), $renderer, $var, $format['param']);
411        }
412        else if(is_scalar($var)) {
413            if($print_separator) {
414                $renderer->doc .= ', ';
415            }
416            if(is_bool($var)) {
417                $renderer->cdata($this->getConf($var ? 'true_str' : 'false_str'));
418            }
419            else if(is_string($var) && is_array($format)) {
420                call_user_func(array($this, $format['func']), $renderer, $var, $format['param']);
421            }
422            else {
423                $renderer->cdata($var);
424            }
425            $i++;
426        }
427        else if(is_array($var)) {
428            if($recursive > 0) {
429                foreach($var as $v) {
430                    if($i > $iprev) {
431                        $print_separator = true;
432                        $iprev = $i;
433                    }
434                    $i += $this->render_var($renderer, $v, $format, $recursive-1, $print_separator);
435                }
436                if($i === 0 && count($var) > 0) {
437                    $renderer->cdata($this->getConf('array_str'));
438                }
439            }
440        }
441        else {
442            $renderer->cdata($this->getConf('null_str'));
443            $i++;
444        }
445
446        return $i;
447    }
448
449
450    /**
451     * Handle format parameter
452     *
453     * @param string $format format parameter from json data extractor
454     *
455     * @return empty string or array with function name and parameter, which
456     *                       renders the variable according to format.
457     */
458    private function handle_format($format) {
459        $param = null;
460
461        // Dokuwiki Header
462        // #header5#
463        if(substr($format, 0, 6) === 'header') {
464            //get header level
465            $param = intval(substr($format, 6));
466            if($param < 1) $param = 1;
467            else if($param > 5) $param = 5;
468            $format = 'header';
469        }
470
471        // Dokuwiki media internal or external link
472        // #media?L200x300#
473        else if(substr($format, 0, 5) === 'media') {
474            list($format, $align_size) = array_pad(explode('?', $format, 2), 2, '');
475            if($align_size === '') {
476                $param = array(null, null, null, null, null);
477            }
478            else if($align_size === 'linkonly') {
479                $param = array(null, null, null, null, 'linkonly');
480            }
481            else {
482                $align = substr($align_size, 0, 1);
483                if     ($align === 'l') $align = 'left';
484                else if($align === 'c') $align = 'center';
485                else if($align === 'r') $align = 'right';
486                else                    $align = null;
487                list($width, $height) = array_pad(explode('x', substr($align_size, 1), 2), 2, '');
488                $width = intval($width);
489                if($width > 0) {
490                    $height = intval($height);
491                    if($height <= 0) {
492                        $height = null;
493                    }
494                }
495                else {
496                    $width = $height = null;
497                }
498                $param = array($align, $width, $height, null, null);
499            }
500        }
501
502        // RSS
503        // #rss?n?nosort?reverse?author?date?details#
504        else if(substr($format, 0, 3) === 'rss') {
505            $param = array();
506            if(preg_match('/\b(\d+)\b/', $format, $match)) {
507               $param['max'] = $match[1];
508            }
509            else {
510               $param['max'] = 8;
511            }
512            $param['reverse'] = preg_match('/\brev/', $format);
513            $param['author']  = preg_match('/\b(by|author)/', $format);
514            $param['date']    = preg_match('/\bdate/', $format);
515            $param['details'] = preg_match('/\b(desc|detail)/', $format);
516            $param['nosort']  = preg_match('/\bnosort/', $format);
517            $format = 'rss';
518        }
519
520        // EJS template
521        // %$#ejs?template%
522        if(substr($format, 0, 4) === 'ejs?') {
523
524            $patterns = array ('/&percnt;/', '/&num;/', '/&colon;/', '/&comma;/', '/<\$=/', '/\$>/');
525            $replace = array ('%', '#', ':', ',', '<%=', '%>');
526
527            //get template and replace some patterns
528            $param = preg_replace($patterns, $replace, substr($format, 4));
529            $format = 'ejs';
530        }
531
532        //other formats don't need special handler
533
534        //get format_function name
535        $func = 'format_'.$format;
536
537        return  method_exists($this, $func) ?
538                array('func' => $func, 'param' => $param) :
539                '';
540    }
541
542
543    /**
544     * Renderers for different formats
545     *
546     * @param Doku_Renderer $renderer The renderer
547     * @param string $var variable to render
548     * @param mixed $param additional parameter
549     */
550    // Dokuwiki Title
551    private function format_header(Doku_Renderer $renderer, $var, $param) {
552        $renderer->header($var, $param, 0);
553    }
554
555    // \\server\share|Title
556    // https://example.com|Title
557    // dokuwiki:link|Title
558    private function format_link(Doku_Renderer $renderer, $var, $param) {
559        list($id, $title) = array_pad(explode('|', $var, 2), 2, '');
560        if(strpos($id, '\\') === 0) {
561            $renderer->windowssharelink($id, $title==='' ? $id : $title);
562        }
563        else if(strpos($id, '://')) {
564            $renderer->externallink($id, $title==='' ? $id : $title);
565        }
566        else if(strlen($id) > 0) {
567            if($title === '') {
568                $tok = explode(':', $id);
569                $last = $tok[count($tok) - 1];
570                $title = $last ? $last : $id;
571            }
572            $renderer->internallink($id, $title);
573        }
574    }
575
576    // https://example.com/media.png|Title
577    // dokuwiki:link.png|Title
578    private function format_media(Doku_Renderer $renderer, $var, $param) {
579        list($id, $title) = array_pad(explode('|', $var, 2), 2, '');
580        if($title === '') {
581            $title = $id;
582        }
583        if(strpos($id, '://')) {
584            $renderer->externalmedia($id, $title, $param[0], $param[1], $param[2], $param[3], $param[4]);
585        }
586        else {
587            $renderer->internalmedia($id, $title, $param[0], $param[1], $param[2], $param[3], $param[4]);
588        }
589    }
590
591    // name@example.com|Title
592    private function format_email(Doku_Renderer $renderer, $var, $param) {
593        list($id, $title) = array_pad(explode('|', $var, 2), 2, '');
594        if($title === '') {
595            $title = $id;
596        }
597        $renderer->emaillink($id, $title);
598    }
599
600    // http://slashdot.org/index.rss
601    private function format_rss(Doku_Renderer $renderer, $var, $param) {
602        $renderer->rss($var, $param);
603    }
604
605    // json_encode
606    private function format_code(Doku_Renderer $renderer, $var, $param) {
607        $renderer->doc .= '<pre class="json-extract-code">'
608                       .htmlspecialchars(json_encode($var, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE))
609                       .'</pre>';
610    }
611
612    // EJS template https://ejs.co/
613    // Javascript will take json data and template from hidden divs, then will generate output
614    private function format_ejs(Doku_Renderer $renderer, $var, $param) {
615        $renderer->doc .= '<span class="json-extract-ejs"><span id="data">'
616                       .htmlspecialchars(json_encode($var))
617                       .'</span><span id="template">'
618                       .htmlspecialchars($param)
619                       .'</span></span>';
620    }
621}
622