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 '/<icon:([A-Za-z0-9_-]+)(?:\|(toc|notoc))?>/', 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