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