xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmQuoteTest.php (revision 13a62f810fbd091d15ab734b467eaec0a6bf829a)
1309a0852SAndreas Gohr<?php
2309a0852SAndreas Gohr
3309a0852SAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode;
4309a0852SAndreas Gohr
5309a0852SAndreas Gohruse dokuwiki\Parsing\ModeRegistry;
6309a0852SAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmQuote;
7309a0852SAndreas Gohr
8309a0852SAndreas Gohr/**
9309a0852SAndreas Gohr * Tests for GFM-style block quotes.
10309a0852SAndreas Gohr *
11309a0852SAndreas Gohr * GfmQuote is the unified blockquote implementation covering both DW and
12309a0852SAndreas Gohr * GFM dialects. The mode captures the entire quote via addSpecialPattern
13309a0852SAndreas Gohr * and sub-parses the stripped body, so the outer parser only needs
14309a0852SAndreas Gohr * gfm_quote attached; inline modes and block modes (lists, code blocks,
15309a0852SAndreas Gohr * nested quotes) are picked up by the sub-parser.
16309a0852SAndreas Gohr *
17309a0852SAndreas Gohr * Two rendering shapes are exercised. Under DW-preferred syntax, a
18309a0852SAndreas Gohr * post-pass flattens the sub-parser's paragraph wrapping into linebreak-
19309a0852SAndreas Gohr * separated cdata so existing DW pages keep their `<br/>`-between-lines
20309a0852SAndreas Gohr * rendering. Under MD-preferred syntax the sub-parser's paragraph
21309a0852SAndreas Gohr * wrapping survives — a quote with one paragraph emits
22309a0852SAndreas Gohr * `<blockquote><p>...</p></blockquote>`.
23309a0852SAndreas Gohr */
24309a0852SAndreas Gohrclass GfmQuoteTest extends ParserTestBase
25309a0852SAndreas Gohr{
26309a0852SAndreas Gohr    public function tearDown(): void
27309a0852SAndreas Gohr    {
28309a0852SAndreas Gohr        ModeRegistry::reset();
29309a0852SAndreas Gohr        parent::tearDown();
30309a0852SAndreas Gohr    }
31309a0852SAndreas Gohr
32309a0852SAndreas Gohr    private function setSyntax(string $syntax): void
33309a0852SAndreas Gohr    {
34309a0852SAndreas Gohr        global $conf;
35309a0852SAndreas Gohr        $conf['syntax'] = $syntax;
36309a0852SAndreas Gohr        ModeRegistry::reset();
37309a0852SAndreas Gohr    }
38309a0852SAndreas Gohr
39309a0852SAndreas Gohr    /**
40309a0852SAndreas Gohr     * Recursively flatten call lists, descending into `nest` content.
41309a0852SAndreas Gohr     * Useful for tests that just check whether an instruction appears
42309a0852SAndreas Gohr     * somewhere in the rendered output regardless of nesting depth.
43309a0852SAndreas Gohr     */
44309a0852SAndreas Gohr    private function flatNames(array $calls): array
45309a0852SAndreas Gohr    {
46309a0852SAndreas Gohr        $names = [];
47309a0852SAndreas Gohr        foreach ($calls as $call) {
48309a0852SAndreas Gohr            $names[] = $call[0];
49309a0852SAndreas Gohr            if ($call[0] === 'nest') {
50309a0852SAndreas Gohr                $names = array_merge($names, $this->flatNames($call[1][0]));
51309a0852SAndreas Gohr            }
52309a0852SAndreas Gohr        }
53309a0852SAndreas Gohr        return $names;
54309a0852SAndreas Gohr    }
55309a0852SAndreas Gohr
56309a0852SAndreas Gohr    public function testSortValue()
57309a0852SAndreas Gohr    {
58309a0852SAndreas Gohr        $mode = new GfmQuote();
59309a0852SAndreas Gohr        $this->assertSame(220, $mode->getSort());
60309a0852SAndreas Gohr    }
61309a0852SAndreas Gohr
62309a0852SAndreas Gohr    // ----- DW-preferred rendering: linebreak-separated, no <p> ------------
63309a0852SAndreas Gohr
64309a0852SAndreas Gohr    public function testDwSingleLine()
65309a0852SAndreas Gohr    {
66*13a62f81SAndreas Gohr        $this->setSyntax('dw');
67309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
68309a0852SAndreas Gohr        $this->P->parse("> foo\n");
69309a0852SAndreas Gohr
70309a0852SAndreas Gohr        $expected = [
71309a0852SAndreas Gohr            ['document_start', []],
72309a0852SAndreas Gohr            ['quote_open', []],
73309a0852SAndreas Gohr            ['nest', [[ ['cdata', ['foo']] ]]],
74309a0852SAndreas Gohr            ['quote_close', []],
75309a0852SAndreas Gohr            ['document_end', []],
76309a0852SAndreas Gohr        ];
77309a0852SAndreas Gohr        $this->assertCalls($expected, $this->H->calls);
78309a0852SAndreas Gohr    }
79309a0852SAndreas Gohr
80309a0852SAndreas Gohr    public function testDwSpaceAfterMarkerOptional()
81309a0852SAndreas Gohr    {
82309a0852SAndreas Gohr        // GFM allows omitting the space after `>`; DW always did. Strip
83309a0852SAndreas Gohr        // logic removes one optional space after the `>`, so `>foo` and
84309a0852SAndreas Gohr        // `> foo` both produce cdata "foo".
85*13a62f81SAndreas Gohr        $this->setSyntax('dw');
86309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
87309a0852SAndreas Gohr        $this->P->parse(">foo\n");
88309a0852SAndreas Gohr
89309a0852SAndreas Gohr        $names = $this->flatNames($this->H->calls);
90309a0852SAndreas Gohr        $this->assertContains('quote_open', $names);
91309a0852SAndreas Gohr        $this->assertContains('cdata', $names);
92309a0852SAndreas Gohr    }
93309a0852SAndreas Gohr
94309a0852SAndreas Gohr    public function testDwTwoLinesEmitLinebreak()
95309a0852SAndreas Gohr    {
96309a0852SAndreas Gohr        // The DW-preferred post-pass converts the sub-parser's paragraph
97309a0852SAndreas Gohr        // wrapping into a linebreak between the two cdata calls, matching
98309a0852SAndreas Gohr        // the historical `<blockquote>foo<br/>bar</blockquote>` shape.
99*13a62f81SAndreas Gohr        $this->setSyntax('dw');
100309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
101309a0852SAndreas Gohr        $this->P->parse("> foo\n> bar\n");
102309a0852SAndreas Gohr
103309a0852SAndreas Gohr        $expected = [
104309a0852SAndreas Gohr            ['document_start', []],
105309a0852SAndreas Gohr            ['quote_open', []],
106309a0852SAndreas Gohr            ['nest', [[
107309a0852SAndreas Gohr                ['cdata', ['foo']],
108309a0852SAndreas Gohr                ['linebreak', []],
109309a0852SAndreas Gohr                ['cdata', ['bar']],
110309a0852SAndreas Gohr            ]]],
111309a0852SAndreas Gohr            ['quote_close', []],
112309a0852SAndreas Gohr            ['document_end', []],
113309a0852SAndreas Gohr        ];
114309a0852SAndreas Gohr        $this->assertCalls($expected, $this->H->calls);
115309a0852SAndreas Gohr    }
116309a0852SAndreas Gohr
117309a0852SAndreas Gohr    public function testDwBlankMarkerLineEmitsTwoLinebreaks()
118309a0852SAndreas Gohr    {
119309a0852SAndreas Gohr        // `>` alone between content lines is a paragraph break in GFM.
120309a0852SAndreas Gohr        // The DW post-pass replaces each p_open and each p_close with a
121309a0852SAndreas Gohr        // linebreak, producing two adjacent linebreak calls between the
122309a0852SAndreas Gohr        // two content cdata — matches the historical DW two-`<br/>` shape.
123*13a62f81SAndreas Gohr        $this->setSyntax('dw');
124309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
125309a0852SAndreas Gohr        $this->P->parse("> foo\n>\n> bar\n");
126309a0852SAndreas Gohr
127309a0852SAndreas Gohr        $expected = [
128309a0852SAndreas Gohr            ['document_start', []],
129309a0852SAndreas Gohr            ['quote_open', []],
130309a0852SAndreas Gohr            ['nest', [[
131309a0852SAndreas Gohr                ['cdata', ['foo']],
132309a0852SAndreas Gohr                ['linebreak', []],
133309a0852SAndreas Gohr                ['linebreak', []],
134309a0852SAndreas Gohr                ['cdata', ['bar']],
135309a0852SAndreas Gohr            ]]],
136309a0852SAndreas Gohr            ['quote_close', []],
137309a0852SAndreas Gohr            ['document_end', []],
138309a0852SAndreas Gohr        ];
139309a0852SAndreas Gohr        $this->assertCalls($expected, $this->H->calls);
140309a0852SAndreas Gohr    }
141309a0852SAndreas Gohr
142309a0852SAndreas Gohr    public function testDwNested()
143309a0852SAndreas Gohr    {
144*13a62f81SAndreas Gohr        $this->setSyntax('dw');
145309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
146309a0852SAndreas Gohr        $this->P->parse("> > foo\n");
147309a0852SAndreas Gohr
148309a0852SAndreas Gohr        // The outer captures a single line `> > foo`. Stripping the
149309a0852SAndreas Gohr        // outer marker leaves `> foo`, which the sub-parser feeds back
150309a0852SAndreas Gohr        // through GfmQuote — recursion produces a nested quote_open /
151309a0852SAndreas Gohr        // quote_close pair carrying the cdata.
152309a0852SAndreas Gohr        $names = $this->flatNames($this->H->calls);
153309a0852SAndreas Gohr        $opens  = array_filter($names, static fn($n) => $n === 'quote_open');
154309a0852SAndreas Gohr        $closes = array_filter($names, static fn($n) => $n === 'quote_close');
155309a0852SAndreas Gohr        $this->assertCount(2, $opens, 'two levels of quote_open expected');
156309a0852SAndreas Gohr        $this->assertCount(2, $closes, 'two levels of quote_close expected');
157309a0852SAndreas Gohr    }
158309a0852SAndreas Gohr
159309a0852SAndreas Gohr    public function testDwNoLazyContinuation()
160309a0852SAndreas Gohr    {
161309a0852SAndreas Gohr        // GfmQuote does not implement lazy continuation: every quote
162309a0852SAndreas Gohr        // line must begin with `>`. `bar` without a `>` prefix terminates
163309a0852SAndreas Gohr        // the quote, so it ends up as a separate paragraph — matching
164309a0852SAndreas Gohr        // today's DW behavior.
165*13a62f81SAndreas Gohr        $this->setSyntax('dw');
166309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
167309a0852SAndreas Gohr        $this->P->parse("> foo\nbar\n");
168309a0852SAndreas Gohr
169309a0852SAndreas Gohr        $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_open');
170309a0852SAndreas Gohr        $this->assertCount(1, $opens, 'quote opens once and stops at the non-`>` line');
171309a0852SAndreas Gohr
172309a0852SAndreas Gohr        // `bar` is outside the quote — find a top-level cdata after the close
173309a0852SAndreas Gohr        $afterClose = false;
174309a0852SAndreas Gohr        $sawBarOutside = false;
175309a0852SAndreas Gohr        foreach ($this->H->calls as $call) {
176309a0852SAndreas Gohr            if ($call[0] === 'quote_close') $afterClose = true;
177309a0852SAndreas Gohr            if ($afterClose && $call[0] === 'cdata' && str_contains($call[1][0], 'bar')) {
178309a0852SAndreas Gohr                $sawBarOutside = true;
179309a0852SAndreas Gohr            }
180309a0852SAndreas Gohr        }
181309a0852SAndreas Gohr        $this->assertTrue($sawBarOutside, '`bar` must appear as cdata outside the quote');
182309a0852SAndreas Gohr    }
183309a0852SAndreas Gohr
184309a0852SAndreas Gohr    public function testDwBlankLineSeparatesQuotes()
185309a0852SAndreas Gohr    {
186309a0852SAndreas Gohr        // A truly blank line ends the quote. The next `>` starts a new
187309a0852SAndreas Gohr        // quote, producing two distinct quote_open / quote_close pairs.
188*13a62f81SAndreas Gohr        $this->setSyntax('dw');
189309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
190309a0852SAndreas Gohr        $this->P->parse("> foo\n\n> bar\n");
191309a0852SAndreas Gohr
192309a0852SAndreas Gohr        $opens = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_open');
193309a0852SAndreas Gohr        $closes = array_filter($this->H->calls, static fn($c) => $c[0] === 'quote_close');
194309a0852SAndreas Gohr        $this->assertCount(2, $opens, 'two distinct quote blocks');
195309a0852SAndreas Gohr        $this->assertCount(2, $closes);
196309a0852SAndreas Gohr    }
197309a0852SAndreas Gohr
198309a0852SAndreas Gohr    public function testDwHeaderInsideQuoteStaysCdata()
199309a0852SAndreas Gohr    {
200309a0852SAndreas Gohr        // Sub-parser excludes BASEONLY (Header / GfmHeader). Header
201309a0852SAndreas Gohr        // instructions drive section-edit anchors and TOC entries that
202309a0852SAndreas Gohr        // do not compose with `<blockquote>`. `# Foo` therefore stays
203309a0852SAndreas Gohr        // as plain cdata text.
204*13a62f81SAndreas Gohr        $this->setSyntax('dw');
205309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
206309a0852SAndreas Gohr        $this->P->parse("> # Foo\n");
207309a0852SAndreas Gohr
208309a0852SAndreas Gohr        $names = $this->flatNames($this->H->calls);
209309a0852SAndreas Gohr        $this->assertNotContains('header', $names);
210309a0852SAndreas Gohr        $this->assertNotContains('section_open', $names);
211309a0852SAndreas Gohr        $this->assertContains('cdata', $names);
212309a0852SAndreas Gohr    }
213309a0852SAndreas Gohr
214309a0852SAndreas Gohr    // ----- MD-preferred rendering: paragraph wrapping survives ------------
215309a0852SAndreas Gohr
216309a0852SAndreas Gohr    public function testMdSingleParagraph()
217309a0852SAndreas Gohr    {
218*13a62f81SAndreas Gohr        $this->setSyntax('md');
219309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
220309a0852SAndreas Gohr        $this->P->parse("> foo\n> bar\n");
221309a0852SAndreas Gohr
222309a0852SAndreas Gohr        // Sub-parser wraps the body in `p_open` / `p_close`. The outer
223309a0852SAndreas Gohr        // wraps them inside a `nest`, and Block treats the nest as
224309a0852SAndreas Gohr        // opaque. Two `>`-content lines join into one paragraph.
225309a0852SAndreas Gohr        $expected = [
226309a0852SAndreas Gohr            ['document_start', []],
227309a0852SAndreas Gohr            ['quote_open', []],
228309a0852SAndreas Gohr            ['nest', [[
229309a0852SAndreas Gohr                ['p_open', []],
230309a0852SAndreas Gohr                ['cdata', ["foo\nbar"]],
231309a0852SAndreas Gohr                ['p_close', []],
232309a0852SAndreas Gohr            ]]],
233309a0852SAndreas Gohr            ['quote_close', []],
234309a0852SAndreas Gohr            ['document_end', []],
235309a0852SAndreas Gohr        ];
236309a0852SAndreas Gohr        $this->assertCalls($expected, $this->H->calls);
237309a0852SAndreas Gohr    }
238309a0852SAndreas Gohr
239309a0852SAndreas Gohr    public function testMdMultiParagraph()
240309a0852SAndreas Gohr    {
241309a0852SAndreas Gohr        // `>` alone between content lines creates two paragraphs in one
242309a0852SAndreas Gohr        // blockquote — under MD-preferred the post-pass does not run, so
243309a0852SAndreas Gohr        // the sub-parser's `p_open` / `p_close` pairs survive intact.
244*13a62f81SAndreas Gohr        $this->setSyntax('md');
245309a0852SAndreas Gohr        $this->P->addMode('gfm_quote', new GfmQuote());
246309a0852SAndreas Gohr        $this->P->parse("> foo\n>\n> bar\n");
247309a0852SAndreas Gohr
248309a0852SAndreas Gohr        $names = $this->flatNames($this->H->calls);
249309a0852SAndreas Gohr        $pOpens = array_filter($names, static fn($n) => $n === 'p_open');
250309a0852SAndreas Gohr        $pCloses = array_filter($names, static fn($n) => $n === 'p_close');
251309a0852SAndreas Gohr        $this->assertCount(2, $pOpens, 'two paragraphs inside one blockquote');
252309a0852SAndreas Gohr        $this->assertCount(2, $pCloses);
253309a0852SAndreas Gohr    }
254309a0852SAndreas Gohr
255309a0852SAndreas Gohr    public function testMdListInsideQuote()
256309a0852SAndreas Gohr    {
257309a0852SAndreas Gohr        // GfmListblock is loaded under MD-preferred syntax, so a list
258309a0852SAndreas Gohr        // inside a quote parses as a real list. The sub-parser's list
259309a0852SAndreas Gohr        // calls land inside the outer `nest` wrapper.
260*13a62f81SAndreas Gohr        $this->setSyntax('md');
261309a0852SAndreas Gohr        ModeRegistry::reset();
262309a0852SAndreas Gohr        // Add the registry's full mode set so gfm_listblock is reachable
263309a0852SAndreas Gohr        // via the sub-parser (the sub-parser uses ModeRegistry::getModes,
264309a0852SAndreas Gohr        // which honors $conf['syntax']).
265309a0852SAndreas Gohr        foreach (ModeRegistry::getInstance()->getModes() as $m) {
266309a0852SAndreas Gohr            $this->P->addMode($m['mode'], $m['obj']);
267309a0852SAndreas Gohr        }
268309a0852SAndreas Gohr
269309a0852SAndreas Gohr        $this->P->parse("> - foo\n> - bar\n");
270309a0852SAndreas Gohr
271309a0852SAndreas Gohr        $names = $this->flatNames($this->H->calls);
272309a0852SAndreas Gohr        $this->assertContains('quote_open', $names);
273309a0852SAndreas Gohr        $this->assertContains('listu_open', $names, 'list inside quote must parse');
274309a0852SAndreas Gohr        $this->assertContains('listu_close', $names);
275309a0852SAndreas Gohr        $this->assertContains('quote_close', $names);
276309a0852SAndreas Gohr    }
277309a0852SAndreas Gohr}
278