1<?php
2/**
3 * DokuWiki Plugin Code Prettifier
4 *
5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author Satoshi Sahara <sahara.satoshi@gmail.com>
7 *
8 * usage: ex. <Code:css linenums:5 lang-css | title > ... </Code>
9 */
10
11if (!defined('DOKU_INC')) die();
12
13class syntax_plugin_codeprettify_code extends DokuWiki_Syntax_Plugin
14{
15    public function getType()
16    {   // Syntax Type
17        return 'protected';
18    }
19
20    public function getPType()
21    {   // Paragraph Type
22        return 'block';
23    }
24
25    /**
26     * Connect pattern to lexer
27     */
28    protected $mode, $pattern;
29
30    public function getSort()
31    {   // sort number used to determine priority of this mode
32        return 199; // < native 'code' mode (=200)
33    }
34
35    public function preConnect()
36    {
37        // syntax mode, drop 'syntax_' from class name
38        $this->mode = substr(get_class($this), 7);
39
40        // allowing nested "<angle pairs>" in title using regex atomic grouping
41        $n = 3;
42        $param = str_repeat('(?>[^<>\n]+|<', $n).str_repeat('>)*', $n);
43
44        // syntax patterns
45        $this->pattern[1] = '<Code\b'.$param.'>'.'(?=.*?</Code>)';
46        $this->pattern[4] = '</Code>';
47
48        // DokuWiki original syntax patterns
49        $this->pattern[11] = '<code\b.*?>(?=.*?</code>)';
50        $this->pattern[14] = '</code>';
51    }
52
53    public function connectTo($mode)
54    {
55        $this->Lexer->addEntryPattern($this->pattern[1], $mode, $this->mode);
56        if ($this->getConf('override')) {
57            $this->Lexer->addEntryPattern($this->pattern[11], $mode, $this->mode);
58        }
59    }
60
61    public function postConnect()
62    {
63        $this->Lexer->addExitPattern($this->pattern[4], $this->mode);
64        if ($this->getConf('override')) {
65            $this->Lexer->addExitPattern($this->pattern[14], $this->mode);
66        }
67    }
68
69
70    /**
71     * GeSHi Options Parser
72     *
73     * DokuWiki release 2018-04-22 "Greebo" supports some GeSHi options
74     * for syntax highlighting
75     * alternative of parse_highlight_options() in inc/parser/handler.php
76     *
77     * @param string $params  space separated list of key-value pairs
78     * @return array
79     * @see also https://www.dokuwiki.org/syntax_highlighting
80     */
81    private function getGeshiOption($params)
82    {
83        $opts = [];
84        // remove enclosing brackets and double-quotes
85        $params = str_replace('"', '', trim($params, '[]'));
86        if (preg_match_all('/(\w+)=?(\w+)?/', $params, $matches)) {
87
88            // make keys lowercase
89            $keys   = array_map('strtolower', $matches[1]);
90            // interpret boolian string values
91            $values = array_map(
92                function($value) {
93                    if (is_numeric($value)) {
94                        return $value;
95                    } else {
96                        $s = strtolower($value);
97                        if ($s == 'true')  $value = 1;
98                        if ($s == 'false') $value = 0;
99                        return $value;
100                    }
101                },
102                $matches[2]
103            );
104
105           // Note: last one prevails if same keys have appeared
106           $opts = array_combine($keys, $values);
107        }
108        return $opts;
109    }
110
111    /**
112     * Convert/interpret GeSHi Options to correspondent Prettifier options
113     * - enable_line_numbers=0    -> nolinenums
114     * - start_line_numbers_at=1  -> linenums:1
115     *
116     * @param array $opts  GeSHi options
117     * @return string  Prettifier linenums parameter
118     * @see also https://www.dokuwiki.org/syntax_highlighting
119     */
120    private function strGeshiOptions(array $opts=[])
121    {
122        if (isset($opts['enable_line_numbers'])) {
123            $option = &$opts['enable_line_numbers'];
124            $prefix = ($option == 0) ? 'no' : '';
125        }
126        if (isset($opts['start_line_numbers_at'])) {
127            $option = &$opts['start_line_numbers_at'];
128            $suffix = ($option > 0) ? ':'.$option : '';
129        }
130        return ($prefix or $suffix) ? $prefix.'linenums'.$suffix : '';
131    }
132
133
134    /**
135     * Prettifier Options Parser
136     *
137     * @param string $params
138     * @return array
139     */
140    private function getPrettifierOptions($params)
141    {
142        $opts = [];
143
144        // offset holds the position of the matched string
145        // if offset become 0, the first token of given params is NOT language
146        $offset = 1;
147        if (preg_match('/\b(no)?linenums(:\d+)?/', $params, $m, PREG_OFFSET_CAPTURE)) {
148            $offset = ($offset > 0) ? $m[0][1] : 1;
149            $opts['linenums'] = $m[1][0] ? '' : $m[0][0];
150        } else {
151            $opts['linenums'] = $this->getConf('linenums') ? 'linenums' : '';
152        }
153        if (preg_match('/\blang-\w+/', $params, $m, PREG_OFFSET_CAPTURE)) {
154            $offset = ($offset > 0) ? $m[0][1] : 1;
155            $opts['language'] = $m[0][0];
156        } elseif ($offset) {
157            // assume the first token is language; ex. C, php, css
158            list ($lang, ) = explode(' ', $params, 2);
159            $opts['language'] = $lang ? 'lang-'.$lang : '';
160        }
161        return $opts;
162    }
163
164
165    /**
166     * Handle the match
167     */
168    public function handle($match, $state, $pos, Doku_Handler $handler)
169    {
170        switch ($state) {
171            case DOKU_LEXER_ENTER:
172                list($params, $title) = explode('|', substr($match, 5, -1), 2);
173
174                // title parameter
175                if ($title) {
176                    // remove first "document_start" and last "document_end" instructions
177                    $calls = array_slice(p_get_instructions($title), 1, -1);
178                } else {
179                    $calls = null;
180                }
181
182                // prettifier parameters
183                $params = trim($params, ' :');
184
185                if ( preg_match('/\[.*\]/', $params, $matches) ) {
186                    // replace GeSHi parameters
187                    $params = str_replace(
188                        $matches[0],
189                        $this->strGeshiOptions( $this->getGeshiOption($matches[0]) ),
190                        $params
191                    );
192                }
193
194                $opts['prettify'] = 'prettyprint';
195                $opts += $this->getPrettifierOptions($params);
196                $params= implode(' ', $opts);
197
198                return $data = [$state, $params, $calls];
199            case DOKU_LEXER_UNMATCHED:
200                return $data = [$state, $match];
201            case DOKU_LEXER_EXIT:
202                return $data = [$state, ''];
203        }
204        return false;
205    }
206
207    /**
208     * Create output
209     */
210    function render($format, Doku_Renderer $renderer, $data)
211    {
212        if ($format == 'metadata') return false;
213        if (empty($data)) return false;
214        list($state, $args, $calls) = $data;
215
216        switch ($state) {
217            case DOKU_LEXER_ENTER:
218                if (isset($calls)) {
219                    // title of code box
220                    $renderer->doc .= '<div class="plugin_codeprettify">';
221                    $renderer->nest($calls);
222                    $renderer->doc .= '</div>';
223                }
224                $renderer->doc .= '<pre class="'.hsc($args).'">';
225                break;
226            case DOKU_LEXER_UNMATCHED:
227                $renderer->doc .= $renderer->_xmlEntities($args);
228                break;
229            case DOKU_LEXER_EXIT:
230                $renderer->doc .= '</pre>';
231                break;
232        }
233        return true;
234    }
235
236}
237