1*8ed75a23SAndreas Gohr<?php 2*8ed75a23SAndreas Gohr 3*8ed75a23SAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode; 4*8ed75a23SAndreas Gohr 5*8ed75a23SAndreas Gohruse dokuwiki\Parsing\ModeRegistry; 6*8ed75a23SAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmBacktickSingle; 7*8ed75a23SAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmEmphasis; 8*8ed75a23SAndreas Gohr 9*8ed75a23SAndreas Gohr/** 10*8ed75a23SAndreas Gohr * Tests for the GFM inline code-span mode — single-backtick spans. 11*8ed75a23SAndreas Gohr */ 12*8ed75a23SAndreas Gohrclass GfmBacktickSingleTest extends ParserTestBase 13*8ed75a23SAndreas Gohr{ 14*8ed75a23SAndreas Gohr public function setUp(): void 15*8ed75a23SAndreas Gohr { 16*8ed75a23SAndreas Gohr parent::setUp(); 17*8ed75a23SAndreas Gohr global $conf; 18*8ed75a23SAndreas Gohr $conf['syntax'] = 'markdown'; 19*8ed75a23SAndreas Gohr ModeRegistry::reset(); 20*8ed75a23SAndreas Gohr } 21*8ed75a23SAndreas Gohr 22*8ed75a23SAndreas Gohr public function tearDown(): void 23*8ed75a23SAndreas Gohr { 24*8ed75a23SAndreas Gohr ModeRegistry::reset(); 25*8ed75a23SAndreas Gohr parent::tearDown(); 26*8ed75a23SAndreas Gohr } 27*8ed75a23SAndreas Gohr 28*8ed75a23SAndreas Gohr function testBasicCodeSpan() 29*8ed75a23SAndreas Gohr { 30*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 31*8ed75a23SAndreas Gohr $this->P->parse('Foo `Bar` Baz'); 32*8ed75a23SAndreas Gohr $calls = [ 33*8ed75a23SAndreas Gohr ['document_start', []], 34*8ed75a23SAndreas Gohr ['p_open', []], 35*8ed75a23SAndreas Gohr ['cdata', ["\nFoo "]], 36*8ed75a23SAndreas Gohr ['monospace_open', []], 37*8ed75a23SAndreas Gohr ['unformatted', ['Bar']], 38*8ed75a23SAndreas Gohr ['monospace_close', []], 39*8ed75a23SAndreas Gohr ['cdata', [' Baz']], 40*8ed75a23SAndreas Gohr ['p_close', []], 41*8ed75a23SAndreas Gohr ['document_end', []], 42*8ed75a23SAndreas Gohr ]; 43*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 44*8ed75a23SAndreas Gohr } 45*8ed75a23SAndreas Gohr 46*8ed75a23SAndreas Gohr function testSingleCharacterBody() 47*8ed75a23SAndreas Gohr { 48*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 49*8ed75a23SAndreas Gohr $this->P->parse('foo `b` bar'); 50*8ed75a23SAndreas Gohr $calls = [ 51*8ed75a23SAndreas Gohr ['document_start', []], 52*8ed75a23SAndreas Gohr ['p_open', []], 53*8ed75a23SAndreas Gohr ['cdata', ["\nfoo "]], 54*8ed75a23SAndreas Gohr ['monospace_open', []], 55*8ed75a23SAndreas Gohr ['unformatted', ['b']], 56*8ed75a23SAndreas Gohr ['monospace_close', []], 57*8ed75a23SAndreas Gohr ['cdata', [' bar']], 58*8ed75a23SAndreas Gohr ['p_close', []], 59*8ed75a23SAndreas Gohr ['document_end', []], 60*8ed75a23SAndreas Gohr ]; 61*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 62*8ed75a23SAndreas Gohr } 63*8ed75a23SAndreas Gohr 64*8ed75a23SAndreas Gohr function testTwoSeparateSpansOnOneLine() 65*8ed75a23SAndreas Gohr { 66*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 67*8ed75a23SAndreas Gohr $this->P->parse('`one` and `two`'); 68*8ed75a23SAndreas Gohr $calls = [ 69*8ed75a23SAndreas Gohr ['document_start', []], 70*8ed75a23SAndreas Gohr ['p_open', []], 71*8ed75a23SAndreas Gohr ['cdata', ["\n"]], 72*8ed75a23SAndreas Gohr ['monospace_open', []], 73*8ed75a23SAndreas Gohr ['unformatted', ['one']], 74*8ed75a23SAndreas Gohr ['monospace_close', []], 75*8ed75a23SAndreas Gohr ['cdata', [' and ']], 76*8ed75a23SAndreas Gohr ['monospace_open', []], 77*8ed75a23SAndreas Gohr ['unformatted', ['two']], 78*8ed75a23SAndreas Gohr ['monospace_close', []], 79*8ed75a23SAndreas Gohr ['cdata', ['']], 80*8ed75a23SAndreas Gohr ['p_close', []], 81*8ed75a23SAndreas Gohr ['document_end', []], 82*8ed75a23SAndreas Gohr ]; 83*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 84*8ed75a23SAndreas Gohr } 85*8ed75a23SAndreas Gohr 86*8ed75a23SAndreas Gohr function testUnmatchedOpenerStaysLiteral() 87*8ed75a23SAndreas Gohr { 88*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 89*8ed75a23SAndreas Gohr $this->P->parse('foo `bar with no closer'); 90*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 91*8ed75a23SAndreas Gohr $this->assertNotContains('monospace_open', $modes, 92*8ed75a23SAndreas Gohr 'Unmatched opening backtick must stay literal'); 93*8ed75a23SAndreas Gohr } 94*8ed75a23SAndreas Gohr 95*8ed75a23SAndreas Gohr function testAsymmetricEdgeSpaceIsPreserved() 96*8ed75a23SAndreas Gohr { 97*8ed75a23SAndreas Gohr // GFM example 342. Input ` a` — a leading space but no trailing 98*8ed75a23SAndreas Gohr // space. Body stays as " a"; strip only fires when BOTH ends are 99*8ed75a23SAndreas Gohr // whitespace. 100*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 101*8ed75a23SAndreas Gohr $this->P->parse('` a`'); 102*8ed75a23SAndreas Gohr $calls = [ 103*8ed75a23SAndreas Gohr ['document_start', []], 104*8ed75a23SAndreas Gohr ['p_open', []], 105*8ed75a23SAndreas Gohr ['cdata', ["\n"]], 106*8ed75a23SAndreas Gohr ['monospace_open', []], 107*8ed75a23SAndreas Gohr ['unformatted', [' a']], 108*8ed75a23SAndreas Gohr ['monospace_close', []], 109*8ed75a23SAndreas Gohr ['cdata', ['']], 110*8ed75a23SAndreas Gohr ['p_close', []], 111*8ed75a23SAndreas Gohr ['document_end', []], 112*8ed75a23SAndreas Gohr ]; 113*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 114*8ed75a23SAndreas Gohr } 115*8ed75a23SAndreas Gohr 116*8ed75a23SAndreas Gohr function testSymmetricEdgeSpaceIsStripped() 117*8ed75a23SAndreas Gohr { 118*8ed75a23SAndreas Gohr // Body with whitespace on both sides and non-whitespace content 119*8ed75a23SAndreas Gohr // in the middle gets one space stripped from each end. Input 120*8ed75a23SAndreas Gohr // body is " foo "; after strip it becomes "foo". 121*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 122*8ed75a23SAndreas Gohr $this->P->parse('` foo `'); 123*8ed75a23SAndreas Gohr $calls = [ 124*8ed75a23SAndreas Gohr ['document_start', []], 125*8ed75a23SAndreas Gohr ['p_open', []], 126*8ed75a23SAndreas Gohr ['cdata', ["\n"]], 127*8ed75a23SAndreas Gohr ['monospace_open', []], 128*8ed75a23SAndreas Gohr ['unformatted', ['foo']], 129*8ed75a23SAndreas Gohr ['monospace_close', []], 130*8ed75a23SAndreas Gohr ['cdata', ['']], 131*8ed75a23SAndreas Gohr ['p_close', []], 132*8ed75a23SAndreas Gohr ['document_end', []], 133*8ed75a23SAndreas Gohr ]; 134*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 135*8ed75a23SAndreas Gohr } 136*8ed75a23SAndreas Gohr 137*8ed75a23SAndreas Gohr function testAllWhitespaceBodyIsPreserved() 138*8ed75a23SAndreas Gohr { 139*8ed75a23SAndreas Gohr // A body of pure whitespace is a valid code span and kept as is 140*8ed75a23SAndreas Gohr // (strip is skipped because trim of the body is empty). 141*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 142*8ed75a23SAndreas Gohr $this->P->parse('a ` ` b'); 143*8ed75a23SAndreas Gohr $calls = [ 144*8ed75a23SAndreas Gohr ['document_start', []], 145*8ed75a23SAndreas Gohr ['p_open', []], 146*8ed75a23SAndreas Gohr ['cdata', ["\na "]], 147*8ed75a23SAndreas Gohr ['monospace_open', []], 148*8ed75a23SAndreas Gohr ['unformatted', [' ']], 149*8ed75a23SAndreas Gohr ['monospace_close', []], 150*8ed75a23SAndreas Gohr ['cdata', [' b']], 151*8ed75a23SAndreas Gohr ['p_close', []], 152*8ed75a23SAndreas Gohr ['document_end', []], 153*8ed75a23SAndreas Gohr ]; 154*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 155*8ed75a23SAndreas Gohr } 156*8ed75a23SAndreas Gohr 157*8ed75a23SAndreas Gohr function testEmptyDelimiterDoesNotMatch() 158*8ed75a23SAndreas Gohr { 159*8ed75a23SAndreas Gohr // Two adjacent backticks with no matching pair later in the 160*8ed75a23SAndreas Gohr // paragraph stay literal — the length-boundary guards reject them 161*8ed75a23SAndreas Gohr // as an n=1 opener followed immediately by an n=1 closer. 162*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 163*8ed75a23SAndreas Gohr $this->P->parse('foo `` bar'); 164*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 165*8ed75a23SAndreas Gohr $this->assertNotContains('monospace_open', $modes, 166*8ed75a23SAndreas Gohr 'Bare adjacent backticks with no closer must stay literal'); 167*8ed75a23SAndreas Gohr } 168*8ed75a23SAndreas Gohr 169*8ed75a23SAndreas Gohr function testN1BodyCanContainDoubleBacktickRun() 170*8ed75a23SAndreas Gohr { 171*8ed75a23SAndreas Gohr // GFM example 340. Input backtick-space-2xbacktick-space-backtick. 172*8ed75a23SAndreas Gohr // The interior run of two is not a valid n=1 closer, so it lives 173*8ed75a23SAndreas Gohr // in the body; edge-space stripping then leaves just the two 174*8ed75a23SAndreas Gohr // backticks as the body content. 175*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 176*8ed75a23SAndreas Gohr $this->P->parse('` `` `'); 177*8ed75a23SAndreas Gohr $calls = [ 178*8ed75a23SAndreas Gohr ['document_start', []], 179*8ed75a23SAndreas Gohr ['p_open', []], 180*8ed75a23SAndreas Gohr ['cdata', ["\n"]], 181*8ed75a23SAndreas Gohr ['monospace_open', []], 182*8ed75a23SAndreas Gohr ['unformatted', ['``']], 183*8ed75a23SAndreas Gohr ['monospace_close', []], 184*8ed75a23SAndreas Gohr ['cdata', ['']], 185*8ed75a23SAndreas Gohr ['p_close', []], 186*8ed75a23SAndreas Gohr ['document_end', []], 187*8ed75a23SAndreas Gohr ]; 188*8ed75a23SAndreas Gohr $this->assertCalls($calls, $this->H->calls); 189*8ed75a23SAndreas Gohr } 190*8ed75a23SAndreas Gohr 191*8ed75a23SAndreas Gohr function testRunOfThreeBackticksIsNotAnN1Span() 192*8ed75a23SAndreas Gohr { 193*8ed75a23SAndreas Gohr // The length-boundary guards on the opener reject a backtick that 194*8ed75a23SAndreas Gohr // is immediately followed by another one, so a run of three or 195*8ed75a23SAndreas Gohr // more never opens an n=1 span. Triple-backtick fenced blocks 196*8ed75a23SAndreas Gohr // are a separate mode's concern. 197*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 198*8ed75a23SAndreas Gohr $this->P->parse('foo ```bar``` baz'); 199*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 200*8ed75a23SAndreas Gohr $this->assertNotContains('monospace_open', $modes, 201*8ed75a23SAndreas Gohr 'A run of 3 backticks must not trigger an n=1 span'); 202*8ed75a23SAndreas Gohr } 203*8ed75a23SAndreas Gohr 204*8ed75a23SAndreas Gohr function testDoesNotSpanParagraphBoundary() 205*8ed75a23SAndreas Gohr { 206*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 207*8ed75a23SAndreas Gohr $this->P->parse("This `has a\n\nnew paragraph`."); 208*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 209*8ed75a23SAndreas Gohr $this->assertNotContains('monospace_open', $modes, 210*8ed75a23SAndreas Gohr 'GfmBacktickSingle must not open when the closer is past a blank line'); 211*8ed75a23SAndreas Gohr } 212*8ed75a23SAndreas Gohr 213*8ed75a23SAndreas Gohr function testAllowsSingleNewline() 214*8ed75a23SAndreas Gohr { 215*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 216*8ed75a23SAndreas Gohr $this->P->parse("`open\nclose`"); 217*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 218*8ed75a23SAndreas Gohr $this->assertContains('monospace_open', $modes, 219*8ed75a23SAndreas Gohr 'GfmBacktickSingle must still match across a single newline'); 220*8ed75a23SAndreas Gohr } 221*8ed75a23SAndreas Gohr 222*8ed75a23SAndreas Gohr function testContentIsLiteral() 223*8ed75a23SAndreas Gohr { 224*8ed75a23SAndreas Gohr // Other inline modes must not parse inside a code span. 225*8ed75a23SAndreas Gohr $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 226*8ed75a23SAndreas Gohr $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 227*8ed75a23SAndreas Gohr $this->P->parse('`*foo*`'); 228*8ed75a23SAndreas Gohr $modes = array_column($this->H->calls, 0); 229*8ed75a23SAndreas Gohr $this->assertNotContains('emphasis_open', $modes, 230*8ed75a23SAndreas Gohr 'Emphasis must not parse inside a code span'); 231*8ed75a23SAndreas Gohr $this->assertContains('monospace_open', $modes, 232*8ed75a23SAndreas Gohr 'Backtick span must emit monospace_open'); 233*8ed75a23SAndreas Gohr 234*8ed75a23SAndreas Gohr // The emphasized text stays as an unformatted (verbatim) call 235*8ed75a23SAndreas Gohr // inside the span — same treatment as nowiki and %%. 236*8ed75a23SAndreas Gohr $unformatted = array_filter($this->H->calls, static fn($c) => $c[0] === 'unformatted'); 237*8ed75a23SAndreas Gohr $bodies = array_map(static fn($c) => $c[1][0], $unformatted); 238*8ed75a23SAndreas Gohr $this->assertContains('*foo*', $bodies, 239*8ed75a23SAndreas Gohr 'Raw *foo* must appear as verbatim unformatted content'); 240*8ed75a23SAndreas Gohr } 241*8ed75a23SAndreas Gohr 242*8ed75a23SAndreas Gohr function testSortValue() 243*8ed75a23SAndreas Gohr { 244*8ed75a23SAndreas Gohr $mode = new GfmBacktickSingle(); 245*8ed75a23SAndreas Gohr $this->assertSame(165, $mode->getSort()); 246*8ed75a23SAndreas Gohr } 247*8ed75a23SAndreas Gohr} 248