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 */
8use dokuwiki\Extension\Event;
9
10if(!defined('DOKU_INC')) die('Meh.');
11
12/**
13 * Select syntax for basic query handling.
14 */
15class syntax_plugin_strata_select extends DokuWiki_Syntax_Plugin {
16    function __construct() {
17        $this->helper =& plugin_load('helper', 'strata_syntax');
18        $this->util =& plugin_load('helper', 'strata_util');
19        $this->triples =& plugin_load('helper', 'strata_triples');
20    }
21
22    function getType() {
23        return 'substition';
24    }
25
26    function getPType() {
27        return 'block';
28    }
29
30    function getSort() {
31        return 450;
32    }
33
34    function connectTo($mode) {
35    }
36
37    function getUISettings($numFields, $hasUIBlock) {
38        $sort_choices = array(
39            'y' => array('default', 'yes', 'y'),
40            'l' => array('left to right', 'ltr', 'l'),
41            'r' => array('right to left', 'rtl', 'r'),
42            'n' => array('none', 'no', 'n')
43        );
44        $filter_choices = array(
45            't' => array('text', 't'),
46            's' => array('select', 's'),
47            'p' => array('prefix select', 'ps'),
48            'e' => array('suffix select', 'ss'),
49            'n' => array('none', 'no', 'n')
50        );
51        $globalProperties = array(
52            'ui' => $this->getUISettingUI($hasUIBlock),
53            'sort' => array('choices' => $sort_choices, 'minOccur' => $numFields, 'maxOccur' => $numFields, 'default' => 'yes'),
54            'filter' => array('choices' => $filter_choices, 'minOccur' => $numFields, 'maxOccur' => $numFields, 'default' => 'none')
55        );
56        $groupProperties = array(
57            'sort' => array('choices' => $sort_choices),
58            'filter' => array('choices' => $filter_choices),
59        );
60        return array($globalProperties, $groupProperties);
61    }
62
63    function getUISettingUI($hasUIBlock) {
64        return array('choices' => array('none' => array('none', 'no', 'n'), 'generic' => array('generic', 'g')), 'default' => ($hasUIBlock ? 'generic' : 'none'));
65    }
66
67    function handle($match, $state, $pos, Doku_Handler $handler) {
68        try {
69            $result = array();
70            $typemap = array();
71
72            // allow subclass handling of the whole match
73            $match = $this->preprocess($match, $state, $pos, $handler, $result, $typemap);
74
75            // split into lines and remove header and footer
76            $lines = explode("\n",$match);
77            $header = trim(array_shift($lines));
78            $footer = trim(array_pop($lines));
79
80            // allow subclass header handling
81            $header = $this->handleHeader($header, $result, $typemap);
82
83            // parse projection information in 'short syntax' if available
84            if(trim($header) != '') {
85                $result['fields'] = $this->helper->parseFieldsShort($header, $typemap);
86            } else {
87                $result['fields'] = [];
88            }
89
90            $tree = $this->helper->constructTree($lines,'query');
91
92            // parse long fields, if available
93            $longFields = $this->helper->getFields($tree, $typemap);
94
95            // check double data
96            if(count($result['fields']) && count($longFields)) {
97                $this->helper->_fail($this->getLang('error_query_bothfields'));
98            }
99
100            // assign longfields if necessary
101            if(count($result['fields']) == 0) {
102                $result['fields'] = $longFields;
103            }
104
105            // check no data
106            if(count($result['fields']) == 0) {
107                $this->helper->_fail($this->helper->getLang('error_query_noselect'));
108            }
109
110            // determine the variables to project
111            $projection = array();
112            foreach($result['fields'] as $f) $projection[] = $f['variable'];
113            $projection = array_unique($projection);
114
115            // allow subclass body handling
116            $this->handleBody($tree, $result, $typemap);
117
118            // parse UI group
119            $this->handleUI($tree, $result, $typemap);
120
121            // parse the query itself
122            list($result['query'], $variables) = $this->helper->constructQuery($tree, $typemap, $projection);
123
124            // allow subclass footer handling
125            $footer = $this->handleFooter($footer, $result, $typemap, $variable);
126
127            // check projected variables and load types
128            foreach($result['fields'] as $i=>$f) {
129                $var = $f['variable'];
130                if(!in_array($var, $variables)) {
131                    $this->helper->_fail(sprintf($this->helper->getLang('error_query_unknownselect'),utf8_tohtml(hsc($var))));
132                }
133
134                if(empty($f['type'])) {
135                    if(!empty($typemap[$var])) {
136                        $result['fields'][$i] = array_merge($result['fields'][$i],$typemap[$var]);
137                    } else {
138                        list($type, $hint) = $this->util->getDefaultType();
139                        $result['fields'][$i]['type'] = $type;
140                        $result['fields'][$i]['hint'] = $hint;
141                    }
142                }
143            }
144
145            return $result;
146        } catch(strata_exception $e) {
147            return array('error'=>array(
148                'message'=>$e->getMessage(),
149                'regions'=>$e->getData(),
150                'lines'=>$lines
151            ));
152        }
153    }
154
155    function handleUI(&$tree, &$result, &$typemap) {
156        $trees = $this->helper->extractGroups($tree, 'ui');
157
158        list($globalProperties, $groupProperties) = $this->getUISettings(count($result['fields']), count($trees));
159
160        // Extract column settings which are set as a group
161
162        // Extract named column settings
163        $namedGroupSettings = array();
164        foreach ($result['fields'] as $i => $f) {
165            if(isset($namedGroupSettings[$f['caption']])) continue;
166            $groups = array();
167            foreach($trees as &$t) {
168                $groups = array_merge($groups, $this->helper->extractGroups($t, $f['caption']));
169            }
170            $namedGroupSettings[$f['caption']] = $this->helper->setProperties($groupProperties, $groups);
171        }
172
173        // Extract numbered column settings
174        $groupsettings = array();
175        foreach ($result['fields'] as $i => $f) {
176            $groups = array();
177            foreach ($trees as &$t) {
178                $groups = array_merge($groups, $this->helper->extractGroups($t, '#' . ($i+1)));
179            }
180
181            // process settings for this column
182            $groupsettings[$i] = $this->helper->setProperties($groupProperties, $groups);
183
184            // fill in unset properties from named settings
185            foreach($namedGroupSettings[$f['caption']] as $k=>$v) {
186                if(!isset($groupsettings[$i][$k])) {
187                    $groupsettings[$i][$k] = $v;
188                }
189            }
190        }
191
192        // Extract global settings
193        $result['strata-ui'] = $this->helper->setProperties($globalProperties, $trees);
194
195        // Merge column settings into global ones
196        foreach ($groupsettings as $i => $s) {
197            foreach ($s as $p => $v) {
198                $result['strata-ui'][$p][$i] = $v[0];
199            }
200        }
201    }
202
203    /**
204     * Handles the whole match. This method is called before any processing
205     * is done by the actual class.
206     *
207     * @param match string the complete match
208     * @param state the parser state
209     * @param pos the position in the source
210     * @param handler object the parser handler
211     * @param result array the result array passed to the render method
212     * @param typemap array the type map
213     * @return a preprocessed string
214     */
215    function preprocess($match, $state, $pos, &$handler, &$result, &$typemap) {
216        return $match;
217    }
218
219
220    /**
221     * Handles the header of the syntax. This method is called before
222     * the header is handled.
223     *
224     * @param header string the complete header
225     * @param result array the result array passed to the render method
226     * @param typemap array the type map
227     * @return a string containing the unhandled parts of the header
228     */
229    function handleHeader($header, &$result, &$typemap) {
230        return $header;
231    }
232
233    /**
234     * Handles the body of the syntax. This method is called before any
235     * of the body is handled, but after the 'fields' groups have been processed.
236     *
237     * @param tree array the parsed tree
238     * @param result array the result array passed to the render method
239     * @param typemap array the type map
240     */
241    function handleBody(&$tree, &$result, &$typemap) {
242    }
243
244    /**
245     * Handles the footer of the syntax. This method is called after the
246     * query has been parsed, but before the typemap is applied to determine
247     * all field types.
248     *
249     * @param footer string the footer string
250     * @param result array the result array passed to the render method
251     * @param typemape array the type map
252     * @param variables array of variables used in query
253     * @return a string containing the unhandled parts of the footer
254     */
255    function handleFooter($footer, &$result, &$typemap, &$variables) {
256        return $footer;
257    }
258
259    /**
260     * This method performs just-in-time modification to prepare
261     * the query for use.
262     *
263     * @param query array the query tree
264     * @return the query tree to use
265     */
266    function prepareQuery($query) {
267        // fire event
268	Event::createAndTrigger('STRATA_PREPARE_QUERY', $query);
269
270        // return the (possibly modified) query
271        return $query;
272    }
273
274    /**
275     * This method renders the data view.
276     *
277     * @param mode the rendering mode
278     * @param R the renderer
279     * @param data the custom data from the handle phase
280     */
281    function render($mode, Doku_Renderer $R, $data) {
282        return false;
283    }
284
285    /**
286     * This method renders the container for any strata select.
287     *
288     * The open tag will contain all give classes plus additional metadata, e.g., generated by the ui group
289     *
290     * @param mode the render mode
291     * @param R the renderer
292     * @param data the custom data from the handle phase
293     * @param additionalClasses array containing classes to be set on the generated container
294     */
295    function ui_container_open($mode, &$R, $data, $additionalClasses=array()) {
296        // only xhtml mode needs the UI container
297        if($mode != 'xhtml') return;
298
299        $p = $data['strata-ui'];
300        $c = array();
301
302        // Default sort: rtl for suffix and ltr otherwise
303        for ($i = 0; $i < count($p['sort']); $i++) {
304            if ($p['sort'][$i] == 'y') {
305                $p['sort'][$i] = ($p['filter'][$i] == 'e' ? 'r' : 'l');
306            }
307        }
308
309        if (trim(implode($p['sort']), 'n') != '') {
310            $c[] = 'strata-ui-sort';
311        }
312        if (trim(implode($p['filter']), 'n') != '') {
313            $c[] = 'strata-ui-filter';
314        }
315
316        $classes = implode(' ', array_merge($c, $additionalClasses));
317        $properties = implode(' ', array_map(
318            function($k, $v) {
319                if (empty($v)) {
320                    return '';
321                } else {
322                    return 'data-strata-ui-' . $k . '="' . implode($v) . '"';
323                }
324            }, array_keys($p), $p)
325        );
326
327        $R->doc .= '<div class="' . $classes . '" ' . $properties . '>' . DOKU_LF;
328    }
329
330    function ui_container_close($mode, &$R) {
331        // only xhtml mode needs the UI container
332        if($mode != 'xhtml') return;
333        $R->doc .= '</div>' . DOKU_LF;
334    }
335
336    protected function displayError($mode, &$R, $data) {
337        if($mode == 'xhtml') {
338            $style = '';
339            if(isset($data['error']['regions'])) $style = ' strata-debug-continued';
340            $R->doc .= '<div class="strata-debug-message '.$style.'">';
341            $R->cdata($this->helper->getLang('content_error_explanation'));
342            $R->doc .= ': '.$data['error']['message'];
343            $R->doc .= '</div>';
344            if(isset($data['error']['regions'])) $R->doc .= $this->helper->debugTree($data['error']['lines'], $data['error']['regions']);
345        } elseif($mode == 'odt') {
346            $R->cdata($this->helper->getLang('content_error_explanation__non_xhtml'));
347        }
348    }
349}
350