1<?php
2
3/**
4 * DokuWiki plugin Struct Template generic syntax
5 *
6 * @author     Iain Hallam <iain@nineworlds.net>
7 * @copyright  © 2022 Iain Hallam
8 * @license    GPL-2.0-only (http://www.gnu.org/licenses/gpl-2.0.html)
9 */
10
11declare(strict_types=1);
12
13namespace dokuwiki\plugin\structtemplate\meta;
14
15use Doku_Handler;
16use Doku_Renderer;
17use dokuwiki\Extension\SyntaxPlugin;
18use dokuwiki\plugin\struct\meta\ConfigParser;
19use dokuwiki\plugin\struct\meta\SearchConfig;
20use dokuwiki\plugin\struct\meta\StructException;
21
22/**
23 * Syntax plugin extending standard DokuWiki class
24 */
25class StructTemplateSyntax extends SyntaxPlugin
26{
27    /** @var  string  TAG             The tag to be used in Wiki markup between < and > */
28    /** @var  string  PLUGIN          The system name of the plugin */
29    /** @var  string  OPEN_SYNTAX     Interpolation syntax */
30    /** @var  string  CLOSE_SYNTAX    Interpolation syntax */
31    public const TAG            = 'struct-template';
32    public const PLUGIN         = 'structtemplate';
33    public const OPEN_SYNTAX    = '{{$$';
34    public const CLOSE_SYNTAX   = '}}';
35
36    /**
37     * Define the type of syntax plugin
38     *
39     * @see  https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
40     */
41    public function getType()
42    {
43        return 'substition';
44    }
45
46    /**
47     * Define the precedence of this plugin to the parser
48     *
49     * @see  https://www.dokuwiki.org/devel:parser:getsort_list
50     */
51    public function getSort()
52    {
53        return 45;
54    }
55
56    /**
57     * Handle matches
58     *
59     * @see  https://www.dokuwiki.org/devel:syntax_plugins#handle_method
60     *
61     * @param   string  $match     Text matched by the patterns
62     * @param   int     $state     Lexer state for the match
63     * @param   int     $position  Character position of the matched text
64     * @param   object  $handler   Doku_Handler object
65     * @return  array              Data for render()
66     */
67    public function handle($match, $state, $position, Doku_Handler $handler): array
68    {
69        // Configuration
70        // -------------------------------------------------------------
71
72        // Access global configuration settings
73        global $conf;
74        // Disable section editing for the template
75        $old_maxseclevel = $conf['maxseclevel'];
76        $conf['maxseclevel'] = 0;
77
78        // Extract the data block and template
79        // -------------------------------------------------------------
80
81        $template_start_index = 0;
82        // Reduce match to Struct search config
83        $lines = explode("\n", $match);
84        // Ignore first two lines (tag and data header) and last line (closing tag)
85        for ($line_index = 2; $line_index <= count($lines) - 1; $line_index++) {
86            if (preg_match('/^----+$/', $lines[$line_index])) {
87                // Reached the end of the data block
88                $template_start_index = $line_index + 1;
89                break;
90            }
91
92            $struct_syntax[] = $lines[$line_index];
93        }
94        // -1: ignore last line containing closing tag
95        $template = implode("\n", array_slice($lines, $template_start_index, -1));
96
97        // Check flags
98        $options['html'] = (preg_match('/\bhtml\b/', strtolower($lines[0])) === 1); //FIXME: check if HTML is allowed
99
100        // Configure the Struct search
101        // -------------------------------------------------------------
102
103        try {
104            $parser = new ConfigParser($struct_syntax);
105            $search_config = $parser->getConfig();
106        } catch (StructException $e) {
107            msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
108            if ($conf['allowdebug']) {
109                msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1);
110            }
111            // Re-enable section editing
112            $conf['maxseclevel'] = $old_maxseclevel;
113            return [];
114        }
115
116        // Return data for rendering
117        // -------------------------------------------------------------
118
119        // Re-enable section editing
120        $conf['maxseclevel'] = $old_maxseclevel;
121
122        return [$search_config, $template, $options];
123    }
124
125    /**
126     * Output rendered matches
127     *
128     * @see  https://www.dokuwiki.org/devel:syntax_plugins#render_method
129     *
130     * @param   string  $mode      Output format to generate
131     * @param   object  $renderer  Doku_Renderer object
132     * @param   array   $data      Data created by handle()
133     * @return  bool               Whether the syntax rendered OK
134     */
135    public function render($mode, Doku_Renderer $renderer, $data): bool
136    {
137        $search_config = $data[0];
138        $template      = $data[1];
139        $options       = $data[2];
140
141        // Access global configuration settings
142        global $conf;
143
144        // Disable section editing for the template
145        $old_maxseclevel = $conf['maxseclevel'];
146        $conf['maxseclevel'] = 0;
147
148        // Run the search (can't be in handler as that is cached)
149        try {
150            $search = new SearchConfig($search_config);
151
152            // Get all matching data, no pagination
153            $search->setLimit(0);
154            $search->setOffset(0);
155
156            // Run the search
157            $struct_data  = $search->execute();
158            $this->n_rows = $search->getCount();
159        } catch (StructException $e) {
160            msg($e->getMessage(), -1, $e->getLine(), $e->getFile());
161            if ($conf['allowdebug']) {
162                msg('<pre>' . hsc($e->getTraceAsString()) . '</pre>', -1);
163            }
164
165            // Re-enable section editing
166            $conf['maxseclevel'] = $old_maxseclevel;
167
168            return false;
169        }
170
171        // Construct a lookup table for column names and indices in the result
172        $columns = $search->getColumns();
173        foreach ($columns as $index => $column) {
174            $column_id = $column->getFullQualifiedLabel(false);
175            // getFullColumnName takes false to disable enforceSingleColumn
176            $column_indices[$column_id] = $index;
177        }
178
179        foreach ($struct_data as $row_index => $row) {
180            $chunks = explode(self::OPEN_SYNTAX, $template);
181
182            // First entry contains no fields
183            $interpolated = $chunks[0];
184            $chunks = array_slice($chunks, 1);
185
186            foreach ($chunks as $chunk) {
187                // Since the string was exploded on the open marker, this must start with a field
188                $chunk_parts    = explode(self::CLOSE_SYNTAX, $chunk, 2);
189                $column_request = $chunk_parts[0];
190                $next_output    = $chunk_parts[1];
191
192                if (array_key_exists($column_request, $column_indices)) {
193                    $interpolated .= $row[$column_indices[$column_request]]->getDisplayValue();
194                } else {
195                    if ($this->getConf('show_not_found')) {
196                        $renderer->cdata($this->getLang('none'));
197                    }
198                }
199                $interpolated .= $next_output;
200            }
201
202            if ($conf['htmlok'] == true and $options['html'] == true) {
203                $html = $interpolated;
204            } else {
205                // Rendering needs an array to write
206                $html_info = [];
207                $html = p_render($mode, p_get_instructions($interpolated), $html_info);
208            }
209
210            // Send to document
211            $renderer->doc .= $html;
212        }
213
214        // Re-enable section editing
215        $conf['maxseclevel'] = $old_maxseclevel;
216
217        return true;
218    }
219}
220