1c8dd1b9dSAndreas Gohr<?php 2c8dd1b9dSAndreas Gohr 3c8dd1b9dSAndreas Gohrnamespace dokuwiki\Parsing; 4c8dd1b9dSAndreas Gohr 5c8dd1b9dSAndreas Gohruse dokuwiki\Extension\PluginInterface; 6c8dd1b9dSAndreas Gohruse dokuwiki\Extension\SyntaxPlugin; 7c8dd1b9dSAndreas Gohruse dokuwiki\Parsing\ParserMode\Acronym; 8c8dd1b9dSAndreas Gohruse dokuwiki\Parsing\ParserMode\ModeInterface; 9c8dd1b9dSAndreas Gohruse dokuwiki\Parsing\ParserMode\Camelcaselink; 10c8dd1b9dSAndreas Gohruse dokuwiki\Parsing\ParserMode\Entity; 11c8dd1b9dSAndreas Gohruse dokuwiki\Parsing\ParserMode\Smiley; 12c8dd1b9dSAndreas Gohr 13c8dd1b9dSAndreas Gohr/** 14c8dd1b9dSAndreas Gohr * Central registry for parser mode categories and mode instantiation. 15c8dd1b9dSAndreas Gohr * 16c8dd1b9dSAndreas Gohr * The underlying data is kept in the global $PARSER_MODES array because 17c8dd1b9dSAndreas Gohr * third-party plugins read and write it directly at runtime (e.g. to register 18c8dd1b9dSAndreas Gohr * their mode in a category). All methods in this class operate on that global 19c8dd1b9dSAndreas Gohr * so changes are visible to both old and new code. 20c8dd1b9dSAndreas Gohr */ 21c8dd1b9dSAndreas Gohrclass ModeRegistry 22c8dd1b9dSAndreas Gohr{ 23c8dd1b9dSAndreas Gohr // Category constants (preserving the historical 'substition' typo) 24c8dd1b9dSAndreas Gohr public const CATEGORY_CONTAINER = 'container'; 25c8dd1b9dSAndreas Gohr public const CATEGORY_BASEONLY = 'baseonly'; 26c8dd1b9dSAndreas Gohr public const CATEGORY_FORMATTING = 'formatting'; 27*56c730b5SAndreas Gohr public const CATEGORY_SUBSTITUTION = 'substition'; 28c8dd1b9dSAndreas Gohr public const CATEGORY_PROTECTED = 'protected'; 29c8dd1b9dSAndreas Gohr public const CATEGORY_DISABLED = 'disabled'; 30c8dd1b9dSAndreas Gohr public const CATEGORY_PARAGRAPHS = 'paragraphs'; 31c8dd1b9dSAndreas Gohr 32c8dd1b9dSAndreas Gohr /** @var array{sort: int, mode: string, obj: ModeInterface}[]|null */ 33c8dd1b9dSAndreas Gohr private ?array $modes = null; 34c8dd1b9dSAndreas Gohr 357958e698SAndreas Gohr /** @var string[] Modes that handle their own line endings (skip EOL connection) */ 367958e698SAndreas Gohr private array $blockEolModes = []; 377958e698SAndreas Gohr 387958e698SAndreas Gohr /** @var array<string, string[]> Mode name => regex-escaped line start marker characters */ 397958e698SAndreas Gohr private array $lineStartMarkers = []; 407958e698SAndreas Gohr 41c8dd1b9dSAndreas Gohr private static ?self $instance = null; 42c8dd1b9dSAndreas Gohr 43c8dd1b9dSAndreas Gohr /** 44c8dd1b9dSAndreas Gohr * Get the singleton instance of the ModeRegistry. 45c8dd1b9dSAndreas Gohr * 46c8dd1b9dSAndreas Gohr * @return self 47c8dd1b9dSAndreas Gohr */ 48c8dd1b9dSAndreas Gohr public static function getInstance(): self 49c8dd1b9dSAndreas Gohr { 50c8dd1b9dSAndreas Gohr if (!self::$instance instanceof self) { 51c8dd1b9dSAndreas Gohr self::$instance = new self(); 52c8dd1b9dSAndreas Gohr } 53c8dd1b9dSAndreas Gohr return self::$instance; 54c8dd1b9dSAndreas Gohr } 55c8dd1b9dSAndreas Gohr 56c8dd1b9dSAndreas Gohr /** 57c8dd1b9dSAndreas Gohr * Reset the singleton instance. 58c8dd1b9dSAndreas Gohr * 59c8dd1b9dSAndreas Gohr * This is mainly useful for testing to force re-initialization. 60c8dd1b9dSAndreas Gohr * 61c8dd1b9dSAndreas Gohr * @return void 62c8dd1b9dSAndreas Gohr */ 63c8dd1b9dSAndreas Gohr public static function reset(): void 64c8dd1b9dSAndreas Gohr { 65c8dd1b9dSAndreas Gohr self::$instance = null; 66c8dd1b9dSAndreas Gohr } 67c8dd1b9dSAndreas Gohr 68c8dd1b9dSAndreas Gohr /** 69c8dd1b9dSAndreas Gohr * Constructor. Initializes the global $PARSER_MODES array with the default mode categories. 70c8dd1b9dSAndreas Gohr */ 71c8dd1b9dSAndreas Gohr private function __construct() 72c8dd1b9dSAndreas Gohr { 73c8dd1b9dSAndreas Gohr global $PARSER_MODES; 74c8dd1b9dSAndreas Gohr $PARSER_MODES = [ 75c8dd1b9dSAndreas Gohr self::CATEGORY_CONTAINER => ['listblock', 'table', 'quote', 'hr'], 76c8dd1b9dSAndreas Gohr self::CATEGORY_BASEONLY => ['header'], 77c8dd1b9dSAndreas Gohr self::CATEGORY_FORMATTING => [ 78c8dd1b9dSAndreas Gohr 'strong', 'emphasis', 'underline', 'monospace', 79c8dd1b9dSAndreas Gohr 'subscript', 'superscript', 'deleted', 'footnote', 80c8dd1b9dSAndreas Gohr ], 81*56c730b5SAndreas Gohr self::CATEGORY_SUBSTITUTION => [ 82c8dd1b9dSAndreas Gohr 'acronym', 'smiley', 'wordblock', 'entity', 83c8dd1b9dSAndreas Gohr 'camelcaselink', 'internallink', 'media', 'externallink', 84c8dd1b9dSAndreas Gohr 'linebreak', 'emaillink', 'windowssharelink', 'filelink', 85c8dd1b9dSAndreas Gohr 'notoc', 'nocache', 'multiplyentity', 'quotes', 'rss', 86c8dd1b9dSAndreas Gohr ], 87c8dd1b9dSAndreas Gohr self::CATEGORY_PROTECTED => ['preformatted', 'code', 'file'], 88c8dd1b9dSAndreas Gohr self::CATEGORY_DISABLED => ['unformatted'], 89c8dd1b9dSAndreas Gohr self::CATEGORY_PARAGRAPHS => ['eol'], 90c8dd1b9dSAndreas Gohr ]; 91c8dd1b9dSAndreas Gohr } 92c8dd1b9dSAndreas Gohr 93c8dd1b9dSAndreas Gohr /** 94c8dd1b9dSAndreas Gohr * Get all mode names in the given categories. 95c8dd1b9dSAndreas Gohr * 96c8dd1b9dSAndreas Gohr * @param string[] $categories One or more CATEGORY_* constants 97c8dd1b9dSAndreas Gohr * @return string[] Unique list of mode names 98c8dd1b9dSAndreas Gohr */ 99c8dd1b9dSAndreas Gohr public function getModesForCategories(array $categories): array 100c8dd1b9dSAndreas Gohr { 101c8dd1b9dSAndreas Gohr global $PARSER_MODES; 102c8dd1b9dSAndreas Gohr $modes = []; 103c8dd1b9dSAndreas Gohr foreach ($categories as $cat) { 104c8dd1b9dSAndreas Gohr if (isset($PARSER_MODES[$cat])) { 105c8dd1b9dSAndreas Gohr $modes = array_merge($modes, $PARSER_MODES[$cat]); 106c8dd1b9dSAndreas Gohr } 107c8dd1b9dSAndreas Gohr } 108c8dd1b9dSAndreas Gohr return array_unique($modes); 109c8dd1b9dSAndreas Gohr } 110c8dd1b9dSAndreas Gohr 111c8dd1b9dSAndreas Gohr /** 112c8dd1b9dSAndreas Gohr * Get the raw categories array. 113c8dd1b9dSAndreas Gohr * 114c8dd1b9dSAndreas Gohr * @return array<string, string[]> Category name => list of mode names 115c8dd1b9dSAndreas Gohr */ 116c8dd1b9dSAndreas Gohr public function getCategories(): array 117c8dd1b9dSAndreas Gohr { 118c8dd1b9dSAndreas Gohr global $PARSER_MODES; 119c8dd1b9dSAndreas Gohr return $PARSER_MODES; 120c8dd1b9dSAndreas Gohr } 121c8dd1b9dSAndreas Gohr 122c8dd1b9dSAndreas Gohr /** 123c8dd1b9dSAndreas Gohr * Register a mode in a category. 124c8dd1b9dSAndreas Gohr * 125c8dd1b9dSAndreas Gohr * @param string $category One of the CATEGORY_* constants 126c8dd1b9dSAndreas Gohr * @param string $modeName The mode name to register 127c8dd1b9dSAndreas Gohr * @return void 128c8dd1b9dSAndreas Gohr */ 129c8dd1b9dSAndreas Gohr public function registerMode(string $category, string $modeName): void 130c8dd1b9dSAndreas Gohr { 131c8dd1b9dSAndreas Gohr global $PARSER_MODES; 132c8dd1b9dSAndreas Gohr $PARSER_MODES[$category][] = $modeName; 133c8dd1b9dSAndreas Gohr $this->modes = null; // invalidate cached mode list 134c8dd1b9dSAndreas Gohr } 135c8dd1b9dSAndreas Gohr 136c8dd1b9dSAndreas Gohr /** 1377958e698SAndreas Gohr * Register a mode that handles its own line endings. 1387958e698SAndreas Gohr * Modes registered here will be skipped by Eol's connectTo(). 1397958e698SAndreas Gohr * 1407958e698SAndreas Gohr * @param string $mode The mode name 1417958e698SAndreas Gohr * @return void 1427958e698SAndreas Gohr */ 1437958e698SAndreas Gohr public function registerBlockEolMode(string $mode): void 1447958e698SAndreas Gohr { 1457958e698SAndreas Gohr $this->blockEolModes[] = $mode; 1467958e698SAndreas Gohr } 1477958e698SAndreas Gohr 1487958e698SAndreas Gohr /** 1497958e698SAndreas Gohr * Get all modes that handle their own line endings. 1507958e698SAndreas Gohr * 1517958e698SAndreas Gohr * @return string[] 1527958e698SAndreas Gohr */ 1537958e698SAndreas Gohr public function getBlockEolModes(): array 1547958e698SAndreas Gohr { 1557958e698SAndreas Gohr return $this->blockEolModes; 1567958e698SAndreas Gohr } 1577958e698SAndreas Gohr 1587958e698SAndreas Gohr /** 1597958e698SAndreas Gohr * Register regex-escaped line start marker characters for a mode. 1607958e698SAndreas Gohr * Preformatted uses these to build a negative lookahead. 1617958e698SAndreas Gohr * 1627958e698SAndreas Gohr * @param string $mode The mode name 1637958e698SAndreas Gohr * @param string[] $markers Regex-escaped marker characters (e.g. ['\\*', '\\-']) 1647958e698SAndreas Gohr * @return void 1657958e698SAndreas Gohr */ 1667958e698SAndreas Gohr public function registerLineStartMarkers(string $mode, array $markers): void 1677958e698SAndreas Gohr { 1687958e698SAndreas Gohr $this->lineStartMarkers[$mode] = $markers; 1697958e698SAndreas Gohr } 1707958e698SAndreas Gohr 1717958e698SAndreas Gohr /** 1727958e698SAndreas Gohr * Get all registered line start markers, merged and deduplicated. 1737958e698SAndreas Gohr * 1747958e698SAndreas Gohr * @return string[] 1757958e698SAndreas Gohr */ 1767958e698SAndreas Gohr public function getLineStartMarkers(): array 1777958e698SAndreas Gohr { 1787958e698SAndreas Gohr if (!$this->lineStartMarkers) return []; 1797958e698SAndreas Gohr return array_unique(array_merge(...array_values($this->lineStartMarkers))); 1807958e698SAndreas Gohr } 1817958e698SAndreas Gohr 1827958e698SAndreas Gohr /** 183c8dd1b9dSAndreas Gohr * Get all parser modes, fully instantiated and sorted by priority. 184c8dd1b9dSAndreas Gohr * 185c8dd1b9dSAndreas Gohr * This includes syntax plugins, built-in modes, formatting modes, and 186c8dd1b9dSAndreas Gohr * data-driven modes (smileys, acronyms, entities). Results are cached 187c8dd1b9dSAndreas Gohr * unless running in a test environment. 188c8dd1b9dSAndreas Gohr * 189c8dd1b9dSAndreas Gohr * @return array[] Each entry is ['sort' => int, 'mode' => string, 'obj' => ModeInterface] 190c8dd1b9dSAndreas Gohr */ 191c8dd1b9dSAndreas Gohr public function getModes(): array 192c8dd1b9dSAndreas Gohr { 193c8dd1b9dSAndreas Gohr global $conf; 194c8dd1b9dSAndreas Gohr 195c8dd1b9dSAndreas Gohr if ($this->modes !== null && !defined('DOKU_UNITTEST')) { 196c8dd1b9dSAndreas Gohr return $this->modes; 197c8dd1b9dSAndreas Gohr } 198c8dd1b9dSAndreas Gohr 199c8dd1b9dSAndreas Gohr global $PARSER_MODES; 200c8dd1b9dSAndreas Gohr $this->modes = []; 201c8dd1b9dSAndreas Gohr 202c8dd1b9dSAndreas Gohr // 1. Load syntax plugins and register their modes 203c8dd1b9dSAndreas Gohr $plugins = plugin_list('syntax'); 204c8dd1b9dSAndreas Gohr foreach ($plugins as $p) { 205c8dd1b9dSAndreas Gohr $obj = plugin_load('syntax', $p); 206c8dd1b9dSAndreas Gohr if (!$obj instanceof PluginInterface) continue; 207c8dd1b9dSAndreas Gohr $PARSER_MODES[$obj->getType()][] = "plugin_$p"; 208c8dd1b9dSAndreas Gohr $this->modes[] = [ 209c8dd1b9dSAndreas Gohr 'sort' => $obj->getSort(), 210c8dd1b9dSAndreas Gohr 'mode' => "plugin_$p", 211c8dd1b9dSAndreas Gohr 'obj' => $obj, 212c8dd1b9dSAndreas Gohr ]; 213c8dd1b9dSAndreas Gohr unset($obj); 214c8dd1b9dSAndreas Gohr } 215c8dd1b9dSAndreas Gohr 216c8dd1b9dSAndreas Gohr // 2. Add standard built-in modes 217c8dd1b9dSAndreas Gohr $builtinModes = [ 218c8dd1b9dSAndreas Gohr 'listblock', 'preformatted', 'notoc', 'nocache', 219c8dd1b9dSAndreas Gohr 'header', 'table', 'linebreak', 'footnote', 220c8dd1b9dSAndreas Gohr 'hr', 'unformatted', 'code', 'file', 'quote', 221c8dd1b9dSAndreas Gohr 'internallink', 'rss', 'media', 'externallink', 222c8dd1b9dSAndreas Gohr 'emaillink', 'windowssharelink', 'eol', 2231f443476SAndreas Gohr 'strong', 'emphasis', 'underline', 'monospace', 2241f443476SAndreas Gohr 'subscript', 'superscript', 'deleted', 225c8dd1b9dSAndreas Gohr ]; 226c8dd1b9dSAndreas Gohr if ($conf['typography']) { 227c8dd1b9dSAndreas Gohr $builtinModes[] = 'quotes'; 228c8dd1b9dSAndreas Gohr $builtinModes[] = 'multiplyentity'; 229c8dd1b9dSAndreas Gohr } 230c8dd1b9dSAndreas Gohr foreach ($builtinModes as $mode) { 231c8dd1b9dSAndreas Gohr $class = 'dokuwiki\\Parsing\\ParserMode\\' . ucfirst($mode); 232c8dd1b9dSAndreas Gohr $obj = new $class(); 233c8dd1b9dSAndreas Gohr $this->modes[] = [ 234c8dd1b9dSAndreas Gohr 'sort' => $obj->getSort(), 235c8dd1b9dSAndreas Gohr 'mode' => $mode, 236c8dd1b9dSAndreas Gohr 'obj' => $obj, 237c8dd1b9dSAndreas Gohr ]; 238c8dd1b9dSAndreas Gohr } 239c8dd1b9dSAndreas Gohr 2401f443476SAndreas Gohr // 3. Add data-driven modes 241c8dd1b9dSAndreas Gohr $obj = new Smiley(array_keys(getSmileys())); 242c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'smiley', 'obj' => $obj]; 243c8dd1b9dSAndreas Gohr 244c8dd1b9dSAndreas Gohr $obj = new Acronym(array_keys(getAcronyms())); 245c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'acronym', 'obj' => $obj]; 246c8dd1b9dSAndreas Gohr 247c8dd1b9dSAndreas Gohr $obj = new Entity(array_keys(getEntities())); 248c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'entity', 'obj' => $obj]; 249c8dd1b9dSAndreas Gohr 2501f443476SAndreas Gohr // 4. Optional camelcase mode 251c8dd1b9dSAndreas Gohr if (!empty($conf['camelcase'])) { 252c8dd1b9dSAndreas Gohr $obj = new Camelcaselink(); 253c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'camelcaselink', 'obj' => $obj]; 254c8dd1b9dSAndreas Gohr } 255c8dd1b9dSAndreas Gohr 2561f443476SAndreas Gohr // 5. Sort by priority 257c8dd1b9dSAndreas Gohr usort($this->modes, self::sortModes(...)); 258c8dd1b9dSAndreas Gohr 259c8dd1b9dSAndreas Gohr return $this->modes; 260c8dd1b9dSAndreas Gohr } 261c8dd1b9dSAndreas Gohr 262c8dd1b9dSAndreas Gohr /** 263c8dd1b9dSAndreas Gohr * Callback function for usort 264c8dd1b9dSAndreas Gohr * 265c8dd1b9dSAndreas Gohr * @param array $a 266c8dd1b9dSAndreas Gohr * @param array $b 267c8dd1b9dSAndreas Gohr * @return int 268c8dd1b9dSAndreas Gohr */ 269c8dd1b9dSAndreas Gohr public static function sortModes(array $a, array $b): int 270c8dd1b9dSAndreas Gohr { 271c8dd1b9dSAndreas Gohr return $a['sort'] <=> $b['sort']; 272c8dd1b9dSAndreas Gohr } 273c8dd1b9dSAndreas Gohr} 274