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