xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmBacktickSingleTest.php (revision 884caed926ca0aa0af6ce3f34ae3aa7317a3361a)
1<?php
2
3namespace dokuwiki\test\Parsing\ParserMode;
4
5use dokuwiki\Parsing\ParserMode\GfmBacktickSingle;
6use dokuwiki\Parsing\ParserMode\GfmEmphasis;
7
8/**
9 * Tests for the GFM inline code-span mode — single-backtick spans.
10 */
11class GfmBacktickSingleTest extends ParserTestBase
12{
13    public function setUp(): void
14    {
15        parent::setUp();
16        $this->setSyntax('md');
17    }
18
19    function testBasicCodeSpan()
20    {
21        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
22        $this->P->parse('Foo `Bar` Baz');
23        $calls = [
24            ['document_start', []],
25            ['p_open', []],
26            ['cdata', ["\nFoo "]],
27            ['monospace_open', []],
28            ['unformatted', ['Bar']],
29            ['monospace_close', []],
30            ['cdata', [' Baz']],
31            ['p_close', []],
32            ['document_end', []],
33        ];
34        $this->assertCalls($calls, $this->H->calls);
35    }
36
37    function testSingleCharacterBody()
38    {
39        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
40        $this->P->parse('foo `b` bar');
41        $calls = [
42            ['document_start', []],
43            ['p_open', []],
44            ['cdata', ["\nfoo "]],
45            ['monospace_open', []],
46            ['unformatted', ['b']],
47            ['monospace_close', []],
48            ['cdata', [' bar']],
49            ['p_close', []],
50            ['document_end', []],
51        ];
52        $this->assertCalls($calls, $this->H->calls);
53    }
54
55    function testTwoSeparateSpansOnOneLine()
56    {
57        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
58        $this->P->parse('`one` and `two`');
59        $calls = [
60            ['document_start', []],
61            ['p_open', []],
62            ['cdata', ["\n"]],
63            ['monospace_open', []],
64            ['unformatted', ['one']],
65            ['monospace_close', []],
66            ['cdata', [' and ']],
67            ['monospace_open', []],
68            ['unformatted', ['two']],
69            ['monospace_close', []],
70            ['cdata', ['']],
71            ['p_close', []],
72            ['document_end', []],
73        ];
74        $this->assertCalls($calls, $this->H->calls);
75    }
76
77    function testUnmatchedOpenerStaysLiteral()
78    {
79        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
80        $this->P->parse('foo `bar with no closer');
81        $modes = array_column($this->H->calls, 0);
82        $this->assertNotContains('monospace_open', $modes,
83            'Unmatched opening backtick must stay literal');
84    }
85
86    function testAsymmetricEdgeSpaceIsPreserved()
87    {
88        // GFM example 342. Input ` a` — a leading space but no trailing
89        // space. Body stays as " a"; strip only fires when BOTH ends are
90        // whitespace.
91        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
92        $this->P->parse('` a`');
93        $calls = [
94            ['document_start', []],
95            ['p_open', []],
96            ['cdata', ["\n"]],
97            ['monospace_open', []],
98            ['unformatted', [' a']],
99            ['monospace_close', []],
100            ['cdata', ['']],
101            ['p_close', []],
102            ['document_end', []],
103        ];
104        $this->assertCalls($calls, $this->H->calls);
105    }
106
107    function testSymmetricEdgeSpaceIsStripped()
108    {
109        // Body with whitespace on both sides and non-whitespace content
110        // in the middle gets one space stripped from each end. Input
111        // body is " foo "; after strip it becomes "foo".
112        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
113        $this->P->parse('` foo `');
114        $calls = [
115            ['document_start', []],
116            ['p_open', []],
117            ['cdata', ["\n"]],
118            ['monospace_open', []],
119            ['unformatted', ['foo']],
120            ['monospace_close', []],
121            ['cdata', ['']],
122            ['p_close', []],
123            ['document_end', []],
124        ];
125        $this->assertCalls($calls, $this->H->calls);
126    }
127
128    function testAllWhitespaceBodyIsPreserved()
129    {
130        // A body of pure whitespace is a valid code span and kept as is
131        // (strip is skipped because trim of the body is empty).
132        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
133        $this->P->parse('a ` ` b');
134        $calls = [
135            ['document_start', []],
136            ['p_open', []],
137            ['cdata', ["\na "]],
138            ['monospace_open', []],
139            ['unformatted', [' ']],
140            ['monospace_close', []],
141            ['cdata', [' b']],
142            ['p_close', []],
143            ['document_end', []],
144        ];
145        $this->assertCalls($calls, $this->H->calls);
146    }
147
148    function testEmptyDelimiterDoesNotMatch()
149    {
150        // Two adjacent backticks with no matching pair later in the
151        // paragraph stay literal — the length-boundary guards reject them
152        // as an n=1 opener followed immediately by an n=1 closer.
153        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
154        $this->P->parse('foo `` bar');
155        $modes = array_column($this->H->calls, 0);
156        $this->assertNotContains('monospace_open', $modes,
157            'Bare adjacent backticks with no closer must stay literal');
158    }
159
160    function testN1BodyCanContainDoubleBacktickRun()
161    {
162        // GFM example 340. Input backtick-space-2xbacktick-space-backtick.
163        // The interior run of two is not a valid n=1 closer, so it lives
164        // in the body; edge-space stripping then leaves just the two
165        // backticks as the body content.
166        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
167        $this->P->parse('` `` `');
168        $calls = [
169            ['document_start', []],
170            ['p_open', []],
171            ['cdata', ["\n"]],
172            ['monospace_open', []],
173            ['unformatted', ['``']],
174            ['monospace_close', []],
175            ['cdata', ['']],
176            ['p_close', []],
177            ['document_end', []],
178        ];
179        $this->assertCalls($calls, $this->H->calls);
180    }
181
182    function testRunOfThreeBackticksIsNotAnN1Span()
183    {
184        // The length-boundary guards on the opener reject a backtick that
185        // is immediately followed by another one, so a run of three or
186        // more never opens an n=1 span. Triple-backtick fenced blocks
187        // are a separate mode's concern.
188        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
189        $this->P->parse('foo ```bar``` baz');
190        $modes = array_column($this->H->calls, 0);
191        $this->assertNotContains('monospace_open', $modes,
192            'A run of 3 backticks must not trigger an n=1 span');
193    }
194
195    function testDoesNotSpanParagraphBoundary()
196    {
197        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
198        $this->P->parse("This `has a\n\nnew paragraph`.");
199        $modes = array_column($this->H->calls, 0);
200        $this->assertNotContains('monospace_open', $modes,
201            'GfmBacktickSingle must not open when the closer is past a blank line');
202    }
203
204    function testAllowsSingleNewline()
205    {
206        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
207        $this->P->parse("`open\nclose`");
208        $modes = array_column($this->H->calls, 0);
209        $this->assertContains('monospace_open', $modes,
210            'GfmBacktickSingle must still match across a single newline');
211    }
212
213    function testContentIsLiteral()
214    {
215        // Other inline modes must not parse inside a code span.
216        $this->P->addMode('gfm_emphasis', new GfmEmphasis());
217        $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle());
218        $this->P->parse('`*foo*`');
219        $modes = array_column($this->H->calls, 0);
220        $this->assertNotContains('emphasis_open', $modes,
221            'Emphasis must not parse inside a code span');
222        $this->assertContains('monospace_open', $modes,
223            'Backtick span must emit monospace_open');
224
225        // The emphasized text stays as an unformatted (verbatim) call
226        // inside the span — same treatment as nowiki and %%.
227        $unformatted = array_filter($this->H->calls, static fn($c) => $c[0] === 'unformatted');
228        $bodies = array_map(static fn($c) => $c[1][0], $unformatted);
229        $this->assertContains('*foo*', $bodies,
230            'Raw *foo* must appear as verbatim unformatted content');
231    }
232
233    function testSortValue()
234    {
235        $mode = new GfmBacktickSingle();
236        $this->assertSame(165, $mode->getSort());
237    }
238}
239