xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisTest.php (revision 057ab5a057a76df652cfe4176512a550ca9937f6)
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