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