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'; 27c8dd1b9dSAndreas Gohr public const CATEGORY_SUBSTITION = '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 35c8dd1b9dSAndreas Gohr private static ?self $instance = null; 36c8dd1b9dSAndreas Gohr 37c8dd1b9dSAndreas Gohr /** 38c8dd1b9dSAndreas Gohr * Get the singleton instance of the ModeRegistry. 39c8dd1b9dSAndreas Gohr * 40c8dd1b9dSAndreas Gohr * @return self 41c8dd1b9dSAndreas Gohr */ 42c8dd1b9dSAndreas Gohr public static function getInstance(): self 43c8dd1b9dSAndreas Gohr { 44c8dd1b9dSAndreas Gohr if (!self::$instance instanceof self) { 45c8dd1b9dSAndreas Gohr self::$instance = new self(); 46c8dd1b9dSAndreas Gohr } 47c8dd1b9dSAndreas Gohr return self::$instance; 48c8dd1b9dSAndreas Gohr } 49c8dd1b9dSAndreas Gohr 50c8dd1b9dSAndreas Gohr /** 51c8dd1b9dSAndreas Gohr * Reset the singleton instance. 52c8dd1b9dSAndreas Gohr * 53c8dd1b9dSAndreas Gohr * This is mainly useful for testing to force re-initialization. 54c8dd1b9dSAndreas Gohr * 55c8dd1b9dSAndreas Gohr * @return void 56c8dd1b9dSAndreas Gohr */ 57c8dd1b9dSAndreas Gohr public static function reset(): void 58c8dd1b9dSAndreas Gohr { 59c8dd1b9dSAndreas Gohr self::$instance = null; 60c8dd1b9dSAndreas Gohr } 61c8dd1b9dSAndreas Gohr 62c8dd1b9dSAndreas Gohr /** 63c8dd1b9dSAndreas Gohr * Constructor. Initializes the global $PARSER_MODES array with the default mode categories. 64c8dd1b9dSAndreas Gohr */ 65c8dd1b9dSAndreas Gohr private function __construct() 66c8dd1b9dSAndreas Gohr { 67c8dd1b9dSAndreas Gohr global $PARSER_MODES; 68c8dd1b9dSAndreas Gohr $PARSER_MODES = [ 69c8dd1b9dSAndreas Gohr self::CATEGORY_CONTAINER => ['listblock', 'table', 'quote', 'hr'], 70c8dd1b9dSAndreas Gohr self::CATEGORY_BASEONLY => ['header'], 71c8dd1b9dSAndreas Gohr self::CATEGORY_FORMATTING => [ 72c8dd1b9dSAndreas Gohr 'strong', 'emphasis', 'underline', 'monospace', 73c8dd1b9dSAndreas Gohr 'subscript', 'superscript', 'deleted', 'footnote', 74c8dd1b9dSAndreas Gohr ], 75c8dd1b9dSAndreas Gohr self::CATEGORY_SUBSTITION => [ 76c8dd1b9dSAndreas Gohr 'acronym', 'smiley', 'wordblock', 'entity', 77c8dd1b9dSAndreas Gohr 'camelcaselink', 'internallink', 'media', 'externallink', 78c8dd1b9dSAndreas Gohr 'linebreak', 'emaillink', 'windowssharelink', 'filelink', 79c8dd1b9dSAndreas Gohr 'notoc', 'nocache', 'multiplyentity', 'quotes', 'rss', 80c8dd1b9dSAndreas Gohr ], 81c8dd1b9dSAndreas Gohr self::CATEGORY_PROTECTED => ['preformatted', 'code', 'file'], 82c8dd1b9dSAndreas Gohr self::CATEGORY_DISABLED => ['unformatted'], 83c8dd1b9dSAndreas Gohr self::CATEGORY_PARAGRAPHS => ['eol'], 84c8dd1b9dSAndreas Gohr ]; 85c8dd1b9dSAndreas Gohr } 86c8dd1b9dSAndreas Gohr 87c8dd1b9dSAndreas Gohr /** 88c8dd1b9dSAndreas Gohr * Get all mode names in the given categories. 89c8dd1b9dSAndreas Gohr * 90c8dd1b9dSAndreas Gohr * @param string[] $categories One or more CATEGORY_* constants 91c8dd1b9dSAndreas Gohr * @return string[] Unique list of mode names 92c8dd1b9dSAndreas Gohr */ 93c8dd1b9dSAndreas Gohr public function getModesForCategories(array $categories): array 94c8dd1b9dSAndreas Gohr { 95c8dd1b9dSAndreas Gohr global $PARSER_MODES; 96c8dd1b9dSAndreas Gohr $modes = []; 97c8dd1b9dSAndreas Gohr foreach ($categories as $cat) { 98c8dd1b9dSAndreas Gohr if (isset($PARSER_MODES[$cat])) { 99c8dd1b9dSAndreas Gohr $modes = array_merge($modes, $PARSER_MODES[$cat]); 100c8dd1b9dSAndreas Gohr } 101c8dd1b9dSAndreas Gohr } 102c8dd1b9dSAndreas Gohr return array_unique($modes); 103c8dd1b9dSAndreas Gohr } 104c8dd1b9dSAndreas Gohr 105c8dd1b9dSAndreas Gohr /** 106c8dd1b9dSAndreas Gohr * Get the raw categories array. 107c8dd1b9dSAndreas Gohr * 108c8dd1b9dSAndreas Gohr * @return array<string, string[]> Category name => list of mode names 109c8dd1b9dSAndreas Gohr */ 110c8dd1b9dSAndreas Gohr public function getCategories(): array 111c8dd1b9dSAndreas Gohr { 112c8dd1b9dSAndreas Gohr global $PARSER_MODES; 113c8dd1b9dSAndreas Gohr return $PARSER_MODES; 114c8dd1b9dSAndreas Gohr } 115c8dd1b9dSAndreas Gohr 116c8dd1b9dSAndreas Gohr /** 117c8dd1b9dSAndreas Gohr * Register a mode in a category. 118c8dd1b9dSAndreas Gohr * 119c8dd1b9dSAndreas Gohr * @param string $category One of the CATEGORY_* constants 120c8dd1b9dSAndreas Gohr * @param string $modeName The mode name to register 121c8dd1b9dSAndreas Gohr * @return void 122c8dd1b9dSAndreas Gohr */ 123c8dd1b9dSAndreas Gohr public function registerMode(string $category, string $modeName): void 124c8dd1b9dSAndreas Gohr { 125c8dd1b9dSAndreas Gohr global $PARSER_MODES; 126c8dd1b9dSAndreas Gohr $PARSER_MODES[$category][] = $modeName; 127c8dd1b9dSAndreas Gohr $this->modes = null; // invalidate cached mode list 128c8dd1b9dSAndreas Gohr } 129c8dd1b9dSAndreas Gohr 130c8dd1b9dSAndreas Gohr /** 131c8dd1b9dSAndreas Gohr * Get all parser modes, fully instantiated and sorted by priority. 132c8dd1b9dSAndreas Gohr * 133c8dd1b9dSAndreas Gohr * This includes syntax plugins, built-in modes, formatting modes, and 134c8dd1b9dSAndreas Gohr * data-driven modes (smileys, acronyms, entities). Results are cached 135c8dd1b9dSAndreas Gohr * unless running in a test environment. 136c8dd1b9dSAndreas Gohr * 137c8dd1b9dSAndreas Gohr * @return array[] Each entry is ['sort' => int, 'mode' => string, 'obj' => ModeInterface] 138c8dd1b9dSAndreas Gohr */ 139c8dd1b9dSAndreas Gohr public function getModes(): array 140c8dd1b9dSAndreas Gohr { 141c8dd1b9dSAndreas Gohr global $conf; 142c8dd1b9dSAndreas Gohr 143c8dd1b9dSAndreas Gohr if ($this->modes !== null && !defined('DOKU_UNITTEST')) { 144c8dd1b9dSAndreas Gohr return $this->modes; 145c8dd1b9dSAndreas Gohr } 146c8dd1b9dSAndreas Gohr 147c8dd1b9dSAndreas Gohr global $PARSER_MODES; 148c8dd1b9dSAndreas Gohr $this->modes = []; 149c8dd1b9dSAndreas Gohr 150c8dd1b9dSAndreas Gohr // 1. Load syntax plugins and register their modes 151c8dd1b9dSAndreas Gohr $plugins = plugin_list('syntax'); 152c8dd1b9dSAndreas Gohr foreach ($plugins as $p) { 153c8dd1b9dSAndreas Gohr $obj = plugin_load('syntax', $p); 154c8dd1b9dSAndreas Gohr if (!$obj instanceof PluginInterface) continue; 155c8dd1b9dSAndreas Gohr $PARSER_MODES[$obj->getType()][] = "plugin_$p"; 156c8dd1b9dSAndreas Gohr $this->modes[] = [ 157c8dd1b9dSAndreas Gohr 'sort' => $obj->getSort(), 158c8dd1b9dSAndreas Gohr 'mode' => "plugin_$p", 159c8dd1b9dSAndreas Gohr 'obj' => $obj, 160c8dd1b9dSAndreas Gohr ]; 161c8dd1b9dSAndreas Gohr unset($obj); 162c8dd1b9dSAndreas Gohr } 163c8dd1b9dSAndreas Gohr 164c8dd1b9dSAndreas Gohr // 2. Add standard built-in modes 165c8dd1b9dSAndreas Gohr $builtinModes = [ 166c8dd1b9dSAndreas Gohr 'listblock', 'preformatted', 'notoc', 'nocache', 167c8dd1b9dSAndreas Gohr 'header', 'table', 'linebreak', 'footnote', 168c8dd1b9dSAndreas Gohr 'hr', 'unformatted', 'code', 'file', 'quote', 169c8dd1b9dSAndreas Gohr 'internallink', 'rss', 'media', 'externallink', 170c8dd1b9dSAndreas Gohr 'emaillink', 'windowssharelink', 'eol', 171*1f443476SAndreas Gohr 'strong', 'emphasis', 'underline', 'monospace', 172*1f443476SAndreas Gohr 'subscript', 'superscript', 'deleted', 173c8dd1b9dSAndreas Gohr ]; 174c8dd1b9dSAndreas Gohr if ($conf['typography']) { 175c8dd1b9dSAndreas Gohr $builtinModes[] = 'quotes'; 176c8dd1b9dSAndreas Gohr $builtinModes[] = 'multiplyentity'; 177c8dd1b9dSAndreas Gohr } 178c8dd1b9dSAndreas Gohr foreach ($builtinModes as $mode) { 179c8dd1b9dSAndreas Gohr $class = 'dokuwiki\\Parsing\\ParserMode\\' . ucfirst($mode); 180c8dd1b9dSAndreas Gohr $obj = new $class(); 181c8dd1b9dSAndreas Gohr $this->modes[] = [ 182c8dd1b9dSAndreas Gohr 'sort' => $obj->getSort(), 183c8dd1b9dSAndreas Gohr 'mode' => $mode, 184c8dd1b9dSAndreas Gohr 'obj' => $obj, 185c8dd1b9dSAndreas Gohr ]; 186c8dd1b9dSAndreas Gohr } 187c8dd1b9dSAndreas Gohr 188*1f443476SAndreas Gohr // 3. Add data-driven modes 189c8dd1b9dSAndreas Gohr $obj = new Smiley(array_keys(getSmileys())); 190c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'smiley', 'obj' => $obj]; 191c8dd1b9dSAndreas Gohr 192c8dd1b9dSAndreas Gohr $obj = new Acronym(array_keys(getAcronyms())); 193c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'acronym', 'obj' => $obj]; 194c8dd1b9dSAndreas Gohr 195c8dd1b9dSAndreas Gohr $obj = new Entity(array_keys(getEntities())); 196c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'entity', 'obj' => $obj]; 197c8dd1b9dSAndreas Gohr 198*1f443476SAndreas Gohr // 4. Optional camelcase mode 199c8dd1b9dSAndreas Gohr if (!empty($conf['camelcase'])) { 200c8dd1b9dSAndreas Gohr $obj = new Camelcaselink(); 201c8dd1b9dSAndreas Gohr $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'camelcaselink', 'obj' => $obj]; 202c8dd1b9dSAndreas Gohr } 203c8dd1b9dSAndreas Gohr 204*1f443476SAndreas Gohr // 5. Sort by priority 205c8dd1b9dSAndreas Gohr usort($this->modes, self::sortModes(...)); 206c8dd1b9dSAndreas Gohr 207c8dd1b9dSAndreas Gohr return $this->modes; 208c8dd1b9dSAndreas Gohr } 209c8dd1b9dSAndreas Gohr 210c8dd1b9dSAndreas Gohr /** 211c8dd1b9dSAndreas Gohr * Callback function for usort 212c8dd1b9dSAndreas Gohr * 213c8dd1b9dSAndreas Gohr * @param array $a 214c8dd1b9dSAndreas Gohr * @param array $b 215c8dd1b9dSAndreas Gohr * @return int 216c8dd1b9dSAndreas Gohr */ 217c8dd1b9dSAndreas Gohr public static function sortModes(array $a, array $b): int 218c8dd1b9dSAndreas Gohr { 219c8dd1b9dSAndreas Gohr return $a['sort'] <=> $b['sort']; 220c8dd1b9dSAndreas Gohr } 221c8dd1b9dSAndreas Gohr} 222