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