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