1<?php
2/**
3 * Table editor
4 *
5 * @author Adrian Lang <lang@cosmocode.de>
6 * @author Andreas Gohr <gohr@cosmocode.de>
7 */
8
9use dokuwiki\Form\Form;
10use dokuwiki\Utf8;
11
12/**
13 * handles all the editor related things
14 *
15 * like displaying the editor and adding custom edit buttons
16 */
17class action_plugin_edittable_editor extends DokuWiki_Action_Plugin
18{
19    /**
20     * Register its handlers with the DokuWiki's event controller
21     */
22    public function register(Doku_Event_Handler $controller)
23    {
24        // register custom edit buttons
25        $controller->register_hook('HTML_SECEDIT_BUTTON', 'BEFORE', $this, 'secedit_button');
26
27        // register our editor
28        $controller->register_hook('EDIT_FORM_ADDTEXTAREA', 'BEFORE', $this, 'editform');
29        $controller->register_hook('HTML_EDIT_FORMSELECTION', 'BEFORE', $this, 'editform');
30
31        // register preprocessing for accepting editor data
32        // $controller->register_hook('ACTION_ACT_PREPROCESS', 'BEFORE', $this, 'handle_table_post');
33        $controller->register_hook('PLUGIN_EDITTABLE_PREPROCESS_EDITOR', 'BEFORE', $this, 'handle_table_post');
34    }
35
36    /**
37     * Add a custom edit button under each table
38     *
39     * The target 'table' is provided by DokuWiki's XHTML core renderer in the table_close() method
40     *
41     * @param Doku_Event $event
42     */
43    public function secedit_button(Doku_Event $event)
44    {
45        if ($event->data['target'] !== 'table') return;
46
47        $event->data['name'] = $this->getLang('secedit_name');
48    }
49
50    /**
51     * Creates the actual Table Editor form
52     *
53     * @param Doku_Event $event
54     */
55    public function editform(Doku_Event $event)
56    {
57        global $TEXT;
58        global $RANGE;
59        global $INPUT;
60
61        if ($event->data['target'] !== 'table') return;
62        if (!$RANGE){
63            // section editing failed, use default editor instead
64            $event->data['target'] = 'section';
65            return;
66        }
67
68        $event->stopPropagation();
69        $event->preventDefault();
70
71        /** @var renderer_plugin_edittable_json $Renderer our own renderer to convert table to array */
72        $Renderer     = plugin_load('renderer', 'edittable_json', true);
73        $instructions = p_get_instructions($TEXT);
74
75        // Loop through the instructions
76        foreach ($instructions as $instruction) {
77            // Execute the callback against the Renderer
78            call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1]);
79        }
80
81        // output data and editor field
82
83        /** @var Doku_Form $form */
84        $form =& $event->data['form'];
85
86        if (is_a($form, Form::class)) { // $event->name is EDIT_FORM_ADDTEXTAREA
87            // data for handsontable
88            $form->setHiddenField('edittable_data', $Renderer->getDataJSON());
89            $form->setHiddenField('edittable_meta', $Renderer->getMetaJSON());
90            $form->addHTML('<div id="edittable__editor"></div>');
91
92            // set data from action asigned to "New Table" button in the toolbar
93            foreach ($INPUT->post->arr('edittable__new', []) as $k => $v) {
94                $form->setHiddenField("edittable__new[$k]", $v);
95            }
96
97            // set target and range to keep track during previews
98            $form->setHiddenField('target', 'table');
99            $form->setHiddenField('range', $RANGE);
100
101        } else { // $event->name is HTML_EDIT_FORMSELECTION
102            // data for handsontable
103            $form->addHidden('edittable_data', $Renderer->getDataJSON());
104            $form->addHidden('edittable_meta', $Renderer->getMetaJSON());
105            $form->addElement('<div id="edittable__editor"></div>');
106
107            // set data from action asigned to "New Table" button in the toolbar
108            foreach ($INPUT->post->arr('edittable__new', []) as $k => $v) {
109                $form->addHidden("edittable__new[$k]", $v);
110            }
111
112            // set target and range to keep track during previews
113            $form->addHidden('target', 'table');
114            $form->addHidden('range', $RANGE);
115        }
116    }
117
118    /**
119     * Handles a POST from the table editor
120     *
121     * This function preprocesses a POST from the table editor and converts it to plain DokuWiki markup
122     *
123     * @author Andreas Gohr <gohr@cosmocode,de>
124     */
125    public function handle_table_post(Doku_Event $event)
126    {
127        global $TEXT;
128        global $INPUT;
129        if (!$INPUT->post->has('edittable_data')) return;
130
131        $data = json_decode($INPUT->post->str('edittable_data'), true);
132        $meta = json_decode($INPUT->post->str('edittable_meta'), true);
133
134        $TEXT = $this->build_table($data, $meta);
135    }
136
137    /**
138     * Create a DokuWiki table
139     *
140     * converts the table array to plain wiki markup text. pads the table so the markup is easy to read
141     *
142     * @param array $data table content for each cell
143     * @param array $meta meta data for each cell
144     * @return string
145     */
146    public function build_table($data, $meta)
147    {
148        $table = '';
149        $rows  = count($data);
150        $cols  = $rows ? count($data[0]) : 0;
151
152        $colmax = $cols ? array_fill(0, $cols, 0) : array();
153
154        // find maximum column widths
155        for ($row = 0; $row < $rows; $row++) {
156            for ($col = 0; $col < $cols; $col++) {
157                $len = $this->strWidth($data[$row][$col]);
158
159                // alignment adds padding
160                if ($meta[$row][$col]['align'] == 'center') {
161                    $len += 4;
162                } else {
163                    $len += 3;
164                }
165
166                // remember lenght
167                $meta[$row][$col]['length'] = $len;
168
169                if ($len > $colmax[$col]) $colmax[$col] = $len;
170            }
171        }
172
173        $last = '|'; // used to close the last cell
174        for ($row = 0; $row < $rows; $row++) {
175            for ($col = 0; $col < $cols; $col++) {
176
177                // minimum padding according to alignment
178                if ($meta[$row][$col]['align'] == 'center') {
179                    $lpad = 2;
180                    $rpad = 2;
181                } elseif ($meta[$row][$col]['align'] == 'right') {
182                    $lpad = 2;
183                    $rpad = 1;
184                } else {
185                    $lpad = 1;
186                    $rpad = 2;
187                }
188
189                // target width of this column
190                $target = $colmax[$col];
191
192                // colspanned columns span all the cells
193                for ($i = 1; $i < $meta[$row][$col]['colspan']; $i++) {
194                    $target += $colmax[$col + $i];
195                }
196
197                // copy colspans to rowspans below if any
198                if ($meta[$row][$col]['colspan'] > 1) {
199                    for ($i = 1; $i < $meta[$row][$col]['rowspan']; $i++) {
200                        $meta[$row + $i][$col]['colspan'] = $meta[$row][$col]['colspan'];
201                    }
202                }
203
204                // how much padding needs to be added?
205                $length = $meta[$row][$col]['length'];
206                $addpad = $target - $length;
207
208                // decide which side needs padding
209                if ($meta[$row][$col]['align'] == 'right') {
210                    $lpad += $addpad;
211                } else {
212                    $rpad += $addpad;
213                }
214
215                // add the padding
216                $cdata = $data[$row][$col];
217                if (!$meta[$row][$col]['hide'] || $cdata) {
218                    $cdata = str_pad('', $lpad).$cdata.str_pad('', $rpad);
219                }
220
221                // finally add the cell
222                $last   =  ($meta[$row][$col]['tag'] == 'th') ? '^' : '|';
223                $table .= $last;
224                $table .= $cdata;
225            }
226
227            // close the row
228            $table .= "$last\n";
229        }
230        $table = rtrim($table, "\n");
231
232        return $table;
233    }
234
235    /**
236     * Return width of string
237     *
238     * @param string $str
239     * @return int
240     */
241    public function strWidth($str)
242    {
243        static $callable;
244
245        if (isset($callable)) {
246            return $callable($str);
247        } else {
248            if (UTF8_MBSTRING) {
249                // count fullwidth characters as 2, halfwidth characters as 1
250                $callable = 'mb_strwidth';
251            } elseif (method_exists(Utf8\PhpString::class, 'strlen')) {
252                // count any characters as 1
253                $callable = [Utf8\PhpString::class, 'strlen'];
254            } else {
255                // fallback deprecated utf8_strlen since 2019-06-09
256                $callable = 'utf8_strlen';
257            }
258            return $this->strWidth($str);
259        }
260    }
261
262}
263