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