xref: /dokuwiki/inc/Parsing/ParserMode/AbstractFormatting.php (revision 057ab5a057a76df652cfe4176512a550ca9937f6)
1<?php
2
3namespace dokuwiki\Parsing\ParserMode;
4
5use dokuwiki\Parsing\Handler;
6use dokuwiki\Parsing\Lexer\Lexer;
7use dokuwiki\Parsing\ModeRegistry;
8
9/**
10 * Base class for inline formatting modes (bold, italic, underline, etc.)
11 *
12 * Each concrete subclass defines its entry/exit patterns, mode name, and sort order.
13 */
14abstract class AbstractFormatting extends AbstractMode
15{
16    /**
17     * Formatting modes accept other formatting, substitutions, and disabled modes.
18     *
19     * @inheritdoc
20     */
21    protected function allowedCategories(): array
22    {
23        return [
24            ModeRegistry::CATEGORY_FORMATTING,
25            ModeRegistry::CATEGORY_SUBSTITUTION,
26            ModeRegistry::CATEGORY_DISABLED,
27        ];
28    }
29
30    /**
31     * Exclude self to prevent self-nesting (e.g. bold inside bold).
32     *
33     * @inheritdoc
34     */
35    protected function filterAllowedModes(array $modes): array
36    {
37        $self = $this->getModeName();
38        return array_values(array_filter($modes, static fn($mode) => $mode !== $self));
39    }
40
41    /** @inheritdoc */
42    public function connectTo($mode)
43    {
44        // Can't nest formatting in itself
45        if ($mode === $this->getModeName()) {
46            return;
47        }
48
49        $this->Lexer->addEntryPattern(
50            $this->getEntryPattern(),
51            $mode,
52            $this->getModeName()
53        );
54    }
55
56    /**
57     * @return string The regex pattern that starts this formatting
58     */
59    abstract protected function getEntryPattern(): string;
60
61    /**
62     * @return string The regex pattern that ends this formatting
63     */
64    abstract protected function getExitPattern(): string;
65
66    /**
67     * Regex fragment matching a valid closer for this formatting: the
68     * closing delimiter itself, with flanking requirements expressed as
69     * lookarounds. A valid closer is exactly where the exit pattern would
70     * fire, so the default returns getExitPattern(). A subclass may
71     * override to demand more context than the exit does, or to return
72     * null for modes whose entry pattern already verifies its closer in
73     * linear time (e.g. a body that cannot contain the delimiter
74     * character, making the lookahead self-limiting).
75     *
76     * The pattern must match where the delimiter starts and must not
77     * consume flanking context: the lexer compares closer positions across
78     * modes to decide whether a nested delimiter may open, and a consumed
79     * flanking character would both skew that comparison and hide a closer
80     * that directly follows the opener (see Lexer::addCloserPattern()).
81     *
82     * When non-null, it is registered via Lexer::addCloserPattern() with the
83     * paragraph break as boundary: an opener candidate is accepted only when
84     * this fragment occurs between it and the next blank line, so an opener
85     * without a valid closer stays literal text and formatting never spans
86     * paragraphs. The entry pattern should then perform only cheap local
87     * checks (delimiter and flanking lookarounds); see addCloserPattern()
88     * for why the closer check must not live in the entry pattern itself.
89     *
90     * @return string|null
91     */
92    protected function getCloserPattern(): ?string
93    {
94        return $this->getExitPattern();
95    }
96
97    /**
98     * @return string The mode name used for lexer registration
99     */
100    abstract protected function getModeName(): string;
101
102    /**
103     * @return string The name used for emitted open/close handler instructions
104     *
105     * Defaults to the mode name. Override in subclasses where the emitted
106     * instruction should differ from the lexer mode name (e.g. Gfm modes
107     * that share instructions with a DW counterpart).
108     */
109    protected function getInstructionName(): string
110    {
111        return $this->getModeName();
112    }
113
114    /** @inheritdoc */
115    public function postConnect()
116    {
117        $this->Lexer->addExitPattern(
118            $this->getExitPattern(),
119            $this->getModeName()
120        );
121
122        $closer = $this->getCloserPattern();
123        if ($closer !== null) {
124            $this->Lexer->addCloserPattern($closer, $this->getModeName(), Lexer::PARA_BREAK);
125        }
126    }
127
128    /** @inheritdoc */
129    public function handle($match, $state, $pos, Handler $handler)
130    {
131        $name = $this->getInstructionName();
132        match ($state) {
133            DOKU_LEXER_ENTER => $handler->addCall($name . '_open', [], $pos),
134            DOKU_LEXER_EXIT => $handler->addCall($name . '_close', [], $pos),
135            DOKU_LEXER_UNMATCHED => $handler->addCall('cdata', [$match], $pos),
136            default => true,
137        };
138        return true;
139    }
140}
141