xref: /dokuwiki/inc/Parsing/ModeRegistry.php (revision c8dd1b9d24a2f9905db764a0ac4646fb1e172af4)
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