xref: /dokuwiki/_test/tests/Parsing/ParserMode/PreformattedTest.php (revision d9043e786a4529b7c55cfef4380c888b792fda4c)
1<?php
2
3namespace dokuwiki\test\Parsing\ParserMode;
4
5use dokuwiki\Parsing\ParserMode\Code;
6use dokuwiki\Parsing\ParserMode\Eol;
7use dokuwiki\Parsing\ParserMode\File;
8use dokuwiki\Parsing\ParserMode\GfmHr;
9use dokuwiki\Parsing\ParserMode\GfmListblock;
10use dokuwiki\Parsing\ParserMode\Header;
11use dokuwiki\Parsing\ParserMode\Listblock;
12use dokuwiki\Parsing\ParserMode\Preformatted;
13use dokuwiki\Parsing\ParserMode\Table;
14
15class PreformattedTest extends ParserTestBase
16{
17
18    function testFile() {
19        $this->P->addMode('file',new File());
20        $this->P->parse('Foo <file>testing</file> Bar');
21        $calls = [
22            ['document_start',[]],
23            ['p_open',[]],
24            ['cdata',["\n".'Foo ']],
25            ['p_close',[]],
26            ['file',['testing',null,null]],
27            ['p_open',[]],
28            ['cdata',['Bar']],
29            ['p_close',[]],
30            ['document_end',[]],
31        ];
32
33        $this->assertCalls($calls, $this->H->calls);
34    }
35
36    function testCode() {
37        $this->P->addMode('code',new Code());
38        $this->P->parse('Foo <code>testing</code> Bar');
39        $calls = [
40            ['document_start',[]],
41            ['p_open',[]],
42            ['cdata',["\n".'Foo ']],
43            ['p_close',[]],
44            ['code',['testing', null, null]],
45            ['p_open',[]],
46            ['cdata',['Bar']],
47            ['p_close',[]],
48            ['document_end',[]],
49        ];
50        $this->assertCalls($calls, $this->H->calls);
51    }
52
53    function testCodeWhitespace() {
54        $this->P->addMode('code',new Code());
55        $this->P->parse("Foo <code \n>testing</code> Bar");
56        $calls = [
57            ['document_start',[]],
58            ['p_open',[]],
59            ['cdata',["\n".'Foo ']],
60            ['p_close',[]],
61            ['code',['testing', null, null]],
62            ['p_open',[]],
63            ['cdata',['Bar']],
64            ['p_close',[]],
65            ['document_end',[]],
66        ];
67        $this->assertCalls($calls, $this->H->calls);
68    }
69
70    function testCodeLang() {
71        $this->P->addMode('code',new Code());
72        $this->P->parse("Foo <code php>testing</code> Bar");
73        $calls = [
74            ['document_start',[]],
75            ['p_open',[]],
76            ['cdata',["\n".'Foo ']],
77            ['p_close',[]],
78            ['code',['testing', 'php', null]],
79            ['p_open',[]],
80            ['cdata',['Bar']],
81            ['p_close',[]],
82            ['document_end',[]],
83        ];
84        $this->assertCalls($calls, $this->H->calls);
85    }
86
87    function testPreformatted() {
88        $this->P->addMode('preformatted',new Preformatted());
89        $this->P->parse("F  oo\n  x  \n    y  \nBar\n");
90        $calls = [
91            ['document_start',[]],
92            ['p_open',[]],
93            ['cdata',["\nF  oo"]],
94            ['p_close',[]],
95            ['preformatted',["x  \n  y  "]],
96            ['p_open',[]],
97            ['cdata',["\nBar"]],
98            ['p_close',[]],
99            ['document_end',[]],
100        ];
101        $this->assertCalls($calls, $this->H->calls);
102    }
103
104    function testPreformattedWinEOL() {
105        $this->P->addMode('preformatted',new Preformatted());
106        $this->P->parse("F  oo\r\n  x  \r\n    y  \r\nBar\r\n");
107        $calls = [
108            ['document_start',[]],
109            ['p_open',[]],
110            ['cdata',["\nF  oo"]],
111            ['p_close',[]],
112            ['preformatted',["x  \n  y  "]],
113            ['p_open',[]],
114            ['cdata',["\nBar"]],
115            ['p_close',[]],
116            ['document_end',[]],
117        ];
118        $this->assertCalls($calls, $this->H->calls);
119    }
120
121    function testPreformattedTab() {
122        $this->P->addMode('preformatted',new Preformatted());
123        $this->P->parse("F  oo\n\tx\t\n\t\ty\t\nBar\n");
124        $calls = [
125            ['document_start',[]],
126            ['p_open',[]],
127            ['cdata',["\nF  oo"]],
128            ['p_close',[]],
129            ['preformatted',["x\t\n\ty\t"]],
130            ['p_open',[]],
131            ['cdata',["\nBar"]],
132            ['p_close',[]],
133            ['document_end',[]],
134        ];
135        $this->assertCalls($calls, $this->H->calls);
136    }
137
138    function testPreformattedTabWinEOL() {
139        $this->P->addMode('preformatted',new Preformatted());
140        $this->P->parse("F  oo\r\n\tx\t\r\n\t\ty\t\r\nBar\r\n");
141        $calls = [
142            ['document_start',[]],
143            ['p_open',[]],
144            ['cdata',["\nF  oo"]],
145            ['p_close',[]],
146            ['preformatted',["x\t\n\ty\t"]],
147            ['p_open',[]],
148            ['cdata',["\nBar"]],
149            ['p_close',[]],
150            ['document_end',[]],
151        ];
152        $this->assertCalls($calls, $this->H->calls);
153    }
154
155    function testPreformattedList() {
156        // Listblock (sort 10) must be added before Preformatted (sort 20) so
157        // the resulting PCRE alternation matches the canonical mode order.
158        // PCRE picks the first alternative that matches at a given position,
159        // and an indented bullet line like "  - x" matches both modes at the
160        // same offset; Listblock has to come first to win the tie.
161        $this->P->addMode('listblock',new Listblock());
162        $this->P->addMode('preformatted',new Preformatted());
163        $this->P->parse("  - x \n  * y \nF  oo\n  x  \n    y  \n  -X\n  *Y\nBar\n");
164        $calls = [
165            ['document_start',[]],
166            ['listo_open',[]],
167            ['listitem_open',[1]],
168            ['listcontent_open',[]],
169            ['cdata',[" x "]],
170            ['listcontent_close',[]],
171            ['listitem_close',[]],
172            ['listo_close',[]],
173            ['listu_open',[]],
174            ['listitem_open',[1]],
175            ['listcontent_open',[]],
176            ['cdata',[" y "]],
177            ['listcontent_close',[]],
178            ['listitem_close',[]],
179            ['listu_close',[]],
180            ['p_open',[]],
181            ['cdata',["F  oo"]],
182            ['p_close',[]],
183            ['preformatted',["x  \n  y  \n-X\n*Y"]],
184            ['p_open',[]],
185            ['cdata',["\nBar"]],
186            ['p_close',[]],
187            ['document_end',[]],
188        ];
189        $this->assertCalls($calls, $this->H->calls);
190    }
191
192
193    function testIndentedListEndsTableNotSwallowedAsPreformatted() {
194        // The list-awareness lookahead on preformatted's entry patterns is
195        // what keeps an indented list line from being read as an indented code
196        // block. In base mode sort order already gives listblock the tie, but
197        // table mode accepts preformatted (a protected mode) without connecting
198        // listblock, so inside a table only the lookahead stops "\n  *" from
199        // entering preformatted and swallowing the list. Without it the list
200        // lines vanish and the table's section-edit range grows to cover them.
201        $this->P->addMode('table', new Table());
202        $this->P->addMode('preformatted', new Preformatted());
203        $this->P->addMode('listblock', new Listblock());
204        $this->P->parse("| a |\n  * one\n  * two\nafter\n");
205        $calls = [
206            ['document_start', []],
207            ['table_open', [1, 1, 1]],
208            ['tablerow_open', []],
209            ['tablecell_open', [1, null, 1]],
210            ['cdata', [" a "]],
211            ['tablecell_close', []],
212            ['tablerow_close', []],
213            ['table_close', [6]],
214            ['p_open', []],
215            ['cdata', ["* one"]],
216            ['p_close', []],
217            ['listu_open', []],
218            ['listitem_open', [1]],
219            ['listcontent_open', []],
220            ['cdata', [" two"]],
221            ['listcontent_close', []],
222            ['listitem_close', []],
223            ['listu_close', []],
224            ['p_open', []],
225            ['cdata', ["after"]],
226            ['p_close', []],
227            ['document_end', []],
228        ];
229        $this->assertCalls($calls, $this->H->calls);
230    }
231
232
233    function testIndentedListEndsTableInMixedMarkdownSyntax() {
234        // Same table/preformatted regression, but in a Markdown-preferred mixed
235        // syntax (md+dw): the DokuWiki table still exists, but GfmListblock owns
236        // lists (with +, ordered markers and tab indents) and is not connected
237        // inside the table either. The list-guard's marker set must follow the
238        // active list mode, so a tab-indented list still ends the table here
239        // instead of being swallowed as an indented code block.
240        $this->setSyntax('md+dw');
241        $this->P->addMode('table', new Table());
242        $this->P->addMode('preformatted', new Preformatted());
243        $this->P->addMode('gfm_listblock', new GfmListblock());
244        $this->P->parse("| a |\n\t* one\n\t* two\nafter\n");
245        $calls = [
246            ['document_start', []],
247            ['table_open', [1, 1, 1]],
248            ['tablerow_open', []],
249            ['tablecell_open', [1, null, 1]],
250            ['cdata', [" a "]],
251            ['tablecell_close', []],
252            ['tablerow_close', []],
253            ['table_close', [6]],
254            ['p_open', []],
255            ['cdata', ["* one"]],
256            ['p_close', []],
257            ['listu_open', []],
258            ['listitem_open', [1]],
259            ['listcontent_open', []],
260            ['nest', [[['cdata', ["two"]]]]],
261            ['listcontent_close', []],
262            ['listitem_close', []],
263            ['listu_close', []],
264            ['p_open', []],
265            ['cdata', ["\nafter"]],
266            ['p_close', []],
267            ['document_end', []],
268        ];
269        $this->assertCalls($calls, $this->H->calls);
270    }
271
272
273    function testMarkdownPreferredUsesFourSpaces() {
274        // In `md` and `md+dw` settings the indent threshold is 4,
275        // matching GFM's indented code block rule. Lines with only 2-3
276        // leading spaces stay as paragraph text.
277        $this->setSyntax('md');
278        $this->P->addMode('preformatted', new Preformatted());
279        $this->P->parse("F  oo\n    x  \n      y  \nBar\n");
280        $calls = [
281            ['document_start', []],
282            ['p_open', []],
283            ['cdata', ["\nF  oo"]],
284            ['p_close', []],
285            ['preformatted', ["x  \n  y  "]],
286            ['p_open', []],
287            ['cdata', ["\nBar"]],
288            ['p_close', []],
289            ['document_end', []],
290        ];
291        $this->assertCalls($calls, $this->H->calls);
292    }
293
294    function testMarkdownPreferredRejectsTwoSpaces() {
295        // 2-space indent in MD-preferred mode does NOT trigger preformatted.
296        $this->setSyntax('md');
297        $this->P->addMode('preformatted', new Preformatted());
298        $this->P->parse("F  oo\n  x\nBar\n");
299        $modes = array_column($this->H->calls, 0);
300        $this->assertNotContains('preformatted', $modes,
301            '2-space indent must not trigger preformatted when Markdown is preferred');
302    }
303
304    function testMarkdownPreferredTabStillTriggers() {
305        // Tab is a trigger regardless of the space threshold.
306        $this->setSyntax('md');
307        $this->P->addMode('preformatted', new Preformatted());
308        $this->P->parse("F  oo\n\tx\nBar\n");
309        $modes = array_column($this->H->calls, 0);
310        $this->assertContains('preformatted', $modes,
311            'A single tab must still trigger preformatted in MD-preferred mode');
312    }
313
314    function testStripsLeadingAndTrailingBlankIndentedLines() {
315        // GFM example #87: leading and trailing blank-but-indented lines
316        // should not appear in the preformatted body. The lexer's
317        // continuation pattern eats their indents, leaving padding `\n`
318        // runs in the rewriter buffer; the rewriter trims them so the
319        // emitted text starts and ends on a non-blank line.
320        $this->setSyntax('md');
321        $this->P->addMode('preformatted', new Preformatted());
322        $this->P->parse("\n    \n    foo\n    \n\n");
323        $calls = [
324            ['document_start', []],
325            ['preformatted', ['foo']],
326            ['document_end', []],
327        ];
328        $this->assertCalls($calls, $this->H->calls);
329    }
330
331    function testWhitespaceOnlyBlockIsSkipped() {
332        // A run of only blank-but-indented lines must not emit a
333        // preformatted call at all - the body would be pure whitespace
334        // and visually meaningless.
335        $this->setSyntax('md');
336        $this->P->addMode('preformatted', new Preformatted());
337        $this->P->parse("\n    \n    \n\n");
338        $modes = array_column($this->H->calls, 0);
339        $this->assertNotContains('preformatted', $modes);
340    }
341
342    function testBlankIndentedLineKeepsFollowingContent() {
343        // A "blank" line that is empty after its indent (classic trailing
344        // whitespace) followed by a column-0 line used to make the
345        // zero-width preformatted exit fire with nothing consumed, tripping
346        // the lexer's no-advance guard and silently dropping the rest of the
347        // document. The block is whitespace-only so it emits no preformatted
348        // call; the following paragraph must survive.
349        $this->P->addMode('preformatted',new Preformatted());
350        $this->P->parse("abc\n  \nmore\n");
351        $calls = [
352            ['document_start',[]],
353            ['p_open',[]],
354            ['cdata',["\nabc"]],
355            ['p_close',[]],
356            ['p_open',[]],
357            ['cdata',["\nmore"]],
358            ['p_close',[]],
359            ['document_end',[]],
360        ];
361        $this->assertCalls($calls, $this->H->calls);
362    }
363
364    function testBlankIndentedLineAfterCodeKeepsFollowingContent() {
365        // Same guard, but the blank indented line is a continuation line of
366        // a non-empty preformatted block: the block renders and the trailing
367        // paragraph must still survive.
368        $this->P->addMode('preformatted',new Preformatted());
369        $this->P->parse("abc\n  code\n  \nmore\n");
370        $calls = [
371            ['document_start',[]],
372            ['p_open',[]],
373            ['cdata',["\nabc"]],
374            ['p_close',[]],
375            ['preformatted',['code']],
376            ['p_open',[]],
377            ['cdata',["\nmore"]],
378            ['p_close',[]],
379            ['document_end',[]],
380        ];
381        $this->assertCalls($calls, $this->H->calls);
382    }
383
384    function testBlankIndentedLineKeepsFollowingBlockBoundary() {
385        // The zero-width preformatted exit exists to leave the boundary \n in
386        // the stream so a following block mode can anchor on it (e.g. an <hr>
387        // right after an indented code block). That must keep working even
388        // when the exit fires with nothing consumed after a blank indented
389        // line: the hr fires and the trailing paragraph survives.
390        $this->P->addMode('preformatted',new Preformatted());
391        $this->P->addMode('gfm_hr',new GfmHr());
392        $this->P->parse("abc\n  \n----\nmore\n");
393        $calls = [
394            ['document_start',[]],
395            ['p_open',[]],
396            ['cdata',["\nabc"]],
397            ['p_close',[]],
398            ['hr',[]],
399            ['p_open',[]],
400            ['cdata',["\nmore"]],
401            ['p_close',[]],
402            ['document_end',[]],
403        ];
404        $this->assertCalls($calls, $this->H->calls);
405    }
406
407    function testPreformattedPlusHeaderAndEol() {
408        // Note that EOL must come after preformatted!
409        $this->P->addMode('preformatted',new Preformatted());
410        $this->P->addMode('header',new Header());
411        $this->P->addMode('eol',new Eol());
412        $this->P->parse("F  oo\n  ==Test==\n    y  \nBar\n");
413        $calls = [
414            ['document_start',[]],
415            ['p_open',[]],
416            ['cdata',["F  oo"]],
417            ['p_close',[]],
418            ['preformatted',["==Test==\n  y  "]],
419            ['p_open',[]],
420            ['cdata',['Bar']],
421            ['p_close',[]],
422            ['document_end',[]],
423        ];
424        $this->assertCalls($calls, $this->H->calls);
425    }
426}
427