xref: /dokuwiki/inc/Parsing/ParserMode/GfmBacktickSingle.php (revision 057ab5a057a76df652cfe4176512a550ca9937f6)
1<?php
2
3namespace dokuwiki\Parsing\ParserMode;
4
5use dokuwiki\Parsing\Handler;
6
7/**
8 * GFM inline code span bounded by single backticks: `text`.
9 *
10 * A backtick span is both monospace-formatted and verbatim: the content
11 * is wrapped in monospace_open / monospace_close (the same instructions
12 * as DokuWiki's doubled-single-quote pair, rendered as an HTML <code>
13 * element) and the body is emitted through the unformatted handler
14 * rather than plain cdata, so renderers that distinguish the two
15 * (metadata, indexer, non-XHTML backends) treat it as literal.
16 *
17 * The entry pattern's lookahead only verifies three things: an opener,
18 * at least one body character, and a valid closer. It does NOT enforce
19 * non-whitespace body edges or a non-whitespace body interior. GFM's
20 * edge rules are applied in handle() after the body has been extracted:
21 *
22 *   1. Line endings become single spaces.
23 *   2. If the body both starts and ends with a space, and is not
24 *      entirely whitespace, one space is stripped from each end.
25 *
26 * This lets the regex stay small while still producing GFM-correct
27 * output for the tricky cases:
28 *
29 *   ` `          ->   <code> </code>     (all-whitespace body, no strip)
30 *   ` a`         ->   <code> a</code>    (asymmetric edge, no strip)
31 *   ` `` `       ->   <code>``</code>    (run of 2 inside body, strip)
32 *
33 * Runs of two or more backticks on either delimiter are rejected by
34 * the length-boundary guards (?<!`)...(?!`), so this mode never steals
35 * input from GfmBacktickDouble. GfmBacktickDouble extends this class
36 * to reuse handle() and normalizeBody().
37 *
38 * No other inline parsing runs inside a span; allowedModes is empty.
39 *
40 * @see GfmBacktickDouble
41 */
42class GfmBacktickSingle extends AbstractMode
43{
44    /** @inheritdoc */
45    public function getSort()
46    {
47        return 165;
48    }
49
50    /** The lexer state / mode name. Subclasses override for n≥2. */
51    protected function getModeName(): string
52    {
53        return 'gfm_backtick_single';
54    }
55
56    /**
57     * Delimiter: a lone backtick. The length-boundary guards
58     * (?<!`)...(?!`) ensure a run of two or more backticks is never read
59     * as an n=1 opener or closer.
60     */
61    protected function getDelimiterPattern(): string
62    {
63        return '(?<!`)`(?!`)';
64    }
65
66    /**
67     * Span body. Admits runs of non-backticks, newlines that don't start
68     * a blank line, and runs of two-or-more backticks — the latter live
69     * inside the body since they cannot be valid n=1 closers.
70     *
71     * The alternatives start with mutually exclusive characters and all
72     * quantifiers are possessive, so a scan over the body never
73     * backtracks: it stops at the first lone backtick (or fails at the
74     * paragraph break) without accumulating backtracking state.
75     */
76    protected function getBodyPattern(): string
77    {
78        return '(?:[^`\n]++|' . self::NOT_AT_PARA_BREAK . '\n|``++)++';
79    }
80
81    /**
82     * Entry pattern: an opening delimiter whose lookahead verifies a body
83     * and a closing delimiter ahead. The lookahead is self-limiting — a
84     * lone backtick ahead is always a valid closer, so at most one scan
85     * per paragraph can fail — which keeps it linear without the memoized
86     * closer machinery of Lexer::addCloserPattern().
87     */
88    protected function getEntryPattern(): string
89    {
90        return $this->getDelimiterPattern()
91            . '(?=' . $this->getBodyPattern() . $this->getDelimiterPattern() . ')';
92    }
93
94    /** Exit pattern: the same delimiter that opened the span. */
95    protected function getExitPattern(): string
96    {
97        return $this->getDelimiterPattern();
98    }
99
100    /** @inheritdoc */
101    public function connectTo($mode)
102    {
103        $this->Lexer->addEntryPattern(
104            $this->getEntryPattern(),
105            $mode,
106            $this->getModeName()
107        );
108    }
109
110    /** @inheritdoc */
111    public function postConnect()
112    {
113        $this->Lexer->addExitPattern($this->getExitPattern(), $this->getModeName());
114    }
115
116    /** @inheritdoc */
117    public function handle($match, $state, $pos, Handler $handler)
118    {
119        match ($state) {
120            DOKU_LEXER_ENTER => $handler->addCall('monospace_open', [], $pos),
121            DOKU_LEXER_EXIT => $handler->addCall('monospace_close', [], $pos),
122            DOKU_LEXER_UNMATCHED => $handler->addCall(
123                'unformatted',
124                [$this->normalizeBody($match)],
125                $pos
126            ),
127            default => true,
128        };
129        return true;
130    }
131
132    /**
133     * GFM code-span body normalization: newlines become spaces; if both
134     * ends are spaces and the body isn't entirely whitespace, strip one
135     * space from each end.
136     */
137    protected function normalizeBody(string $body): string
138    {
139        $body = str_replace(["\r\n", "\r", "\n"], ' ', $body);
140        if (
141            strlen($body) >= 2
142            && $body[0] === ' '
143            && $body[-1] === ' '
144            && trim($body) !== ''
145        ) {
146            $body = substr($body, 1, -1);
147        }
148        return $body;
149    }
150}
151