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 /** @var array<string, string[]> Mode name => regex-escaped line start marker characters */ 39 private array $lineStartMarkers = []; 40 41 private static ?self $instance = null; 42 43 /** 44 * Get the singleton instance of the ModeRegistry. 45 * 46 * @return self 47 */ 48 public static function getInstance(): self 49 { 50 if (!self::$instance instanceof self) { 51 self::$instance = new self(); 52 } 53 return self::$instance; 54 } 55 56 /** 57 * Reset the singleton instance. 58 * 59 * This is mainly useful for testing to force re-initialization. 60 * 61 * @return void 62 */ 63 public static function reset(): void 64 { 65 self::$instance = null; 66 } 67 68 /** 69 * Constructor. Initializes the global $PARSER_MODES array with the default mode categories. 70 */ 71 private function __construct() 72 { 73 global $PARSER_MODES; 74 $PARSER_MODES = [ 75 self::CATEGORY_CONTAINER => ['listblock', 'table', 'quote', 'hr'], 76 self::CATEGORY_BASEONLY => ['header'], 77 self::CATEGORY_FORMATTING => [ 78 'strong', 'emphasis', 'underline', 'monospace', 79 'subscript', 'superscript', 'deleted', 'footnote', 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 ], 87 self::CATEGORY_PROTECTED => ['preformatted', 'code', 'file'], 88 self::CATEGORY_DISABLED => ['unformatted'], 89 self::CATEGORY_PARAGRAPHS => ['eol'], 90 ]; 91 } 92 93 /** 94 * Get all mode names in the given categories. 95 * 96 * @param string[] $categories One or more CATEGORY_* constants 97 * @return string[] Unique list of mode names 98 */ 99 public function getModesForCategories(array $categories): array 100 { 101 global $PARSER_MODES; 102 $modes = []; 103 foreach ($categories as $cat) { 104 if (isset($PARSER_MODES[$cat])) { 105 $modes = array_merge($modes, $PARSER_MODES[$cat]); 106 } 107 } 108 return array_unique($modes); 109 } 110 111 /** 112 * Get the raw categories array. 113 * 114 * @return array<string, string[]> Category name => list of mode names 115 */ 116 public function getCategories(): array 117 { 118 global $PARSER_MODES; 119 return $PARSER_MODES; 120 } 121 122 /** 123 * Register a mode in a category. 124 * 125 * @param string $category One of the CATEGORY_* constants 126 * @param string $modeName The mode name to register 127 * @return void 128 */ 129 public function registerMode(string $category, string $modeName): void 130 { 131 global $PARSER_MODES; 132 $PARSER_MODES[$category][] = $modeName; 133 $this->modes = null; // invalidate cached mode list 134 } 135 136 /** 137 * Register a mode that handles its own line endings. 138 * Modes registered here will be skipped by Eol's connectTo(). 139 * 140 * @param string $mode The mode name 141 * @return void 142 */ 143 public function registerBlockEolMode(string $mode): void 144 { 145 $this->blockEolModes[] = $mode; 146 } 147 148 /** 149 * Get all modes that handle their own line endings. 150 * 151 * @return string[] 152 */ 153 public function getBlockEolModes(): array 154 { 155 return $this->blockEolModes; 156 } 157 158 /** 159 * Register regex-escaped line start marker characters for a mode. 160 * Preformatted uses these to build a negative lookahead. 161 * 162 * @param string $mode The mode name 163 * @param string[] $markers Regex-escaped marker characters (e.g. ['\\*', '\\-']) 164 * @return void 165 */ 166 public function registerLineStartMarkers(string $mode, array $markers): void 167 { 168 $this->lineStartMarkers[$mode] = $markers; 169 } 170 171 /** 172 * Get all registered line start markers, merged and deduplicated. 173 * 174 * @return string[] 175 */ 176 public function getLineStartMarkers(): array 177 { 178 if (!$this->lineStartMarkers) return []; 179 return array_unique(array_merge(...array_values($this->lineStartMarkers))); 180 } 181 182 /** 183 * Get all parser modes, fully instantiated and sorted by priority. 184 * 185 * This includes syntax plugins, built-in modes, formatting modes, and 186 * data-driven modes (smileys, acronyms, entities). Results are cached 187 * unless running in a test environment. 188 * 189 * @return array[] Each entry is ['sort' => int, 'mode' => string, 'obj' => ModeInterface] 190 */ 191 public function getModes(): array 192 { 193 global $conf; 194 195 if ($this->modes !== null && !defined('DOKU_UNITTEST')) { 196 return $this->modes; 197 } 198 199 global $PARSER_MODES; 200 $this->modes = []; 201 202 // 1. Load syntax plugins and register their modes 203 $plugins = plugin_list('syntax'); 204 foreach ($plugins as $p) { 205 $obj = plugin_load('syntax', $p); 206 if (!$obj instanceof PluginInterface) continue; 207 $PARSER_MODES[$obj->getType()][] = "plugin_$p"; 208 $this->modes[] = [ 209 'sort' => $obj->getSort(), 210 'mode' => "plugin_$p", 211 'obj' => $obj, 212 ]; 213 unset($obj); 214 } 215 216 // 2. Add standard built-in modes 217 $builtinModes = [ 218 'listblock', 'preformatted', 'notoc', 'nocache', 219 'header', 'table', 'linebreak', 'footnote', 220 'hr', 'unformatted', 'code', 'file', 'quote', 221 'internallink', 'rss', 'media', 'externallink', 222 'emaillink', 'windowssharelink', 'eol', 223 'strong', 'emphasis', 'underline', 'monospace', 224 'subscript', 'superscript', 'deleted', 225 ]; 226 if ($conf['typography']) { 227 $builtinModes[] = 'quotes'; 228 $builtinModes[] = 'multiplyentity'; 229 } 230 foreach ($builtinModes as $mode) { 231 $class = 'dokuwiki\\Parsing\\ParserMode\\' . ucfirst($mode); 232 $obj = new $class(); 233 $this->modes[] = [ 234 'sort' => $obj->getSort(), 235 'mode' => $mode, 236 'obj' => $obj, 237 ]; 238 } 239 240 // 3. Add data-driven modes 241 $obj = new Smiley(array_keys(getSmileys())); 242 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'smiley', 'obj' => $obj]; 243 244 $obj = new Acronym(array_keys(getAcronyms())); 245 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'acronym', 'obj' => $obj]; 246 247 $obj = new Entity(array_keys(getEntities())); 248 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'entity', 'obj' => $obj]; 249 250 // 4. Optional camelcase mode 251 if (!empty($conf['camelcase'])) { 252 $obj = new Camelcaselink(); 253 $this->modes[] = ['sort' => $obj->getSort(), 'mode' => 'camelcaselink', 'obj' => $obj]; 254 } 255 256 // 5. Sort by priority 257 usort($this->modes, self::sortModes(...)); 258 259 return $this->modes; 260 } 261 262 /** 263 * Callback function for usort 264 * 265 * @param array $a 266 * @param array $b 267 * @return int 268 */ 269 public static function sortModes(array $a, array $b): int 270 { 271 return $a['sort'] <=> $b['sort']; 272 } 273} 274