1*b1c59bedSAndreas Gohr<?php 2*b1c59bedSAndreas Gohr 3*b1c59bedSAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode; 4*b1c59bedSAndreas Gohr 5*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ModeRegistry; 6*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ParserMode\Eol; 7*b1c59bedSAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmCode; 8*b1c59bedSAndreas Gohr 9*b1c59bedSAndreas Gohr/** 10*b1c59bedSAndreas Gohr * Tests for GFM backtick-fenced code blocks (`GfmCode`). 11*b1c59bedSAndreas Gohr */ 12*b1c59bedSAndreas Gohrclass GfmCodeTest extends ParserTestBase 13*b1c59bedSAndreas Gohr{ 14*b1c59bedSAndreas Gohr public function setUp(): void 15*b1c59bedSAndreas Gohr { 16*b1c59bedSAndreas Gohr parent::setUp(); 17*b1c59bedSAndreas Gohr global $conf; 18*b1c59bedSAndreas Gohr $conf['syntax'] = 'markdown'; 19*b1c59bedSAndreas Gohr ModeRegistry::reset(); 20*b1c59bedSAndreas Gohr } 21*b1c59bedSAndreas Gohr 22*b1c59bedSAndreas Gohr public function tearDown(): void 23*b1c59bedSAndreas Gohr { 24*b1c59bedSAndreas Gohr ModeRegistry::reset(); 25*b1c59bedSAndreas Gohr parent::tearDown(); 26*b1c59bedSAndreas Gohr } 27*b1c59bedSAndreas Gohr 28*b1c59bedSAndreas Gohr /** 29*b1c59bedSAndreas Gohr * Register the mode plus Eol. Order matters: the ParallelRegex 30*b1c59bedSAndreas Gohr * alternates patterns in insertion order and leftmost-match picks the 31*b1c59bedSAndreas Gohr * first alternative, so the block mode must be added before Eol 32*b1c59bedSAndreas Gohr * (same effect ModeRegistry achieves in production via sort values). 33*b1c59bedSAndreas Gohr */ 34*b1c59bedSAndreas Gohr private function addModes(): void 35*b1c59bedSAndreas Gohr { 36*b1c59bedSAndreas Gohr $this->P->addMode('gfm_code', new GfmCode()); 37*b1c59bedSAndreas Gohr $this->P->addMode('eol', new Eol()); 38*b1c59bedSAndreas Gohr } 39*b1c59bedSAndreas Gohr 40*b1c59bedSAndreas Gohr function testBasicBacktickFence() 41*b1c59bedSAndreas Gohr { 42*b1c59bedSAndreas Gohr $this->addModes(); 43*b1c59bedSAndreas Gohr $this->P->parse("```\nhello\n```"); 44*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 45*b1c59bedSAndreas Gohr $this->H->calls, 46*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 47*b1c59bedSAndreas Gohr )); 48*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 49*b1c59bedSAndreas Gohr $this->assertSame("hello\n", $codeCalls[0][1][0]); 50*b1c59bedSAndreas Gohr $this->assertNull($codeCalls[0][1][1]); 51*b1c59bedSAndreas Gohr $this->assertNull($codeCalls[0][1][2]); 52*b1c59bedSAndreas Gohr } 53*b1c59bedSAndreas Gohr 54*b1c59bedSAndreas Gohr function testLanguageFromInfoString() 55*b1c59bedSAndreas Gohr { 56*b1c59bedSAndreas Gohr $this->addModes(); 57*b1c59bedSAndreas Gohr $this->P->parse("```ruby\nx\n```"); 58*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 59*b1c59bedSAndreas Gohr $this->H->calls, 60*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 61*b1c59bedSAndreas Gohr )); 62*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 63*b1c59bedSAndreas Gohr $this->assertSame("x\n", $codeCalls[0][1][0]); 64*b1c59bedSAndreas Gohr $this->assertSame('ruby', $codeCalls[0][1][1]); 65*b1c59bedSAndreas Gohr } 66*b1c59bedSAndreas Gohr 67*b1c59bedSAndreas Gohr function testLanguageIsFirstWord() 68*b1c59bedSAndreas Gohr { 69*b1c59bedSAndreas Gohr // GFM spec example 113: only the first token of the info string 70*b1c59bedSAndreas Gohr // is treated as a language; extra junk is dropped. 71*b1c59bedSAndreas Gohr $this->addModes(); 72*b1c59bedSAndreas Gohr $this->P->parse("```ruby startline=3 \$%@#\$\nx\n```"); 73*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 74*b1c59bedSAndreas Gohr $this->H->calls, 75*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 76*b1c59bedSAndreas Gohr )); 77*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 78*b1c59bedSAndreas Gohr $this->assertSame('ruby', $codeCalls[0][1][1]); 79*b1c59bedSAndreas Gohr } 80*b1c59bedSAndreas Gohr 81*b1c59bedSAndreas Gohr function testBacktickInfoRejectsBackticks() 82*b1c59bedSAndreas Gohr { 83*b1c59bedSAndreas Gohr // GFM spec example 115: a backtick run with backticks in its 84*b1c59bedSAndreas Gohr // info string is NOT a fence — stays for inline code parsing. 85*b1c59bedSAndreas Gohr $this->addModes(); 86*b1c59bedSAndreas Gohr $this->P->parse("``` aa ```\nfoo"); 87*b1c59bedSAndreas Gohr $modes = array_column($this->H->calls, 0); 88*b1c59bedSAndreas Gohr $this->assertNotContains('code', $modes, 89*b1c59bedSAndreas Gohr 'Backtick fence must reject backticks in info string'); 90*b1c59bedSAndreas Gohr } 91*b1c59bedSAndreas Gohr 92*b1c59bedSAndreas Gohr function testLongerCloseFenceIsValid() 93*b1c59bedSAndreas Gohr { 94*b1c59bedSAndreas Gohr // Opener 3, closer 5 — valid because closer is ≥ opener. 95*b1c59bedSAndreas Gohr $this->addModes(); 96*b1c59bedSAndreas Gohr $this->P->parse("```\naaa\n`````"); 97*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 98*b1c59bedSAndreas Gohr $this->H->calls, 99*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 100*b1c59bedSAndreas Gohr )); 101*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 102*b1c59bedSAndreas Gohr $this->assertSame("aaa\n", $codeCalls[0][1][0]); 103*b1c59bedSAndreas Gohr } 104*b1c59bedSAndreas Gohr 105*b1c59bedSAndreas Gohr function testIndentedFenceIsNotFence() 106*b1c59bedSAndreas Gohr { 107*b1c59bedSAndreas Gohr // Column-0-only policy: any leading space rejects the fence. 108*b1c59bedSAndreas Gohr $this->addModes(); 109*b1c59bedSAndreas Gohr $this->P->parse(" ```\nx\n ```"); 110*b1c59bedSAndreas Gohr $modes = array_column($this->H->calls, 0); 111*b1c59bedSAndreas Gohr $this->assertNotContains('code', $modes, 112*b1c59bedSAndreas Gohr 'Fence must start at column 0; indent is out of scope'); 113*b1c59bedSAndreas Gohr } 114*b1c59bedSAndreas Gohr 115*b1c59bedSAndreas Gohr function testUnclosedFenceStaysLiteral() 116*b1c59bedSAndreas Gohr { 117*b1c59bedSAndreas Gohr // An unclosed fence must not emit a code call — the ``` stays as 118*b1c59bedSAndreas Gohr // paragraph text. Diverges from strict GFM (which would consume 119*b1c59bedSAndreas Gohr // to EOF); see class docblock for the rationale. 120*b1c59bedSAndreas Gohr $this->addModes(); 121*b1c59bedSAndreas Gohr $this->P->parse("```\nabc\ndef"); 122*b1c59bedSAndreas Gohr $modes = array_column($this->H->calls, 0); 123*b1c59bedSAndreas Gohr $this->assertNotContains('code', $modes, 124*b1c59bedSAndreas Gohr 'Unclosed fences must stay literal, not emit code'); 125*b1c59bedSAndreas Gohr } 126*b1c59bedSAndreas Gohr 127*b1c59bedSAndreas Gohr function testEmptyBody() 128*b1c59bedSAndreas Gohr { 129*b1c59bedSAndreas Gohr $this->addModes(); 130*b1c59bedSAndreas Gohr $this->P->parse("```\n```"); 131*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 132*b1c59bedSAndreas Gohr $this->H->calls, 133*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 134*b1c59bedSAndreas Gohr )); 135*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 136*b1c59bedSAndreas Gohr $this->assertSame('', $codeCalls[0][1][0]); 137*b1c59bedSAndreas Gohr } 138*b1c59bedSAndreas Gohr 139*b1c59bedSAndreas Gohr function testCloseWithTrailingSpaces() 140*b1c59bedSAndreas Gohr { 141*b1c59bedSAndreas Gohr $this->addModes(); 142*b1c59bedSAndreas Gohr $this->P->parse("```\nx\n``` "); 143*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 144*b1c59bedSAndreas Gohr $this->H->calls, 145*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 146*b1c59bedSAndreas Gohr )); 147*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 148*b1c59bedSAndreas Gohr $this->assertSame("x\n", $codeCalls[0][1][0]); 149*b1c59bedSAndreas Gohr } 150*b1c59bedSAndreas Gohr 151*b1c59bedSAndreas Gohr function testCloseWithTrailingTabs() 152*b1c59bedSAndreas Gohr { 153*b1c59bedSAndreas Gohr $this->addModes(); 154*b1c59bedSAndreas Gohr $this->P->parse("```\nx\n```\t\t"); 155*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 156*b1c59bedSAndreas Gohr $this->H->calls, 157*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 158*b1c59bedSAndreas Gohr )); 159*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 160*b1c59bedSAndreas Gohr $this->assertSame("x\n", $codeCalls[0][1][0]); 161*b1c59bedSAndreas Gohr } 162*b1c59bedSAndreas Gohr 163*b1c59bedSAndreas Gohr function testFenceInterruptsParagraph() 164*b1c59bedSAndreas Gohr { 165*b1c59bedSAndreas Gohr // GFM spec example 110: a fence doesn't need a blank line before 166*b1c59bedSAndreas Gohr // it; the `code` instruction is block-level and paragraphs break. 167*b1c59bedSAndreas Gohr $this->addModes(); 168*b1c59bedSAndreas Gohr $this->P->parse("foo\n```\nbar\n```\nbaz"); 169*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 170*b1c59bedSAndreas Gohr $this->H->calls, 171*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 172*b1c59bedSAndreas Gohr )); 173*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 174*b1c59bedSAndreas Gohr $this->assertSame("bar\n", $codeCalls[0][1][0]); 175*b1c59bedSAndreas Gohr } 176*b1c59bedSAndreas Gohr 177*b1c59bedSAndreas Gohr function testEmptyInfoStringMeansNullLanguage() 178*b1c59bedSAndreas Gohr { 179*b1c59bedSAndreas Gohr $this->addModes(); 180*b1c59bedSAndreas Gohr $this->P->parse("```\nx\n```"); 181*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 182*b1c59bedSAndreas Gohr $this->H->calls, 183*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 184*b1c59bedSAndreas Gohr )); 185*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 186*b1c59bedSAndreas Gohr $this->assertNull($codeCalls[0][1][1]); 187*b1c59bedSAndreas Gohr } 188*b1c59bedSAndreas Gohr 189*b1c59bedSAndreas Gohr function testInfoStringSpecialChar() 190*b1c59bedSAndreas Gohr { 191*b1c59bedSAndreas Gohr // GFM spec example 114: a semicolon is a valid language token. 192*b1c59bedSAndreas Gohr $this->addModes(); 193*b1c59bedSAndreas Gohr $this->P->parse("```;\n```"); 194*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 195*b1c59bedSAndreas Gohr $this->H->calls, 196*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 197*b1c59bedSAndreas Gohr )); 198*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 199*b1c59bedSAndreas Gohr $this->assertSame(';', $codeCalls[0][1][1]); 200*b1c59bedSAndreas Gohr } 201*b1c59bedSAndreas Gohr 202*b1c59bedSAndreas Gohr function testTildeLineDoesNotCloseBacktickFence() 203*b1c59bedSAndreas Gohr { 204*b1c59bedSAndreas Gohr $this->addModes(); 205*b1c59bedSAndreas Gohr $this->P->parse("```\naaa\n~~~\nbbb\n```"); 206*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 207*b1c59bedSAndreas Gohr $this->H->calls, 208*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 209*b1c59bedSAndreas Gohr )); 210*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 211*b1c59bedSAndreas Gohr $this->assertSame("aaa\n~~~\nbbb\n", $codeCalls[0][1][0]); 212*b1c59bedSAndreas Gohr } 213*b1c59bedSAndreas Gohr 214*b1c59bedSAndreas Gohr function testFilenameAfterLanguage() 215*b1c59bedSAndreas Gohr { 216*b1c59bedSAndreas Gohr // DokuWiki's Code mode treats the second whitespace token as 217*b1c59bedSAndreas Gohr // the filename (turns the block into a download link). GfmCode 218*b1c59bedSAndreas Gohr // accepts the same vocabulary on the info string. 219*b1c59bedSAndreas Gohr $this->addModes(); 220*b1c59bedSAndreas Gohr $this->P->parse("```php myfile.php\n<?php\n```"); 221*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 222*b1c59bedSAndreas Gohr $this->H->calls, 223*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 224*b1c59bedSAndreas Gohr )); 225*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 226*b1c59bedSAndreas Gohr $this->assertSame('php', $codeCalls[0][1][1]); 227*b1c59bedSAndreas Gohr $this->assertSame('myfile.php', $codeCalls[0][1][2]); 228*b1c59bedSAndreas Gohr } 229*b1c59bedSAndreas Gohr 230*b1c59bedSAndreas Gohr function testHtmlAliasedToHtml4Strict() 231*b1c59bedSAndreas Gohr { 232*b1c59bedSAndreas Gohr // Same GeSHi alias DokuWiki's Code mode applies. 233*b1c59bedSAndreas Gohr $this->addModes(); 234*b1c59bedSAndreas Gohr $this->P->parse("```html\n<p>\n```"); 235*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 236*b1c59bedSAndreas Gohr $this->H->calls, 237*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 238*b1c59bedSAndreas Gohr )); 239*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 240*b1c59bedSAndreas Gohr $this->assertSame('html4strict', $codeCalls[0][1][1]); 241*b1c59bedSAndreas Gohr } 242*b1c59bedSAndreas Gohr 243*b1c59bedSAndreas Gohr function testDashMeansNoLanguage() 244*b1c59bedSAndreas Gohr { 245*b1c59bedSAndreas Gohr // DokuWiki uses `-` as an explicit "no language" marker; lets 246*b1c59bedSAndreas Gohr // a filename follow without a language argument first. 247*b1c59bedSAndreas Gohr $this->addModes(); 248*b1c59bedSAndreas Gohr $this->P->parse("```- somefile.txt\nx\n```"); 249*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 250*b1c59bedSAndreas Gohr $this->H->calls, 251*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 252*b1c59bedSAndreas Gohr )); 253*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 254*b1c59bedSAndreas Gohr $this->assertNull($codeCalls[0][1][1]); 255*b1c59bedSAndreas Gohr $this->assertSame('somefile.txt', $codeCalls[0][1][2]); 256*b1c59bedSAndreas Gohr } 257*b1c59bedSAndreas Gohr 258*b1c59bedSAndreas Gohr function testHighlightOptions() 259*b1c59bedSAndreas Gohr { 260*b1c59bedSAndreas Gohr // DokuWiki uses space-separated keys inside `[...]`; comma 261*b1c59bedSAndreas Gohr // separators inside a value survive (as GeSHi line lists). 262*b1c59bedSAndreas Gohr $this->addModes(); 263*b1c59bedSAndreas Gohr $this->P->parse("```php [enable_line_numbers start_line_numbers_at=\"10\"]\nx\n```"); 264*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 265*b1c59bedSAndreas Gohr $this->H->calls, 266*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 267*b1c59bedSAndreas Gohr )); 268*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 269*b1c59bedSAndreas Gohr $this->assertSame('php', $codeCalls[0][1][1]); 270*b1c59bedSAndreas Gohr $this->assertNull($codeCalls[0][1][2]); 271*b1c59bedSAndreas Gohr $this->assertCount(4, $codeCalls[0][1]); 272*b1c59bedSAndreas Gohr $this->assertSame( 273*b1c59bedSAndreas Gohr ['enable_line_numbers' => true, 'start_line_numbers_at' => 10], 274*b1c59bedSAndreas Gohr $codeCalls[0][1][3] 275*b1c59bedSAndreas Gohr ); 276*b1c59bedSAndreas Gohr } 277*b1c59bedSAndreas Gohr 278*b1c59bedSAndreas Gohr function testFilenameAndOptions() 279*b1c59bedSAndreas Gohr { 280*b1c59bedSAndreas Gohr $this->addModes(); 281*b1c59bedSAndreas Gohr $this->P->parse("```php myfile.php [enable_line_numbers]\nx\n```"); 282*b1c59bedSAndreas Gohr $codeCalls = array_values(array_filter( 283*b1c59bedSAndreas Gohr $this->H->calls, 284*b1c59bedSAndreas Gohr static fn($c) => $c[0] === 'code' 285*b1c59bedSAndreas Gohr )); 286*b1c59bedSAndreas Gohr $this->assertCount(1, $codeCalls); 287*b1c59bedSAndreas Gohr $this->assertSame('php', $codeCalls[0][1][1]); 288*b1c59bedSAndreas Gohr $this->assertSame('myfile.php', $codeCalls[0][1][2]); 289*b1c59bedSAndreas Gohr $this->assertSame(['enable_line_numbers' => true], $codeCalls[0][1][3]); 290*b1c59bedSAndreas Gohr } 291*b1c59bedSAndreas Gohr 292*b1c59bedSAndreas Gohr function testSortValue() 293*b1c59bedSAndreas Gohr { 294*b1c59bedSAndreas Gohr $this->assertSame(200, (new GfmCode())->getSort()); 295*b1c59bedSAndreas Gohr } 296*b1c59bedSAndreas Gohr} 297