1<?php
2/**
3 * EditSections2 Plugin for DokuWiki / action.php
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author  Ben Coburn <btcoburn@silicodon.net>
7 * @author  Kazutaka Miyasaka <kazmiya@gmail.com>
8 */
9
10// must be run within DokuWiki
11if (!defined('DOKU_INC')) {
12    die();
13}
14
15if (!defined('DOKU_PLUGIN')) {
16    define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
17}
18
19require_once(DOKU_PLUGIN . 'action.php');
20
21class action_plugin_editsections2 extends DokuWiki_Action_Plugin
22{
23    var $section_levels = null;
24
25    /**
26     * Returns some info
27     */
28    function getInfo()
29    {
30        return confToHash(DOKU_PLUGIN . '/editsections2/plugin.info.txt');
31    }
32
33    /**
34     * Registers event handlers
35     */
36    function register(&$controller)
37    {
38        if (function_exists('html_secedit_get_button')) {
39            $controller->register_hook(
40                'DOKUWIKI_STARTED', 'BEFORE',
41                $this, 'exportToJSINFO'
42            );
43
44            $controller->register_hook(
45                'PARSER_HANDLER_DONE', 'BEFORE',
46                $this, 'handleInstructions'
47            );
48
49            $controller->register_hook(
50                'RENDERER_CONTENT_POSTPROCESS', 'BEFORE',
51                $this, 'handleHtmlContent'
52            );
53        } else {
54            // for DokuWiki Lemming or earlier
55            $controller->register_hook(
56                'PARSER_HANDLER_DONE', 'BEFORE',
57                $this, 'rewriteEditInstructions'
58            );
59        }
60    }
61
62    /**
63     * Exports configuration settings to $JSINFO
64     */
65    function exportToJSINFO(&$event)
66    {
67        global $JSINFO;
68
69        $JSINFO['plugin_editsections2'] = array(
70            'highlight_target' => $this->getConf('highlight_target'),
71        );
72    }
73
74    /**
75     * Prepends special instruction to put dummy section edit marker
76     */
77    function handleInstructions(&$event, $param)
78    {
79        $calls =& $event->data->calls;
80
81        // prepend plugin's instruction to the beginning of instructions
82        // (this instruction will be processed by render() in syntax.php)
83        array_unshift(
84            $calls,
85            array('plugin', array('editsections2', array(), 0))
86        );
87
88        // keeps section levels for hierarchical mode
89        if ($this->getConf('order_type')) {
90            $this->setSectionLevels($calls);
91        }
92    }
93
94    /**
95     * Replaces section edit markers
96     */
97    function handleHtmlContent(&$event, $param)
98    {
99        if ($event->data[0] !== 'xhtml') {
100            return;
101        }
102
103        $doc =& $event->data[1];
104
105        $marker_regexp =
106            '/<!-- EDIT(\d+) SECTION "([^"]*)" \[(\-?\d+)-(\d*)\] -->/';
107
108        if (preg_match_all($marker_regexp, $doc, $matches)) {
109            list($markers, $secids, $titles, $starts, $ends) = $matches;
110        } else {
111            return;
112        }
113
114        $markers_search = array_reverse($markers);
115
116        // build $markers_replace
117        if ($this->getConf('order_type')) {
118            // mode: hierarchical
119            $markers_replace = array();
120
121            // in case instruction is cached
122            if ($this->section_levels === null) {
123                $this->setSectionLevels();
124            }
125
126            // calculate nested section ranges and build new markers
127            for ($i = 1, $i_max = count($markers); $i < $i_max; $i++) {
128                $markers_replace[] = sprintf(
129                    '<!-- EDIT%s SECTION "%s" [%s-%s] -->',
130                    $secids[$i],
131                    $titles[$i],
132                    $starts[$i],
133                    $ends[$this->findNestedSectionEnd($i)]
134                );
135            }
136
137            $markers_replace = array_merge(
138                array(''),
139                array_reverse($markers_replace)
140            );
141        } else {
142            // mode: flat (shift button positions only)
143            $markers_replace = array_merge(
144                array(''),
145                array_slice($markers_search, 0, -1)
146            );
147        }
148
149        // shift edit button positions
150        $doc = str_replace($markers_search, $markers_replace, $doc);
151    }
152
153    /**
154     * Keeps section levels
155     */
156    function setSectionLevels($calls = null)
157    {
158        global $ID;
159        global $conf;
160
161        if ($calls === null) {
162            $calls = p_cached_instructions(wikiFN($ID), 'cacheonly');
163        }
164
165        list($handler_name, $instructions) = array(0, 1);
166        $this->section_levels = array('dummy_entry_for_padding');
167
168        for ($i = 0, $i_max = count($calls); $i < $i_max; $i++) {
169            if ($calls[$i][$handler_name] === 'section_open') {
170                $section_level = $calls[$i][$instructions][0];
171
172                if ($section_level <= $conf['maxseclevel']) {
173                    $this->section_levels[] = $section_level;
174                }
175            }
176        }
177    }
178
179    /**
180     * Finds the end of nested edit sections
181     */
182    function findNestedSectionEnd($offset_idx)
183    {
184        $offset_level = $this->section_levels[$offset_idx];
185        $end_idx = $offset_idx;
186        $i_max = count($this->section_levels);
187
188        for ($i = $offset_idx + 1; $i < $i_max; $i++) {
189            if ($this->section_levels[$i] > $offset_level) {
190                $end_idx = $i;
191            } else {
192                break;
193            }
194        }
195
196        return $end_idx;
197    }
198
199    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
200
201    /**
202     * Rewrites instructions (for DokuWiki Lemming or earlier)
203     */
204    function rewriteEditInstructions(&$event, $param)
205    {
206        // get the instructions list from the handler
207        $calls =& $event->data->calls;
208
209        // index numbers for readability
210        list($handler_name, $instructions) = array(0, 1);
211        list($pos_start, $pos_end, $sec_level) = array(0, 1, 2);
212
213        // scan instructions for edit
214        $sections = array();
215
216        for ($i = 0, $i_max = count($calls); $i < $i_max; $i++) {
217            if ($calls[$i][$handler_name] === 'section_edit') {
218                $sections[] =& $calls[$i][$instructions];
219            }
220        }
221
222        $section_count = count($sections);
223
224        // no need to rewrite
225        if ($section_count < 2) {
226            return;
227        }
228
229        // rewrite instructions
230        if ($this->getConf('order_type')) {
231            // mode: hierarchical
232            for ($i = 0, $i_max = $section_count - 1; $i < $i_max; $i++) {
233                // shift instructions
234                $sections[$i] = $sections[$i + 1];
235
236                // set default pos_end value (end of the wiki page)
237                $sections[$i][$pos_end] = 0;
238
239                // find and set the end point of hierarchical section
240                $level = $sections[$i][$sec_level];
241
242                for ($j = $i + 2; $j < $section_count; $j++) {
243                    if ($level >= $sections[$j][$sec_level]) {
244                        $sections[$i][$pos_end] = $sections[$j][$pos_start] - 1;
245                        break;
246                    }
247                }
248            }
249        } else {
250            // mode: flat (shift instructions only)
251            for ($i = 0, $i_max = $section_count - 1; $i < $i_max; $i++) {
252                $sections[$i] = $sections[$i + 1];
253            }
254        }
255
256        // hide old last section
257        $sections[$section_count - 1][$pos_start] = -1;
258    }
259}
260