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