1<?php 2 3namespace dokuwiki\Parsing; 4 5use dokuwiki\Extension\PluginInterface; 6use dokuwiki\Extension\SyntaxPlugin; 7use dokuwiki\Parsing\ParserMode\Acronym; 8use dokuwiki\Parsing\ParserMode\ModeInterface; 9use dokuwiki\Parsing\ParserMode\Camelcaselink; 10use dokuwiki\Parsing\ParserMode\Entity; 11use dokuwiki\Parsing\ParserMode\Smiley; 12 13/** 14 * Central registry for parser mode categories and mode instantiation. 15 * 16 * The underlying data is kept in the global $PARSER_MODES array because 17 * third-party plugins read and write it directly at runtime (e.g. to register 18 * their mode in a category). All methods in this class operate on that global 19 * so changes are visible to both old and new code. 20 */ 21class ModeRegistry 22{ 23 // Category constants (preserving the historical 'substition' typo) 24 public const CATEGORY_CONTAINER = 'container'; 25 public const CATEGORY_BASEONLY = 'baseonly'; 26 public const CATEGORY_FORMATTING = 'formatting'; 27 public const CATEGORY_SUBSTITION = 'substition'; 28 public const CATEGORY_PROTECTED = 'protected'; 29 public const CATEGORY_DISABLED = 'disabled'; 30 public const CATEGORY_PARAGRAPHS = 'paragraphs'; 31 32 /** @var array{sort: int, mode: string, obj: ModeInterface}[]|null */ 33 private ?array $modes = null; 34 35 /** @var string[] Modes that handle their own line endings (skip EOL connection) */ 36 private array $blockEolModes = []; 37 38 private static ?self $instance = null; 39 40 /** 41 * Get the singleton instance of the ModeRegistry. 42 * 43 * @return self 44 */ 45 public static function getInstance(): self 46 { 47 if (!self::$instance instanceof self) { 48 self::$instance = new self(); 49 } 50 return self::$instance; 51 } 52 53 /** 54 * Reset the singleton instance. 55 * 56 * This is mainly useful for testing to force re-initialization. 57 * 58 * @return void 59 */ 60 public static function reset(): void 61 { 62 self::$instance = null; 63 } 64 65 /** 66 * Constructor. Initializes the global $PARSER_MODES array with the default mode categories. 67 */ 68 private function __construct() 69 { 70 global $PARSER_MODES; 71 $PARSER_MODES = [ 72 self::CATEGORY_CONTAINER => ['listblock', 'table', 'quote', 'hr'], 73 self::CATEGORY_BASEONLY => ['header', 'gfm_header'], 74 self::CATEGORY_FORMATTING => [ 75 'strong', 'emphasis', 'underline', 'monospace', 76 'subscript', 'superscript', 'deleted', 'footnote', 77 'gfm_emphasis', 'gfm_emphasis_underscore', 'gfm_strong_underscore', 78 'gfm_emphasis_strong', 'gfm_emphasis_strong_underscore', 79 'gfm_deleted', 'gfm_backtick_single', 'gfm_backtick_double', 80 ], 81 self::CATEGORY_SUBSTITION => [ 82 'acronym', 'smiley', 'wordblock', 'entity', 83 'camelcaselink', 'internallink', 'media', 'externallink', 84 'linebreak', 'emaillink', 'windowssharelink', 'filelink', 85 'notoc', 'nocache', 'multiplyentity', 'quotes', 'rss', 86 'gfm_link', 'gfm_media', 87 ], 88 self::CATEGORY_PROTECTED => ['preformatted', 'code', 'file', 'gfm_code', 'gfm_file'], 89 self::CATEGORY_DISABLED => ['unformatted'], 90 self::CATEGORY_PARAGRAPHS => ['eol'], 91 ]; 92 } 93 94 /** 95 * Get all mode names in the given categories. 96 * 97 * @param string[] $categories One or more CATEGORY_* constants 98 * @return string[] Unique list of mode names 99 */ 100 public function getModesForCategories(array $categories): array 101 { 102 global $PARSER_MODES; 103 $modes = []; 104 foreach ($categories as $cat) { 105 if (isset($PARSER_MODES[$cat])) { 106 $modes = array_merge($modes, $PARSER_MODES[$cat]); 107 } 108 } 109 return array_unique($modes); 110 } 111 112 /** 113 * Get the raw categories array. 114 * 115 * @return array<string, string[]> Category name => list of mode names 116 */ 117 public function getCategories(): array 118 { 119 global $PARSER_MODES; 120 return $PARSER_MODES; 121 } 122 123 /** 124 * Register a mode in a category. 125 * 126 * @param string $category One of the CATEGORY_* constants 127 * @param string $modeName The mode name to register 128 * @return void 129 */ 130 public function registerMode(string $category, string $modeName): void 131 { 132 global $PARSER_MODES; 133 $PARSER_MODES[$category][] = $modeName; 134 $this->modes = null; // invalidate cached mode list 135 } 136 137 /** 138 * Register a mode that handles its own line endings. 139 * Modes registered here will be skipped by Eol's connectTo(). 140 * 141 * @param string $mode The mode name 142 * @return void 143 */ 144 public function registerBlockEolMode(string $mode): void 145 { 146 $this->blockEolModes[] = $mode; 147 } 148 149 /** 150 * Get all modes that handle their own line endings. 151 * 152 * @return string[] 153 */ 154 public function getBlockEolModes(): array 155 { 156 return $this->blockEolModes; 157 } 158 159 /** 160 * Get all parser modes, fully instantiated and sorted by priority. 161 * 162 * This includes syntax plugins, built-in modes, formatting modes, and 163 * data-driven modes (smileys, acronyms, entities). Results are cached 164 * unless running in a test environment. 165 * 166 * @return array[] Each entry is ['sort' => int, 'mode' => string, 'obj' => ModeInterface] 167 */ 168 public function getModes(): array 169 { 170 global $conf; 171 172 if ($this->modes !== null && !defined('DOKU_UNITTEST')) { 173 return $this->modes; 174 } 175 176 $this->modes = []; 177 $syntax = $conf['syntax'] ?? 'dokuwiki'; 178 $loadDw = in_array($syntax, ['dokuwiki', 'dw+md', 'md+dw']); 179 $loadMd = in_array($syntax, ['markdown', 'dw+md', 'md+dw']); 180 181 $this->loadPluginModes(); 182 $this->loadAlwaysModes(); 183 if ($loadDw) $this->loadDokuWikiModes(); 184 if ($loadMd) $this->loadMarkdownModes(); 185 $this->loadDataModes(); 186 187 usort($this->modes, self::sortModes(...)); 188 return $this->modes; 189 } 190 191 /** 192 * Load syntax plugin modes and register them in their categories. 193 */ 194 protected function loadPluginModes(): void 195 { 196 global $PARSER_MODES; 197 198 $plugins = plugin_list('syntax'); 199 foreach ($plugins as $p) { 200 $obj = plugin_load('syntax', $p); 201 if (!$obj instanceof PluginInterface) continue; 202 $PARSER_MODES[$obj->getType()][] = "plugin_$p"; 203 $this->modes[] = [ 204 'sort' => $obj->getSort(), 205 'mode' => "plugin_$p", 206 'obj' => $obj, 207 ]; 208 unset($obj); 209 } 210 } 211 212 /** 213 * Load modes that have no equivalent in the other syntax. 214 * These are always active regardless of the syntax setting. 215 */ 216 protected function loadAlwaysModes(): void 217 { 218 global $conf; 219 220 $modes = [ 221 'strong', 'subscript', 'superscript', 222 'footnote', 'eol', 'preformatted', 223 'quote', 'externallink', 'emaillink', 'windowssharelink', 224 'notoc', 'nocache', 'rss', 225 ]; 226 227 if ($conf['typography']) { 228 $modes[] = 'quotes'; 229 $modes[] = 'multiplyentity'; 230 } 231 232 $this->instantiateModes($modes); 233 } 234 235 /** 236 * Load DokuWiki-specific modes for features that also exist in Markdown. 237 * Skipped when syntax is 'markdown'. 238 */ 239 protected function loadDokuWikiModes(): void 240 { 241 global $conf; 242 $syntax = $conf['syntax'] ?? 'dokuwiki'; 243 $dwPreferred = in_array($syntax, ['dokuwiki', 'dw+md'], true); 244 245 $modes = [ 246 'emphasis', 'deleted', 'code', 'header', 'hr', 247 'linebreak', 'internallink', 'media', 'listblock', 'table', 248 'monospace', 'unformatted', 'file', 249 ]; 250 251 // Underline only loads when DokuWiki is preferred. In MD-preferred 252 // modes, `__` means strong (via gfm_strong_underscore) and loading 253 // Underline here would conflict. 254 if ($dwPreferred) { 255 $modes[] = 'underline'; 256 } 257 258 $this->instantiateModes($modes); 259 } 260 261 /** 262 * Load Markdown-specific modes for features that also exist in DokuWiki. 263 * Skipped when syntax is 'dokuwiki'. 264 */ 265 protected function loadMarkdownModes(): void 266 { 267 global $conf; 268 $syntax = $conf['syntax'] ?? 'dokuwiki'; 269 $mdPreferred = in_array($syntax, ['markdown', 'md+dw'], true); 270 271 $modes = [ 272 'gfm_emphasis', 'gfm_emphasis_strong', 'gfm_deleted', 273 'gfm_backtick_single', 'gfm_backtick_double', 274 'gfm_header', 'gfm_link', 'gfm_media', 275 'gfm_code', 'gfm_file', 276 ]; 277 278 // Underscore-based emphasis and strong only load when Markdown is 279 // preferred. In DW-preferred modes, `__` means underline and loading 280 // these would conflict. 281 if ($mdPreferred) { 282 $modes[] = 'gfm_emphasis_underscore'; 283 $modes[] = 'gfm_strong_underscore'; 284 $modes[] = 'gfm_emphasis_strong_underscore'; 285 } 286 287 $this->instantiateModes($modes); 288 } 289 290 /** 291 * Load data-driven modes that require constructor arguments 292 * (smileys, acronyms, entities) and optional config-gated modes. 293 */ 294 protected function loadDataModes(): void 295 { 296 global $conf; 297 298 $obj = new Smiley(array_keys(getSmileys())); 299 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'smiley', 'obj' => $obj]; 300 301 $obj = new Acronym(array_keys(getAcronyms())); 302 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'acronym', 'obj' => $obj]; 303 304 $obj = new Entity(array_keys(getEntities())); 305 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'entity', 'obj' => $obj]; 306 307 if (!empty($conf['camelcase'])) { 308 $obj = new Camelcaselink(); 309 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'camelcaselink', 'obj' => $obj]; 310 } 311 } 312 313 /** 314 * Instantiate mode classes by name and add them to the mode list. 315 * 316 * Mode names are split on `_` and each segment is PascalCased to form the 317 * class name (e.g. `gfm_emphasis_underscore` → `GfmEmphasisUnderscore`, 318 * `internallink` → `Internallink`, `strong` → `Strong`). 319 * 320 * @param string[] $modeNames 321 */ 322 protected function instantiateModes(array $modeNames): void 323 { 324 foreach ($modeNames as $mode) { 325 $class = implode('', array_map('ucfirst', explode('_', $mode))); // snake_case to PascalCase 326 $class = 'dokuwiki\\Parsing\\ParserMode\\' . $class; // prepend namespace 327 $obj = new $class(); 328 $this->modes[] = [ 329 'sort' => $obj->getSort(), 330 'mode' => $mode, 331 'obj' => $obj, 332 ]; 333 } 334 } 335 336 /** 337 * Callback function for usort 338 * 339 * @param array $a 340 * @param array $b 341 * @return int 342 */ 343 public static function sortModes(array $a, array $b): int 344 { 345 return $a['sort'] <=> $b['sort']; 346 } 347} 348