xref: /plugin/fontello/action.php (revision 95357802f68767bcd0367676ca97b384fa2109c6)
1<?php
2
3use dokuwiki\Extension\ActionPlugin;
4use dokuwiki\Extension\Event;
5use dokuwiki\Extension\EventHandler;
6
7/**
8 * Action component for globally loading the active Fontello stylesheet.
9 */
10class action_plugin_fontello extends ActionPlugin
11{
12    /**
13     * @param EventHandler $controller
14     * @return void
15     */
16    public function register(EventHandler $controller)
17    {
18        $controller->register_hook('DOKUWIKI_STARTED', 'AFTER', $this, 'handleJsInfo');
19        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handleHeader');
20        $controller->register_hook('RENDERER_CONTENT_POSTPROCESS', 'BEFORE', $this, 'handleRendererPostprocess');
21        $controller->register_hook('TPL_TOC_RENDER', 'BEFORE', $this, 'handleToc');
22        $controller->register_hook('TPL_CONTENT_DISPLAY', 'BEFORE', $this, 'handleContentDisplay');
23        $controller->register_hook('TOOLBAR_DEFINE', 'AFTER', $this, 'handleToolbar');
24        $controller->register_hook('JS_CACHE_USE', 'BEFORE', $this, 'handleJsCache');
25    }
26
27    /**
28     * Provide active icon metadata to plugin JavaScript before JSINFO is emitted.
29     *
30     * @return void
31     */
32    public function handleJsInfo()
33    {
34        global $JSINFO;
35
36        /** @var helper_plugin_fontello $helper */
37        $helper = $this->loadHelper('fontello', false);
38        if ($helper === null || !$helper->hasActivePackage()) return;
39
40        $package = $helper->getPackageInfo();
41        if ($package === null) return;
42
43        $icons = [];
44        foreach ($package['icons'] as $icon) {
45            if (!isset($icon['name'], $icon['class'])) continue;
46            $icons[$icon['name']] = $icon['class'];
47        }
48
49        $JSINFO['plugin_fontello'] = [
50            'icons' => $icons,
51            'showInToc' => (bool) $helper->getConf('showInToc'),
52        ];
53    }
54
55    /**
56     * Inject the active stylesheet when a package is available.
57     *
58     * @param Event $event
59     * @param mixed $param
60     * @return void
61     */
62    public function handleHeader(Event &$event, $param)
63    {
64        /** @var helper_plugin_fontello $helper */
65        $helper = $this->loadHelper('fontello', false);
66        if ($helper === null || !$helper->hasActivePackage()) return;
67
68        foreach ($event->data['link'] as $link) {
69            if (($link['href'] ?? '') === $helper->getCssUrl()) return;
70        }
71
72        $event->data['link'][] = [
73            'rel' => 'stylesheet',
74            'type' => 'text/css',
75            'href' => $helper->getCssUrl(),
76        ];
77    }
78
79    /**
80     * Remove TOC-hidden icon tokens before DokuWiki builds the TOC HTML.
81     *
82     * @param Event $event
83     * @param mixed $param
84     * @return void
85     */
86    public function handleToc(Event &$event, $param)
87    {
88        /** @var helper_plugin_fontello $helper */
89        $helper = $this->loadHelper('fontello', false);
90        if ($helper === null || !is_array($event->data)) return;
91
92        foreach ($event->data as $index => $item) {
93            if (!isset($item['title'])) continue;
94
95            $event->data[$index]['title'] = $this->filterTocTitle((string) $item['title'], $helper);
96        }
97    }
98
99    /**
100     * Render Fontello tokens in rendered XHTML headings.
101     *
102     * @param Event $event
103     * @param mixed $param
104     * @return void
105     */
106    public function handleRendererPostprocess(Event &$event, $param)
107    {
108        /** @var helper_plugin_fontello $helper */
109        $helper = $this->loadHelper('fontello', false);
110        if (
111            $helper === null ||
112            !$helper->hasActivePackage() ||
113            !is_array($event->data) ||
114            ($event->data[0] ?? '') !== 'xhtml' ||
115            !isset($event->data[1]) ||
116            !is_string($event->data[1])
117        ) {
118            return;
119        }
120
121        $event->data[1] = $this->replaceHeadingIconTokens($event->data[1], $helper);
122        $event->data[1] = $this->replaceLinkIconTokens($event->data[1], $helper);
123        $event->data[1] = $this->replaceCatlistIconTokens($event->data[1], $helper);
124    }
125
126    /**
127     * Render Fontello tokens in visible TOC links.
128     *
129     * @param Event $event
130     * @param mixed $param
131     * @return void
132     */
133    public function handleContentDisplay(Event &$event, $param)
134    {
135        /** @var helper_plugin_fontello $helper */
136        $helper = $this->loadHelper('fontello', false);
137        if ($helper === null || !$helper->hasActivePackage() || !is_string($event->data)) return;
138
139        $event->data = preg_replace_callback(
140            '/(<!-- TOC START -->.*?<!-- TOC END -->)/s',
141            function ($match) use ($helper) {
142                return $this->replaceEscapedIconTokens($match[1], $helper, false);
143            },
144            $event->data
145        );
146    }
147
148    /**
149     * Add a Fontello picker button to the editor toolbar.
150     *
151     * @param Event $event
152     * @param mixed $param
153     * @return void
154     */
155    public function handleToolbar(Event &$event, $param)
156    {
157        /** @var helper_plugin_fontello $helper */
158        $helper = $this->loadHelper('fontello', false);
159        if ($helper === null || !$helper->hasActivePackage()) return;
160
161        $icons = $helper->getActiveIcons();
162        if ($icons === []) return;
163
164        $button = [
165            'type' => 'fontello',
166            'title' => $this->getLang('toolbar_icons'),
167            'icon' => DOKU_BASE . 'lib/plugins/fontello/images/toolbar/fontello.svg',
168            'class' => 'pk_fontello',
169            'list' => array_map(static function ($icon) {
170                return [
171                    'name' => $icon['name'],
172                    'class' => $icon['class'],
173                    'insert' => '<icon:' . $icon['name'] . '>',
174                ];
175            }, $icons),
176            'block' => false,
177        ];
178
179        $insertAt = count($event->data);
180        foreach ($event->data as $index => $item) {
181            if (($item['type'] ?? '') === 'picker' && ($item['icobase'] ?? '') === 'smileys') {
182                $insertAt = $index + 1;
183                break;
184            }
185        }
186
187        array_splice($event->data, $insertAt, 0, [$button]);
188    }
189
190    /**
191     * Make dynamic toolbar data sensitive to active package changes.
192     *
193     * DokuWiki caches the generated toolbar JavaScript. The toolbar button list
194     * depends on runtime JSON files, so they need to be cache dependencies.
195     *
196     * @param Event $event
197     * @param mixed $param
198     * @return void
199     */
200    public function handleJsCache(Event &$event, $param)
201    {
202        if (!isset($event->data->depends['files']) || !is_array($event->data->depends['files'])) {
203            $event->data->depends['files'] = [];
204        }
205
206        $files = [
207            DOKU_PLUGIN . 'fontello/assets/active/config.json',
208            DOKU_PLUGIN . 'fontello/assets/active/enabled.json',
209        ];
210
211        foreach ($files as $file) {
212            if (file_exists($file)) {
213                $event->data->depends['files'][] = $file;
214            }
215        }
216    }
217
218    /**
219     * Replace escaped icon tokens in XHTML headings.
220     *
221     * @param string $html
222     * @param helper_plugin_fontello $helper
223     * @return string
224     */
225    protected function replaceHeadingIconTokens($html, helper_plugin_fontello $helper)
226    {
227        return preg_replace_callback(
228            '/<h([1-6])\b([^>]*)>(.*?)<\/h\1>/s',
229            function ($match) use ($helper) {
230                return '<h' . $match[1] . $match[2] . '>' .
231                    $this->replaceEscapedIconTokens($match[3], $helper, true) .
232                    '</h' . $match[1] . '>';
233            },
234            $html
235        );
236    }
237
238    /**
239     * Replace escaped icon tokens in rendered link labels.
240     *
241     * This covers plugins such as catlist that render page titles via the
242     * XHTML renderer's internallink() method instead of reparsing title text.
243     *
244     * @param string $html
245     * @param helper_plugin_fontello $helper
246     * @return string
247     */
248    protected function replaceLinkIconTokens($html, helper_plugin_fontello $helper)
249    {
250        return preg_replace_callback(
251            '/<a\b([^>]*)>(.*?)<\/a>/s',
252            function ($match) use ($helper) {
253                return '<a' . $match[1] . '>' .
254                    $this->replaceEscapedIconTokens($match[2], $helper, true) .
255                    '</a>';
256            },
257            $html
258        );
259    }
260
261    /**
262     * Replace escaped icon tokens in catlist labels that are not links.
263     *
264     * @param string $html
265     * @param helper_plugin_fontello $helper
266     * @return string
267     */
268    protected function replaceCatlistIconTokens($html, helper_plugin_fontello $helper)
269    {
270        $pattern = '/<(?P<tag>h[1-5]|strong|span|li)\b' .
271            '(?P<attrs>[^>]*\bclass="[^"]*\bcatlist-(?:head|nshead|page)\b[^"]*"[^>]*)>' .
272            '(?P<body>.*?)<\/(?P=tag)>/s';
273
274        return preg_replace_callback(
275            $pattern,
276            function ($match) use ($helper) {
277                return '<' . $match['tag'] . $match['attrs'] . '>' .
278                    $this->replaceEscapedIconTokens($match['body'], $helper, true) .
279                    '</' . $match['tag'] . '>';
280            },
281            $html
282        );
283    }
284
285    /**
286     * Keep only TOC-visible tokens in a title.
287     *
288     * @param string $title
289     * @param helper_plugin_fontello $helper
290     * @return string
291     */
292    protected function filterTocTitle($title, helper_plugin_fontello $helper)
293    {
294        $title = preg_replace_callback(
295            '/<icon:[A-Za-z0-9_-]+(?:\|(?:toc|notoc))?>/',
296            function ($match) use ($helper) {
297                $token = $helper->parseIconToken($match[0]);
298                if ($token === null || !$helper->iconTokenShowsInToc($token)) return '';
299                if ($helper->renderIconXhtml($token['name']) === null) return $match[0];
300                return $match[0];
301            },
302            $title
303        );
304
305        return trim(preg_replace('/[ \t]{2,}/', ' ', $title));
306    }
307
308    /**
309     * Replace escaped icon tokens with local icon HTML.
310     *
311     * @param string $html
312     * @param helper_plugin_fontello $helper
313     * @param bool $ignoreTocFlag
314     * @return string
315     */
316    protected function replaceEscapedIconTokens($html, helper_plugin_fontello $helper, $ignoreTocFlag)
317    {
318        return preg_replace_callback(
319            '/&lt;icon:([A-Za-z0-9_-]+)(?:\|(toc|notoc))?&gt;/',
320            function ($match) use ($helper, $ignoreTocFlag) {
321                $raw = '<icon:' . $match[1] . (isset($match[2]) && $match[2] !== '' ? '|' . $match[2] : '') . '>';
322                $token = $helper->parseIconToken($raw);
323                if ($token === null) return $match[0];
324                if (!$ignoreTocFlag && !$helper->iconTokenShowsInToc($token)) return '';
325
326                return $helper->renderIconXhtml($token['name']) ?: $match[0];
327            },
328            $html
329        );
330    }
331}
332