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