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