1<?php
2/**
3 * DokuWiki Plugin structcondstyle (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Lars Ohnemus <ohnemus.lars@gmail.com>
7 */
8
9// must be run within Dokuwiki
10if (!defined('DOKU_INC')) {
11    die();
12}
13
14use dokuwiki\plugin\structcondstyle\meta\Operator;
15use dokuwiki\plugin\structcondstyle\meta\NumericOperator;
16use dokuwiki\plugin\struct\meta\StructException;
17
18class action_plugin_structcondstyle extends DokuWiki_Action_Plugin
19{
20    protected $search_configs = [];
21    protected $ops = [];
22
23
24    /**
25     * Registers a callback function for a given event
26     *
27     * @param Doku_Event_Handler $controller DokuWiki's event controller object
28     *
29     * @return void
30     */
31    public function register(Doku_Event_Handler $controller)
32    {
33        // Define functions that are used by multiple operators
34        $not_func = function($lhs, $rhs){return $lhs !== $rhs;};
35        $in_func = function($lhs, $rhs){
36            // Check if $lhs is array or string
37            if(is_string($lhs)){
38                return strpos($lhs, $rhs) !== false;
39            }else return false;
40            };
41
42        // Define operators
43        $this->ops = [  "="         => new Operator("=", function($lhs, $rhs){return $lhs == $rhs;}),
44                        "!="        => new Operator("!=", $not_func),
45                        "not"       => new Operator("not", $not_func),
46                        "<"         => new NumericOperator("<", function($lhs, $rhs){return $lhs < $rhs;}),
47                        "<="        => new NumericOperator("<=", function($lhs, $rhs){return $lhs <= $rhs;}),
48                        ">"         => new NumericOperator(">", function($lhs, $rhs){return $lhs > $rhs;}),
49                        ">="        => new NumericOperator(">=", function($lhs, $rhs){return $lhs >= $rhs;}),
50                        "contains"  => new Operator("contains",$in_func)
51                    ];
52
53        // Register hooks
54        $controller->register_hook('PLUGIN_STRUCT_CONFIGPARSER_UNKNOWNKEY', 'BEFORE', $this, 'handle_plugin_struct_configparser_unknownkey');
55        $controller->register_hook('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', 'BEFORE', $this, 'handle_plugin_struct_aggregationtable_renderresultrow_before');
56        $controller->register_hook('PLUGIN_STRUCT_AGGREGATIONTABLE_RENDERRESULTROW', 'AFTER', $this, 'handle_plugin_struct_aggregationtable_renderresultrow_after');
57    }
58
59    /**
60     * [Custom event handler which performs action]
61     *
62     * Called for event:
63     *
64     * @param Doku_Event $event  event object by reference
65     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
66     *                           handler was registered]
67     *
68     * @return void
69     */
70    public function handle_plugin_struct_configparser_unknownkey(Doku_Event $event, $param)
71    {
72        // Retrieve the key and data and value passed for this agregation line
73        $data = $event->data;
74        $key = $data['key'];
75        $val = trim($data['val']);
76
77        // If the key is not associated with this plugin, return instantly
78        if ($key != 'condstyle') return;
79
80        // Else prevent errors and default handling to inject custom code into struct
81        $event->preventDefault();
82        $event->stopPropagation();
83
84        // Try to parse value into ternary statement and show error message if it does not work out
85        if(!preg_match('/\s*[a-zA-z]+\s*.+\s*[a-zA-z0-9]+\s*\?\s*".+"\s*:\s*.+"/',$val)){
86            msg("condstyle: $val is not correct", -1);
87            return;
88        }
89
90        // split value
91        $condition = preg_split("/\s*\?\s*/",$val,2)[0];
92        $styles = preg_split("/\s*\?\s*/",$val,2)[1];
93        $style_true = trim(preg_split('/"\s*:\s*"/',$styles)[0],'"');
94        $style_false = trim(preg_split('/"\s*:\s*"/',$styles)[1],'"');
95
96        // Parse operator
97        $operator = NULL;
98
99        foreach ($this->ops as $k => $op) {
100            $op_reg = $op->getReg();
101            if(preg_match("/\s*[a-zA-z]+\s*$op_reg\s*[a-zA-z0-9]+\s*/",$condition)){
102                $operator = $op_reg;
103            }
104        }
105
106        if(!$operator){
107            msg("condstyle: unknown operator ($val)", -1);
108        }
109
110        // parse column and argument
111        $column = trim(preg_split("/\s*$operator\s*/",$condition)[0]);
112        $argument = trim(preg_split("/\s*$operator\s*/",$condition)[1]);
113
114        // package parsed command into data
115        $config = array("operator"    => $operator,
116                        "column"      => $column,
117                        "argument"    => $argument,
118                        "style_true"  => $style_true,
119                        "style_false" => $style_false
120                        );
121
122        // Check if condstyle is already existing in data
123        if(!isset($data['config'][$key])){
124            $data['config'][$key] = [];
125        }
126
127        // Add command to data
128        $data['config'][$key][] = $config;
129    }
130
131     /**
132     * [Custom event handler which performs action]
133     *
134     * Called for event:
135     *
136     * @param Doku_Event $event  event object by reference
137     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
138     *                           handler was registered]
139     *
140     * @return void
141     */
142    public function handle_plugin_struct_aggregationtable_renderresultrow_before(Doku_Event $event, $param)
143    {
144        // Retrieve mode, renderer and data from this event
145        $mode = $event->data['mode'];
146        $renderer = $event->data['renderer'];
147        $data = $event->data['data'];
148
149        // Check if mode is xhtml otherwise return
150        if ($mode != 'xhtml') return;
151
152
153        // get all styles
154        $condstyles = $data['condstyle'];
155        if(!isset($condstyles)) return;
156
157        // loop over each style
158        foreach ($condstyles as $stylenum => $style) {
159
160            // Unpack condition and styles
161            $operator = NULL;
162            $column = NULL;
163            $argument = NULL;
164            extract($style);
165
166            // Check if valid data was send, otherwise move to next style
167            if(!$operator) continue;
168
169            // Query struct database to get full schema info (in case the column used for condition is not displayed)
170            /** @var SearchConfig $searchConfig */
171            $searchConfig = $event->data['searchConfig'];
172            $searchConfig_hash = spl_object_hash($searchConfig) . $stylenum;
173
174            if (!isset($this->search_configs[$searchConfig_hash])) {
175                // Add new Entry for this search configuration
176                $this->search_configs[$searchConfig_hash] = [];
177
178                // Retrieve Column
179                $cond_column = $searchConfig->findColumn($column);
180                //dbg(var_dump($cond_column));
181
182                // Add all columns to be sure that all information was retrieved and execute query
183                $searchConfig->addColumn('*');
184                $result = $searchConfig->execute();
185
186                // Check for each row if the condition matches and add store that information for later use
187                foreach ($result as $rownum => $row) {
188                    /** @var Value $value */
189                    foreach ($row as $colnum => $value) {
190                        if ($value->getColumn() === $cond_column) {
191                            // Retrieve row value for comparison
192                            $row_val = NULL;
193                            try{
194                                // try to get the displayed value, which might not be available
195                                $row_val = $value->getDisplayValue();
196                            } catch (StructException $e) {
197                                // use raw value instead
198                                $row_val = $value->getRawValue();
199                            }
200                            // check condition
201                            $cond_applies = $this->ops[$operator]->evaluate($row_val,$argument);
202
203                            // store condition to inject style later
204                            $this->search_configs[$searchConfig_hash][$rownum] = $cond_applies;
205
206                            break;
207                        }
208                    }
209                }
210
211            }
212
213        }// END style loop
214
215        // save row start position
216        $event->data['rowstart']= mb_strlen($renderer->doc);
217
218    }
219
220     /**
221     * [Custom event handler which performs action]
222     *
223     * Called for event:
224     *
225     * @param Doku_Event $event  event object by reference
226     * @param mixed      $param  [the parameters passed as fifth argument to register_hook() when this
227     *                           handler was registered]
228     *
229     * @return void
230     */
231    public function handle_plugin_struct_aggregationtable_renderresultrow_after(Doku_Event $event, $param)
232    {
233        // Retrieve and check mode
234        $mode = $event->data['mode'];
235        if ($mode != 'xhtml') return;
236
237        // Retrieve renderer
238        $renderer = $event->data['renderer'];
239
240        // Retrieve searchconfig and data
241        /** @var SearchConfig $searchConfig */
242        $searchConfig = $event->data['searchConfig'];
243        $data = $event->data['data'];
244        $rownum  = $event->data['rownum'];
245        $rowstart = $event->data['rowstart'];
246
247        // get all styles
248        $condstyles = $data['condstyle'];
249        if(!isset($condstyles)) return;
250
251        // String to store all styles
252        $style_tag = "";
253
254        // loop over each style
255        foreach ($condstyles as $stylenum => $style) {
256            // Unpack condition and styles
257            $style_true = NULL;
258            $style_false = NULL;
259            extract($style);
260
261            // Check if proper info is available
262            if (!isset($style_true)) continue;
263            if (!isset($style_false)) continue;
264            if (!$rowstart) continue;
265
266
267            // Lookup the style condition
268            $searchConfig_hash = spl_object_hash($searchConfig) . $stylenum;
269            $cond_applies = $this->search_configs[$searchConfig_hash][$rownum];
270            if (!isset($cond_applies)) continue;
271
272            // set the style for this column based on condition
273            $style_tag .= $cond_applies ? $style_true : $style_false;
274
275        } // END style loop
276
277
278        // split doc to inject styling
279        $rest = mb_substr($renderer->doc, 0,  $rowstart);
280        $row = mb_substr($renderer->doc, $rowstart);
281        $row = ltrim($row);
282        //check if we processing row
283        if (mb_substr($row, 0, 3) != '<tr') return;
284
285        $tr_tag = mb_substr($row, 0, 3);
286        $tr_rest = mb_substr($row, 3);
287
288
289        // inject style into document
290        if(trim($style_tag) != "")
291            $renderer->doc = $rest . $tr_tag . ' style="'.$style_tag.'" ' . $tr_rest;
292
293
294    }
295
296}
297
298