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