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