xref: /plugin/fontello/action.php (revision 1776c5c5befa8de3cc97e981444f6918b129ab85)
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        foreach ([
207            DOKU_PLUGIN . 'fontello/assets/active/config.json',
208            DOKU_PLUGIN . 'fontello/assets/active/enabled.json',
209        ] as $file) {
210            if (file_exists($file)) {
211                $event->data->depends['files'][] = $file;
212            }
213        }
214    }
215
216    /**
217     * Replace escaped icon tokens in XHTML headings.
218     *
219     * @param string $html
220     * @param helper_plugin_fontello $helper
221     * @return string
222     */
223    protected function replaceHeadingIconTokens($html, helper_plugin_fontello $helper)
224    {
225        return preg_replace_callback(
226            '/<h([1-6])\b([^>]*)>(.*?)<\/h\1>/s',
227            function ($match) use ($helper) {
228                return '<h' . $match[1] . $match[2] . '>' .
229                    $this->replaceEscapedIconTokens($match[3], $helper, true) .
230                    '</h' . $match[1] . '>';
231            },
232            $html
233        );
234    }
235
236    /**
237     * Replace escaped icon tokens in rendered link labels.
238     *
239     * This covers plugins such as catlist that render page titles via the
240     * XHTML renderer's internallink() method instead of reparsing title text.
241     *
242     * @param string $html
243     * @param helper_plugin_fontello $helper
244     * @return string
245     */
246    protected function replaceLinkIconTokens($html, helper_plugin_fontello $helper)
247    {
248        return preg_replace_callback(
249            '/<a\b([^>]*)>(.*?)<\/a>/s',
250            function ($match) use ($helper) {
251                return '<a' . $match[1] . '>' .
252                    $this->replaceEscapedIconTokens($match[2], $helper, true) .
253                    '</a>';
254            },
255            $html
256        );
257    }
258
259    /**
260     * Replace escaped icon tokens in catlist labels that are not links.
261     *
262     * @param string $html
263     * @param helper_plugin_fontello $helper
264     * @return string
265     */
266    protected function replaceCatlistIconTokens($html, helper_plugin_fontello $helper)
267    {
268        return preg_replace_callback(
269            '/<(?P<tag>h[1-5]|strong|span|li)\b(?P<attrs>[^>]*\bclass="[^"]*\bcatlist-(?:head|nshead|page)\b[^"]*"[^>]*)>(?P<body>.*?)<\/(?P=tag)>/s',
270            function ($match) use ($helper) {
271                return '<' . $match['tag'] . $match['attrs'] . '>' .
272                    $this->replaceEscapedIconTokens($match['body'], $helper, true) .
273                    '</' . $match['tag'] . '>';
274            },
275            $html
276        );
277    }
278
279    /**
280     * Keep only TOC-visible tokens in a title.
281     *
282     * @param string $title
283     * @param helper_plugin_fontello $helper
284     * @return string
285     */
286    protected function filterTocTitle($title, helper_plugin_fontello $helper)
287    {
288        $title = preg_replace_callback(
289            '/<icon:[A-Za-z0-9_-]+(?:\|(?:toc|notoc))?>/',
290            function ($match) use ($helper) {
291                $token = $helper->parseIconToken($match[0]);
292                if ($token === null || !$helper->iconTokenShowsInToc($token)) return '';
293                if ($helper->renderIconXhtml($token['name']) === null) return $match[0];
294                return $match[0];
295            },
296            $title
297        );
298
299        return trim(preg_replace('/[ \t]{2,}/', ' ', $title));
300    }
301
302    /**
303     * Replace escaped icon tokens with local icon HTML.
304     *
305     * @param string $html
306     * @param helper_plugin_fontello $helper
307     * @param bool $ignoreTocFlag
308     * @return string
309     */
310    protected function replaceEscapedIconTokens($html, helper_plugin_fontello $helper, $ignoreTocFlag)
311    {
312        return preg_replace_callback(
313            '/&lt;icon:([A-Za-z0-9_-]+)(?:\|(toc|notoc))?&gt;/',
314            function ($match) use ($helper, $ignoreTocFlag) {
315                $raw = '<icon:' . $match[1] . (isset($match[2]) && $match[2] !== '' ? '|' . $match[2] : '') . '>';
316                $token = $helper->parseIconToken($raw);
317                if ($token === null) return $match[0];
318                if (!$ignoreTocFlag && !$helper->iconTokenShowsInToc($token)) return '';
319
320                return $helper->renderIconXhtml($token['name']) ?: $match[0];
321            },
322            $html
323        );
324    }
325}
326