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