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