1<?php 2 3namespace dokuwiki\test\Parsing\ParserMode; 4 5use dokuwiki\Parsing\ParserMode\GfmBacktickSingle; 6use dokuwiki\Parsing\ParserMode\GfmEmphasis; 7use dokuwiki\Parsing\ParserMode\Monospace; 8 9/** 10 * Tests for the GFM asterisk emphasis mode (`*text*`). 11 */ 12class GfmEmphasisTest extends ParserTestBase 13{ 14 public function setUp(): void 15 { 16 parent::setUp(); 17 $this->setSyntax('md'); 18 } 19 20 function testBasicAsterisk() 21 { 22 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 23 $this->P->parse('Foo *Bar* Baz'); 24 $calls = [ 25 ['document_start', []], 26 ['p_open', []], 27 ['cdata', ["\nFoo "]], 28 ['emphasis_open', []], 29 ['cdata', ['Bar']], 30 ['emphasis_close', []], 31 ['cdata', [' Baz']], 32 ['p_close', []], 33 ['document_end', []], 34 ]; 35 $this->assertCalls($calls, $this->H->calls); 36 } 37 38 function testSingleCharacter() 39 { 40 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 41 $this->P->parse('foo *b* bar'); 42 $calls = [ 43 ['document_start', []], 44 ['p_open', []], 45 ['cdata', ["\nfoo "]], 46 ['emphasis_open', []], 47 ['cdata', ['b']], 48 ['emphasis_close', []], 49 ['cdata', [' bar']], 50 ['p_close', []], 51 ['document_end', []], 52 ]; 53 $this->assertCalls($calls, $this->H->calls); 54 } 55 56 function testMultipleWords() 57 { 58 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 59 $this->P->parse('*three four five*'); 60 $calls = [ 61 ['document_start', []], 62 ['p_open', []], 63 ['cdata', ["\n"]], 64 ['emphasis_open', []], 65 ['cdata', ['three four five']], 66 ['emphasis_close', []], 67 ['cdata', ['']], 68 ['p_close', []], 69 ['document_end', []], 70 ]; 71 $this->assertCalls($calls, $this->H->calls); 72 } 73 74 function testTwoSeparateEmphasisOnOneLine() 75 { 76 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 77 $this->P->parse('*one* and *two*'); 78 $calls = [ 79 ['document_start', []], 80 ['p_open', []], 81 ['cdata', ["\n"]], 82 ['emphasis_open', []], 83 ['cdata', ['one']], 84 ['emphasis_close', []], 85 ['cdata', [' and ']], 86 ['emphasis_open', []], 87 ['cdata', ['two']], 88 ['emphasis_close', []], 89 ['cdata', ['']], 90 ['p_close', []], 91 ['document_end', []], 92 ]; 93 $this->assertCalls($calls, $this->H->calls); 94 } 95 96 function testUnmatchedOpenerDoesNotEmphasise() 97 { 98 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 99 $this->P->parse('foo *bar with no closer'); 100 $calls = [ 101 ['document_start', []], 102 ['p_open', []], 103 ['cdata', ["\nfoo *bar with no closer"]], 104 ['p_close', []], 105 ['document_end', []], 106 ]; 107 $this->assertCalls($calls, $this->H->calls); 108 } 109 110 function testOpenerFollowedBySpaceDoesNotEmphasise() 111 { 112 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 113 $this->P->parse('foo * bar* baz'); 114 $calls = [ 115 ['document_start', []], 116 ['p_open', []], 117 ['cdata', ["\nfoo * bar* baz"]], 118 ['p_close', []], 119 ['document_end', []], 120 ]; 121 $this->assertCalls($calls, $this->H->calls); 122 } 123 124 function testEmptyDelimiterDoesNotEmphasise() 125 { 126 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 127 $this->P->parse('foo ** bar'); 128 $calls = [ 129 ['document_start', []], 130 ['p_open', []], 131 ['cdata', ["\nfoo ** bar"]], 132 ['p_close', []], 133 ['document_end', []], 134 ]; 135 $this->assertCalls($calls, $this->H->calls); 136 } 137 138 function testUnderscoreIsNotEmphasised() 139 { 140 // GfmEmphasis handles `*` only — `_` is reserved to avoid the 141 // `__underline__` conflict with DokuWiki's underline syntax; 142 // GfmEmphasisUnderscore handles `_` separately when MD-preferred. 143 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 144 $this->P->parse('foo _bar_ baz'); 145 $calls = [ 146 ['document_start', []], 147 ['p_open', []], 148 ['cdata', ["\nfoo _bar_ baz"]], 149 ['p_close', []], 150 ['document_end', []], 151 ]; 152 $this->assertCalls($calls, $this->H->calls); 153 } 154 155 function testMultilineEmphasis() 156 { 157 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 158 $this->P->parse("*line\nline\nline*"); 159 $calls = [ 160 ['document_start', []], 161 ['p_open', []], 162 ['cdata', ["\n"]], 163 ['emphasis_open', []], 164 ['cdata', ["line\nline\nline"]], 165 ['emphasis_close', []], 166 ['cdata', ['']], 167 ['p_close', []], 168 ['document_end', []], 169 ]; 170 $this->assertCalls($calls, $this->H->calls); 171 } 172 173 function testModeNameIsDistinctFromInstructionName() 174 { 175 // The lexer mode is registered as 'gfm_emphasis' (to avoid collision 176 // with DW Emphasis), but instructions are 'emphasis_open/close' 177 // so the existing XHTML renderer emits <em>. 178 $mode = new GfmEmphasis(); 179 $this->assertSame(80, $mode->getSort()); 180 } 181 182 function testDoesNotSpanParagraphBoundary() 183 { 184 // An unclosed `*` followed by a blank line must stay literal — the 185 // entry pattern's lookahead is paragraph-boundary-safe. 186 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 187 $this->P->parse("*open\n\nclose*"); 188 $modes = array_column($this->H->calls, 0); 189 $this->assertNotContains('emphasis_open', $modes, 190 'GfmEmphasis must not open when the closing `*` is past a blank line'); 191 } 192 193 function testAllowsSingleNewline() 194 { 195 // Single newlines are fine inside emphasis (multi-line emphasis). 196 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 197 $this->P->parse("*open\nclose*"); 198 $modes = array_column($this->H->calls, 0); 199 $this->assertContains('emphasis_open', $modes, 200 'GfmEmphasis must still match across a single newline'); 201 } 202 203 function testAsteriskDoesNotSpanMonospaceBoundary() 204 { 205 // A `*` in a glob inside ''…'' must not pair with the `*` of a 206 // following ''…'' span: its closer lies past the monospace closer, so 207 // it stays literal instead of dragging the monospace boundary along. 208 $this->setSyntax('dw+md'); 209 $this->P->addMode('monospace', new Monospace()); 210 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 211 $this->P->parse("''aaa*.conf'', ''bbb*.conf''"); 212 213 $calls = array_map(fn($call) => [$call[0], $call[1]], $this->H->calls); 214 $this->assertNotContains(['emphasis_open', []], $calls); 215 $this->assertContains(['cdata', ['aaa*.conf']], $calls); 216 $this->assertContains(['cdata', ['bbb*.conf']], $calls); 217 } 218 219 function testAsteriskStillEmphasisesWithinOneMonospaceSpan() 220 { 221 // The counterpart: a `*…*` pair fully inside one ''…'' span still 222 // renders as emphasis. 223 $this->setSyntax('dw+md'); 224 $this->P->addMode('monospace', new Monospace()); 225 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 226 $this->P->parse("''a*b*c''"); 227 228 $calls = array_map(fn($call) => [$call[0], $call[1]], $this->H->calls); 229 $this->assertContains(['emphasis_open', []], $calls); 230 $this->assertContains(['cdata', ['b']], $calls); 231 } 232 233 function testAsteriskAdjacentToMonospaceCloserStaysLiteral() 234 { 235 // A lone `*` as the entire ''…'' content sits right before the 236 // monospace closer; pairing it with a `*` in a later span would 237 // drag the monospace boundary along, so it stays literal. 238 $this->setSyntax('dw+md'); 239 $this->P->addMode('monospace', new Monospace()); 240 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 241 $this->P->parse("''*'' as the subdomain name, e.g.: ''*.example.com''"); 242 243 $calls = array_map(fn($call) => [$call[0], $call[1]], $this->H->calls); 244 $this->assertNotContains(['emphasis_open', []], $calls); 245 $this->assertContains(['cdata', ['*']], $calls); 246 $this->assertContains(['cdata', ['*.example.com']], $calls); 247 } 248 249 function testAsteriskCloserInsideBacktickSpanDoesNotCount() 250 { 251 // The only closing `*` candidate lives inside a backtick code 252 // span whose content is verbatim — the opener can never close, so 253 // it stays literal. 254 $this->setSyntax('dw+md'); 255 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 256 $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 257 $this->P->parse('*conf `x*y` end.'); 258 259 $calls = array_map(fn($call) => [$call[0], $call[1]], $this->H->calls); 260 $this->assertNotContains(['emphasis_open', []], $calls); 261 $this->assertContains(['unformatted', ['x*y']], $calls); 262 } 263 264 function testAsteriskPairSpanningBacktickSpanStillEmphasises() 265 { 266 // The counterpart: with a real closer behind the code span, the 267 // emphasis wraps the span instead of being rejected because of it. 268 $this->setSyntax('dw+md'); 269 $this->P->addMode('gfm_emphasis', new GfmEmphasis()); 270 $this->P->addMode('gfm_backtick_single', new GfmBacktickSingle()); 271 $this->P->parse('*a `b` c*'); 272 273 $calls = array_map(fn($call) => [$call[0], $call[1]], $this->H->calls); 274 $this->assertContains(['emphasis_open', []], $calls); 275 $this->assertContains(['emphasis_close', []], $calls); 276 $this->assertContains(['unformatted', ['b']], $calls); 277 } 278} 279