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