xref: /dokuwiki/_test/tests/Parsing/ParserMode/PreformattedTest.php (revision ebaa848199c71ea8546897e44a6ac97ab143dedd)
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\Header;
10use dokuwiki\Parsing\ParserMode\Listblock;
11use dokuwiki\Parsing\ParserMode\Preformatted;
12
13class PreformattedTest extends ParserTestBase
14{
15
16    function testFile() {
17        $this->P->addMode('file',new File());
18        $this->P->parse('Foo <file>testing</file> Bar');
19        $calls = [
20            ['document_start',[]],
21            ['p_open',[]],
22            ['cdata',["\n".'Foo ']],
23            ['p_close',[]],
24            ['file',['testing',null,null]],
25            ['p_open',[]],
26            ['cdata',['Bar']],
27            ['p_close',[]],
28            ['document_end',[]],
29        ];
30
31        $this->assertCalls($calls, $this->H->calls);
32    }
33
34    function testCode() {
35        $this->P->addMode('code',new Code());
36        $this->P->parse('Foo <code>testing</code> Bar');
37        $calls = [
38            ['document_start',[]],
39            ['p_open',[]],
40            ['cdata',["\n".'Foo ']],
41            ['p_close',[]],
42            ['code',['testing', null, null]],
43            ['p_open',[]],
44            ['cdata',['Bar']],
45            ['p_close',[]],
46            ['document_end',[]],
47        ];
48        $this->assertCalls($calls, $this->H->calls);
49    }
50
51    function testCodeWhitespace() {
52        $this->P->addMode('code',new Code());
53        $this->P->parse("Foo <code \n>testing</code> Bar");
54        $calls = [
55            ['document_start',[]],
56            ['p_open',[]],
57            ['cdata',["\n".'Foo ']],
58            ['p_close',[]],
59            ['code',['testing', null, null]],
60            ['p_open',[]],
61            ['cdata',['Bar']],
62            ['p_close',[]],
63            ['document_end',[]],
64        ];
65        $this->assertCalls($calls, $this->H->calls);
66    }
67
68    function testCodeLang() {
69        $this->P->addMode('code',new Code());
70        $this->P->parse("Foo <code php>testing</code> Bar");
71        $calls = [
72            ['document_start',[]],
73            ['p_open',[]],
74            ['cdata',["\n".'Foo ']],
75            ['p_close',[]],
76            ['code',['testing', 'php', null]],
77            ['p_open',[]],
78            ['cdata',['Bar']],
79            ['p_close',[]],
80            ['document_end',[]],
81        ];
82        $this->assertCalls($calls, $this->H->calls);
83    }
84
85    function testPreformatted() {
86        $this->P->addMode('preformatted',new Preformatted());
87        $this->P->parse("F  oo\n  x  \n    y  \nBar\n");
88        $calls = [
89            ['document_start',[]],
90            ['p_open',[]],
91            ['cdata',["\nF  oo"]],
92            ['p_close',[]],
93            ['preformatted',["x  \n  y  "]],
94            ['p_open',[]],
95            ['cdata',["\nBar"]],
96            ['p_close',[]],
97            ['document_end',[]],
98        ];
99        $this->assertCalls($calls, $this->H->calls);
100    }
101
102    function testPreformattedWinEOL() {
103        $this->P->addMode('preformatted',new Preformatted());
104        $this->P->parse("F  oo\r\n  x  \r\n    y  \r\nBar\r\n");
105        $calls = [
106            ['document_start',[]],
107            ['p_open',[]],
108            ['cdata',["\nF  oo"]],
109            ['p_close',[]],
110            ['preformatted',["x  \n  y  "]],
111            ['p_open',[]],
112            ['cdata',["\nBar"]],
113            ['p_close',[]],
114            ['document_end',[]],
115        ];
116        $this->assertCalls($calls, $this->H->calls);
117    }
118
119    function testPreformattedTab() {
120        $this->P->addMode('preformatted',new Preformatted());
121        $this->P->parse("F  oo\n\tx\t\n\t\ty\t\nBar\n");
122        $calls = [
123            ['document_start',[]],
124            ['p_open',[]],
125            ['cdata',["\nF  oo"]],
126            ['p_close',[]],
127            ['preformatted',["x\t\n\ty\t"]],
128            ['p_open',[]],
129            ['cdata',["\nBar"]],
130            ['p_close',[]],
131            ['document_end',[]],
132        ];
133        $this->assertCalls($calls, $this->H->calls);
134    }
135
136    function testPreformattedTabWinEOL() {
137        $this->P->addMode('preformatted',new Preformatted());
138        $this->P->parse("F  oo\r\n\tx\t\r\n\t\ty\t\r\nBar\r\n");
139        $calls = [
140            ['document_start',[]],
141            ['p_open',[]],
142            ['cdata',["\nF  oo"]],
143            ['p_close',[]],
144            ['preformatted',["x\t\n\ty\t"]],
145            ['p_open',[]],
146            ['cdata',["\nBar"]],
147            ['p_close',[]],
148            ['document_end',[]],
149        ];
150        $this->assertCalls($calls, $this->H->calls);
151    }
152
153    function testPreformattedList() {
154        // Listblock (sort 10) must be added before Preformatted (sort 20) so
155        // the resulting PCRE alternation matches the canonical mode order.
156        // PCRE picks the first alternative that matches at a given position,
157        // and an indented bullet line like "  - x" matches both modes at the
158        // same offset; Listblock has to come first to win the tie.
159        $this->P->addMode('listblock',new Listblock());
160        $this->P->addMode('preformatted',new Preformatted());
161        $this->P->parse("  - x \n  * y \nF  oo\n  x  \n    y  \n  -X\n  *Y\nBar\n");
162        $calls = [
163            ['document_start',[]],
164            ['listo_open',[]],
165            ['listitem_open',[1]],
166            ['listcontent_open',[]],
167            ['cdata',[" x "]],
168            ['listcontent_close',[]],
169            ['listitem_close',[]],
170            ['listo_close',[]],
171            ['listu_open',[]],
172            ['listitem_open',[1]],
173            ['listcontent_open',[]],
174            ['cdata',[" y "]],
175            ['listcontent_close',[]],
176            ['listitem_close',[]],
177            ['listu_close',[]],
178            ['p_open',[]],
179            ['cdata',["F  oo"]],
180            ['p_close',[]],
181            ['preformatted',["x  \n  y  \n-X\n*Y"]],
182            ['p_open',[]],
183            ['cdata',["\nBar"]],
184            ['p_close',[]],
185            ['document_end',[]],
186        ];
187        $this->assertCalls($calls, $this->H->calls);
188    }
189
190
191    function testMarkdownPreferredUsesFourSpaces() {
192        // In `md` and `md+dw` settings the indent threshold is 4,
193        // matching GFM's indented code block rule. Lines with only 2-3
194        // leading spaces stay as paragraph text.
195        $this->setSyntax('md');
196        $this->P->addMode('preformatted', new Preformatted());
197        $this->P->parse("F  oo\n    x  \n      y  \nBar\n");
198        $calls = [
199            ['document_start', []],
200            ['p_open', []],
201            ['cdata', ["\nF  oo"]],
202            ['p_close', []],
203            ['preformatted', ["x  \n  y  "]],
204            ['p_open', []],
205            ['cdata', ["\nBar"]],
206            ['p_close', []],
207            ['document_end', []],
208        ];
209        $this->assertCalls($calls, $this->H->calls);
210    }
211
212    function testMarkdownPreferredRejectsTwoSpaces() {
213        // 2-space indent in MD-preferred mode does NOT trigger preformatted.
214        $this->setSyntax('md');
215        $this->P->addMode('preformatted', new Preformatted());
216        $this->P->parse("F  oo\n  x\nBar\n");
217        $modes = array_column($this->H->calls, 0);
218        $this->assertNotContains('preformatted', $modes,
219            '2-space indent must not trigger preformatted when Markdown is preferred');
220    }
221
222    function testMarkdownPreferredTabStillTriggers() {
223        // Tab is a trigger regardless of the space threshold.
224        $this->setSyntax('md');
225        $this->P->addMode('preformatted', new Preformatted());
226        $this->P->parse("F  oo\n\tx\nBar\n");
227        $modes = array_column($this->H->calls, 0);
228        $this->assertContains('preformatted', $modes,
229            'A single tab must still trigger preformatted in MD-preferred mode');
230    }
231
232    function testStripsLeadingAndTrailingBlankIndentedLines() {
233        // GFM example #87: leading and trailing blank-but-indented lines
234        // should not appear in the preformatted body. The lexer's
235        // continuation pattern eats their indents, leaving padding `\n`
236        // runs in the rewriter buffer; the rewriter trims them so the
237        // emitted text starts and ends on a non-blank line.
238        $this->setSyntax('md');
239        $this->P->addMode('preformatted', new Preformatted());
240        $this->P->parse("\n    \n    foo\n    \n\n");
241        $calls = [
242            ['document_start', []],
243            ['preformatted', ['foo']],
244            ['document_end', []],
245        ];
246        $this->assertCalls($calls, $this->H->calls);
247    }
248
249    function testWhitespaceOnlyBlockIsSkipped() {
250        // A run of only blank-but-indented lines must not emit a
251        // preformatted call at all - the body would be pure whitespace
252        // and visually meaningless.
253        $this->setSyntax('md');
254        $this->P->addMode('preformatted', new Preformatted());
255        $this->P->parse("\n    \n    \n\n");
256        $modes = array_column($this->H->calls, 0);
257        $this->assertNotContains('preformatted', $modes);
258    }
259
260    function testBlankIndentedLineKeepsFollowingContent() {
261        // A "blank" line that is empty after its indent (classic trailing
262        // whitespace) followed by a column-0 line used to make the
263        // zero-width preformatted exit fire with nothing consumed, tripping
264        // the lexer's no-advance guard and silently dropping the rest of the
265        // document. The block is whitespace-only so it emits no preformatted
266        // call; the following paragraph must survive.
267        $this->P->addMode('preformatted',new Preformatted());
268        $this->P->parse("abc\n  \nmore\n");
269        $calls = [
270            ['document_start',[]],
271            ['p_open',[]],
272            ['cdata',["\nabc"]],
273            ['p_close',[]],
274            ['p_open',[]],
275            ['cdata',["\nmore"]],
276            ['p_close',[]],
277            ['document_end',[]],
278        ];
279        $this->assertCalls($calls, $this->H->calls);
280    }
281
282    function testBlankIndentedLineAfterCodeKeepsFollowingContent() {
283        // Same guard, but the blank indented line is a continuation line of
284        // a non-empty preformatted block: the block renders and the trailing
285        // paragraph must still survive.
286        $this->P->addMode('preformatted',new Preformatted());
287        $this->P->parse("abc\n  code\n  \nmore\n");
288        $calls = [
289            ['document_start',[]],
290            ['p_open',[]],
291            ['cdata',["\nabc"]],
292            ['p_close',[]],
293            ['preformatted',['code']],
294            ['p_open',[]],
295            ['cdata',["\nmore"]],
296            ['p_close',[]],
297            ['document_end',[]],
298        ];
299        $this->assertCalls($calls, $this->H->calls);
300    }
301
302    function testBlankIndentedLineKeepsFollowingBlockBoundary() {
303        // The zero-width preformatted exit exists to leave the boundary \n in
304        // the stream so a following block mode can anchor on it (e.g. an <hr>
305        // right after an indented code block). That must keep working even
306        // when the exit fires with nothing consumed after a blank indented
307        // line: the hr fires and the trailing paragraph survives.
308        $this->P->addMode('preformatted',new Preformatted());
309        $this->P->addMode('gfm_hr',new GfmHr());
310        $this->P->parse("abc\n  \n----\nmore\n");
311        $calls = [
312            ['document_start',[]],
313            ['p_open',[]],
314            ['cdata',["\nabc"]],
315            ['p_close',[]],
316            ['hr',[]],
317            ['p_open',[]],
318            ['cdata',["\nmore"]],
319            ['p_close',[]],
320            ['document_end',[]],
321        ];
322        $this->assertCalls($calls, $this->H->calls);
323    }
324
325    function testPreformattedPlusHeaderAndEol() {
326        // Note that EOL must come after preformatted!
327        $this->P->addMode('preformatted',new Preformatted());
328        $this->P->addMode('header',new Header());
329        $this->P->addMode('eol',new Eol());
330        $this->P->parse("F  oo\n  ==Test==\n    y  \nBar\n");
331        $calls = [
332            ['document_start',[]],
333            ['p_open',[]],
334            ['cdata',["F  oo"]],
335            ['p_close',[]],
336            ['preformatted',["==Test==\n  y  "]],
337            ['p_open',[]],
338            ['cdata',['Bar']],
339            ['p_close',[]],
340            ['document_end',[]],
341        ];
342        $this->assertCalls($calls, $this->H->calls);
343    }
344}
345