xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmCodeTest.php (revision b1c59bed2e3645a1f5f11438cdbe7d1596f4a3a4)
1*b1c59bedSAndreas Gohr<?php
2*b1c59bedSAndreas Gohr
3*b1c59bedSAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode;
4*b1c59bedSAndreas Gohr
5*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ModeRegistry;
6*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ParserMode\Eol;
7*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmCode;
8*b1c59bedSAndreas Gohr
9*b1c59bedSAndreas Gohr/**
10*b1c59bedSAndreas Gohr * Tests for GFM backtick-fenced code blocks (`GfmCode`).
11*b1c59bedSAndreas Gohr */
12*b1c59bedSAndreas Gohrclass GfmCodeTest extends ParserTestBase
13*b1c59bedSAndreas Gohr{
14*b1c59bedSAndreas Gohr    public function setUp(): void
15*b1c59bedSAndreas Gohr    {
16*b1c59bedSAndreas Gohr        parent::setUp();
17*b1c59bedSAndreas Gohr        global $conf;
18*b1c59bedSAndreas Gohr        $conf['syntax'] = 'markdown';
19*b1c59bedSAndreas Gohr        ModeRegistry::reset();
20*b1c59bedSAndreas Gohr    }
21*b1c59bedSAndreas Gohr
22*b1c59bedSAndreas Gohr    public function tearDown(): void
23*b1c59bedSAndreas Gohr    {
24*b1c59bedSAndreas Gohr        ModeRegistry::reset();
25*b1c59bedSAndreas Gohr        parent::tearDown();
26*b1c59bedSAndreas Gohr    }
27*b1c59bedSAndreas Gohr
28*b1c59bedSAndreas Gohr    /**
29*b1c59bedSAndreas Gohr     * Register the mode plus Eol. Order matters: the ParallelRegex
30*b1c59bedSAndreas Gohr     * alternates patterns in insertion order and leftmost-match picks the
31*b1c59bedSAndreas Gohr     * first alternative, so the block mode must be added before Eol
32*b1c59bedSAndreas Gohr     * (same effect ModeRegistry achieves in production via sort values).
33*b1c59bedSAndreas Gohr     */
34*b1c59bedSAndreas Gohr    private function addModes(): void
35*b1c59bedSAndreas Gohr    {
36*b1c59bedSAndreas Gohr        $this->P->addMode('gfm_code', new GfmCode());
37*b1c59bedSAndreas Gohr        $this->P->addMode('eol', new Eol());
38*b1c59bedSAndreas Gohr    }
39*b1c59bedSAndreas Gohr
40*b1c59bedSAndreas Gohr    function testBasicBacktickFence()
41*b1c59bedSAndreas Gohr    {
42*b1c59bedSAndreas Gohr        $this->addModes();
43*b1c59bedSAndreas Gohr        $this->P->parse("```\nhello\n```");
44*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
45*b1c59bedSAndreas Gohr            $this->H->calls,
46*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
47*b1c59bedSAndreas Gohr        ));
48*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
49*b1c59bedSAndreas Gohr        $this->assertSame("hello\n", $codeCalls[0][1][0]);
50*b1c59bedSAndreas Gohr        $this->assertNull($codeCalls[0][1][1]);
51*b1c59bedSAndreas Gohr        $this->assertNull($codeCalls[0][1][2]);
52*b1c59bedSAndreas Gohr    }
53*b1c59bedSAndreas Gohr
54*b1c59bedSAndreas Gohr    function testLanguageFromInfoString()
55*b1c59bedSAndreas Gohr    {
56*b1c59bedSAndreas Gohr        $this->addModes();
57*b1c59bedSAndreas Gohr        $this->P->parse("```ruby\nx\n```");
58*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
59*b1c59bedSAndreas Gohr            $this->H->calls,
60*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
61*b1c59bedSAndreas Gohr        ));
62*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
63*b1c59bedSAndreas Gohr        $this->assertSame("x\n", $codeCalls[0][1][0]);
64*b1c59bedSAndreas Gohr        $this->assertSame('ruby', $codeCalls[0][1][1]);
65*b1c59bedSAndreas Gohr    }
66*b1c59bedSAndreas Gohr
67*b1c59bedSAndreas Gohr    function testLanguageIsFirstWord()
68*b1c59bedSAndreas Gohr    {
69*b1c59bedSAndreas Gohr        // GFM spec example 113: only the first token of the info string
70*b1c59bedSAndreas Gohr        // is treated as a language; extra junk is dropped.
71*b1c59bedSAndreas Gohr        $this->addModes();
72*b1c59bedSAndreas Gohr        $this->P->parse("```ruby startline=3 \$%@#\$\nx\n```");
73*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
74*b1c59bedSAndreas Gohr            $this->H->calls,
75*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
76*b1c59bedSAndreas Gohr        ));
77*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
78*b1c59bedSAndreas Gohr        $this->assertSame('ruby', $codeCalls[0][1][1]);
79*b1c59bedSAndreas Gohr    }
80*b1c59bedSAndreas Gohr
81*b1c59bedSAndreas Gohr    function testBacktickInfoRejectsBackticks()
82*b1c59bedSAndreas Gohr    {
83*b1c59bedSAndreas Gohr        // GFM spec example 115: a backtick run with backticks in its
84*b1c59bedSAndreas Gohr        // info string is NOT a fence — stays for inline code parsing.
85*b1c59bedSAndreas Gohr        $this->addModes();
86*b1c59bedSAndreas Gohr        $this->P->parse("``` aa ```\nfoo");
87*b1c59bedSAndreas Gohr        $modes = array_column($this->H->calls, 0);
88*b1c59bedSAndreas Gohr        $this->assertNotContains('code', $modes,
89*b1c59bedSAndreas Gohr            'Backtick fence must reject backticks in info string');
90*b1c59bedSAndreas Gohr    }
91*b1c59bedSAndreas Gohr
92*b1c59bedSAndreas Gohr    function testLongerCloseFenceIsValid()
93*b1c59bedSAndreas Gohr    {
94*b1c59bedSAndreas Gohr        // Opener 3, closer 5 — valid because closer is ≥ opener.
95*b1c59bedSAndreas Gohr        $this->addModes();
96*b1c59bedSAndreas Gohr        $this->P->parse("```\naaa\n`````");
97*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
98*b1c59bedSAndreas Gohr            $this->H->calls,
99*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
100*b1c59bedSAndreas Gohr        ));
101*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
102*b1c59bedSAndreas Gohr        $this->assertSame("aaa\n", $codeCalls[0][1][0]);
103*b1c59bedSAndreas Gohr    }
104*b1c59bedSAndreas Gohr
105*b1c59bedSAndreas Gohr    function testIndentedFenceIsNotFence()
106*b1c59bedSAndreas Gohr    {
107*b1c59bedSAndreas Gohr        // Column-0-only policy: any leading space rejects the fence.
108*b1c59bedSAndreas Gohr        $this->addModes();
109*b1c59bedSAndreas Gohr        $this->P->parse(" ```\nx\n ```");
110*b1c59bedSAndreas Gohr        $modes = array_column($this->H->calls, 0);
111*b1c59bedSAndreas Gohr        $this->assertNotContains('code', $modes,
112*b1c59bedSAndreas Gohr            'Fence must start at column 0; indent is out of scope');
113*b1c59bedSAndreas Gohr    }
114*b1c59bedSAndreas Gohr
115*b1c59bedSAndreas Gohr    function testUnclosedFenceStaysLiteral()
116*b1c59bedSAndreas Gohr    {
117*b1c59bedSAndreas Gohr        // An unclosed fence must not emit a code call — the ``` stays as
118*b1c59bedSAndreas Gohr        // paragraph text. Diverges from strict GFM (which would consume
119*b1c59bedSAndreas Gohr        // to EOF); see class docblock for the rationale.
120*b1c59bedSAndreas Gohr        $this->addModes();
121*b1c59bedSAndreas Gohr        $this->P->parse("```\nabc\ndef");
122*b1c59bedSAndreas Gohr        $modes = array_column($this->H->calls, 0);
123*b1c59bedSAndreas Gohr        $this->assertNotContains('code', $modes,
124*b1c59bedSAndreas Gohr            'Unclosed fences must stay literal, not emit code');
125*b1c59bedSAndreas Gohr    }
126*b1c59bedSAndreas Gohr
127*b1c59bedSAndreas Gohr    function testEmptyBody()
128*b1c59bedSAndreas Gohr    {
129*b1c59bedSAndreas Gohr        $this->addModes();
130*b1c59bedSAndreas Gohr        $this->P->parse("```\n```");
131*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
132*b1c59bedSAndreas Gohr            $this->H->calls,
133*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
134*b1c59bedSAndreas Gohr        ));
135*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
136*b1c59bedSAndreas Gohr        $this->assertSame('', $codeCalls[0][1][0]);
137*b1c59bedSAndreas Gohr    }
138*b1c59bedSAndreas Gohr
139*b1c59bedSAndreas Gohr    function testCloseWithTrailingSpaces()
140*b1c59bedSAndreas Gohr    {
141*b1c59bedSAndreas Gohr        $this->addModes();
142*b1c59bedSAndreas Gohr        $this->P->parse("```\nx\n```   ");
143*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
144*b1c59bedSAndreas Gohr            $this->H->calls,
145*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
146*b1c59bedSAndreas Gohr        ));
147*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
148*b1c59bedSAndreas Gohr        $this->assertSame("x\n", $codeCalls[0][1][0]);
149*b1c59bedSAndreas Gohr    }
150*b1c59bedSAndreas Gohr
151*b1c59bedSAndreas Gohr    function testCloseWithTrailingTabs()
152*b1c59bedSAndreas Gohr    {
153*b1c59bedSAndreas Gohr        $this->addModes();
154*b1c59bedSAndreas Gohr        $this->P->parse("```\nx\n```\t\t");
155*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
156*b1c59bedSAndreas Gohr            $this->H->calls,
157*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
158*b1c59bedSAndreas Gohr        ));
159*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
160*b1c59bedSAndreas Gohr        $this->assertSame("x\n", $codeCalls[0][1][0]);
161*b1c59bedSAndreas Gohr    }
162*b1c59bedSAndreas Gohr
163*b1c59bedSAndreas Gohr    function testFenceInterruptsParagraph()
164*b1c59bedSAndreas Gohr    {
165*b1c59bedSAndreas Gohr        // GFM spec example 110: a fence doesn't need a blank line before
166*b1c59bedSAndreas Gohr        // it; the `code` instruction is block-level and paragraphs break.
167*b1c59bedSAndreas Gohr        $this->addModes();
168*b1c59bedSAndreas Gohr        $this->P->parse("foo\n```\nbar\n```\nbaz");
169*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
170*b1c59bedSAndreas Gohr            $this->H->calls,
171*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
172*b1c59bedSAndreas Gohr        ));
173*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
174*b1c59bedSAndreas Gohr        $this->assertSame("bar\n", $codeCalls[0][1][0]);
175*b1c59bedSAndreas Gohr    }
176*b1c59bedSAndreas Gohr
177*b1c59bedSAndreas Gohr    function testEmptyInfoStringMeansNullLanguage()
178*b1c59bedSAndreas Gohr    {
179*b1c59bedSAndreas Gohr        $this->addModes();
180*b1c59bedSAndreas Gohr        $this->P->parse("```\nx\n```");
181*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
182*b1c59bedSAndreas Gohr            $this->H->calls,
183*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
184*b1c59bedSAndreas Gohr        ));
185*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
186*b1c59bedSAndreas Gohr        $this->assertNull($codeCalls[0][1][1]);
187*b1c59bedSAndreas Gohr    }
188*b1c59bedSAndreas Gohr
189*b1c59bedSAndreas Gohr    function testInfoStringSpecialChar()
190*b1c59bedSAndreas Gohr    {
191*b1c59bedSAndreas Gohr        // GFM spec example 114: a semicolon is a valid language token.
192*b1c59bedSAndreas Gohr        $this->addModes();
193*b1c59bedSAndreas Gohr        $this->P->parse("```;\n```");
194*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
195*b1c59bedSAndreas Gohr            $this->H->calls,
196*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
197*b1c59bedSAndreas Gohr        ));
198*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
199*b1c59bedSAndreas Gohr        $this->assertSame(';', $codeCalls[0][1][1]);
200*b1c59bedSAndreas Gohr    }
201*b1c59bedSAndreas Gohr
202*b1c59bedSAndreas Gohr    function testTildeLineDoesNotCloseBacktickFence()
203*b1c59bedSAndreas Gohr    {
204*b1c59bedSAndreas Gohr        $this->addModes();
205*b1c59bedSAndreas Gohr        $this->P->parse("```\naaa\n~~~\nbbb\n```");
206*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
207*b1c59bedSAndreas Gohr            $this->H->calls,
208*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
209*b1c59bedSAndreas Gohr        ));
210*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
211*b1c59bedSAndreas Gohr        $this->assertSame("aaa\n~~~\nbbb\n", $codeCalls[0][1][0]);
212*b1c59bedSAndreas Gohr    }
213*b1c59bedSAndreas Gohr
214*b1c59bedSAndreas Gohr    function testFilenameAfterLanguage()
215*b1c59bedSAndreas Gohr    {
216*b1c59bedSAndreas Gohr        // DokuWiki's Code mode treats the second whitespace token as
217*b1c59bedSAndreas Gohr        // the filename (turns the block into a download link). GfmCode
218*b1c59bedSAndreas Gohr        // accepts the same vocabulary on the info string.
219*b1c59bedSAndreas Gohr        $this->addModes();
220*b1c59bedSAndreas Gohr        $this->P->parse("```php myfile.php\n<?php\n```");
221*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
222*b1c59bedSAndreas Gohr            $this->H->calls,
223*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
224*b1c59bedSAndreas Gohr        ));
225*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
226*b1c59bedSAndreas Gohr        $this->assertSame('php', $codeCalls[0][1][1]);
227*b1c59bedSAndreas Gohr        $this->assertSame('myfile.php', $codeCalls[0][1][2]);
228*b1c59bedSAndreas Gohr    }
229*b1c59bedSAndreas Gohr
230*b1c59bedSAndreas Gohr    function testHtmlAliasedToHtml4Strict()
231*b1c59bedSAndreas Gohr    {
232*b1c59bedSAndreas Gohr        // Same GeSHi alias DokuWiki's Code mode applies.
233*b1c59bedSAndreas Gohr        $this->addModes();
234*b1c59bedSAndreas Gohr        $this->P->parse("```html\n<p>\n```");
235*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
236*b1c59bedSAndreas Gohr            $this->H->calls,
237*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
238*b1c59bedSAndreas Gohr        ));
239*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
240*b1c59bedSAndreas Gohr        $this->assertSame('html4strict', $codeCalls[0][1][1]);
241*b1c59bedSAndreas Gohr    }
242*b1c59bedSAndreas Gohr
243*b1c59bedSAndreas Gohr    function testDashMeansNoLanguage()
244*b1c59bedSAndreas Gohr    {
245*b1c59bedSAndreas Gohr        // DokuWiki uses `-` as an explicit "no language" marker; lets
246*b1c59bedSAndreas Gohr        // a filename follow without a language argument first.
247*b1c59bedSAndreas Gohr        $this->addModes();
248*b1c59bedSAndreas Gohr        $this->P->parse("```- somefile.txt\nx\n```");
249*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
250*b1c59bedSAndreas Gohr            $this->H->calls,
251*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
252*b1c59bedSAndreas Gohr        ));
253*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
254*b1c59bedSAndreas Gohr        $this->assertNull($codeCalls[0][1][1]);
255*b1c59bedSAndreas Gohr        $this->assertSame('somefile.txt', $codeCalls[0][1][2]);
256*b1c59bedSAndreas Gohr    }
257*b1c59bedSAndreas Gohr
258*b1c59bedSAndreas Gohr    function testHighlightOptions()
259*b1c59bedSAndreas Gohr    {
260*b1c59bedSAndreas Gohr        // DokuWiki uses space-separated keys inside `[...]`; comma
261*b1c59bedSAndreas Gohr        // separators inside a value survive (as GeSHi line lists).
262*b1c59bedSAndreas Gohr        $this->addModes();
263*b1c59bedSAndreas Gohr        $this->P->parse("```php [enable_line_numbers start_line_numbers_at=\"10\"]\nx\n```");
264*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
265*b1c59bedSAndreas Gohr            $this->H->calls,
266*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
267*b1c59bedSAndreas Gohr        ));
268*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
269*b1c59bedSAndreas Gohr        $this->assertSame('php', $codeCalls[0][1][1]);
270*b1c59bedSAndreas Gohr        $this->assertNull($codeCalls[0][1][2]);
271*b1c59bedSAndreas Gohr        $this->assertCount(4, $codeCalls[0][1]);
272*b1c59bedSAndreas Gohr        $this->assertSame(
273*b1c59bedSAndreas Gohr            ['enable_line_numbers' => true, 'start_line_numbers_at' => 10],
274*b1c59bedSAndreas Gohr            $codeCalls[0][1][3]
275*b1c59bedSAndreas Gohr        );
276*b1c59bedSAndreas Gohr    }
277*b1c59bedSAndreas Gohr
278*b1c59bedSAndreas Gohr    function testFilenameAndOptions()
279*b1c59bedSAndreas Gohr    {
280*b1c59bedSAndreas Gohr        $this->addModes();
281*b1c59bedSAndreas Gohr        $this->P->parse("```php myfile.php [enable_line_numbers]\nx\n```");
282*b1c59bedSAndreas Gohr        $codeCalls = array_values(array_filter(
283*b1c59bedSAndreas Gohr            $this->H->calls,
284*b1c59bedSAndreas Gohr            static fn($c) => $c[0] === 'code'
285*b1c59bedSAndreas Gohr        ));
286*b1c59bedSAndreas Gohr        $this->assertCount(1, $codeCalls);
287*b1c59bedSAndreas Gohr        $this->assertSame('php', $codeCalls[0][1][1]);
288*b1c59bedSAndreas Gohr        $this->assertSame('myfile.php', $codeCalls[0][1][2]);
289*b1c59bedSAndreas Gohr        $this->assertSame(['enable_line_numbers' => true], $codeCalls[0][1][3]);
290*b1c59bedSAndreas Gohr    }
291*b1c59bedSAndreas Gohr
292*b1c59bedSAndreas Gohr    function testSortValue()
293*b1c59bedSAndreas Gohr    {
294*b1c59bedSAndreas Gohr        $this->assertSame(200, (new GfmCode())->getSort());
295*b1c59bedSAndreas Gohr    }
296*b1c59bedSAndreas Gohr}
297