xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmEmphasisUnderscoreTest.php (revision fe58309edafb067d90f3f40ef3d416100d558a04)
1<?php
2
3namespace dokuwiki\test\Parsing\ParserMode;
4
5use dokuwiki\Parsing\ParserMode\GfmEmphasisUnderscore;
6
7/**
8 * Tests for the GFM underscore emphasis mode (`_text_`).
9 */
10class GfmEmphasisUnderscoreTest extends ParserTestBase
11{
12    public function setUp(): void
13    {
14        parent::setUp();
15        $this->setSyntax('md');
16    }
17
18    public function testBasicUnderscore()
19    {
20        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
21        $this->P->parse('Foo _Bar_ Baz');
22
23        $calls = [
24            ['document_start', []],
25            ['p_open', []],
26            ['cdata', ["\nFoo "]],
27            ['emphasis_open', []],
28            ['cdata', ['Bar']],
29            ['emphasis_close', []],
30            ['cdata', [' Baz']],
31            ['p_close', []],
32            ['document_end', []],
33        ];
34        $this->assertCalls($calls, $this->H->calls);
35    }
36
37    public function testSingleCharacter()
38    {
39        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
40        $this->P->parse('foo _b_ bar');
41
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    public function testMultipleWords()
57    {
58        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
59        $this->P->parse('_one two three_');
60
61        $calls = [
62            ['document_start', []],
63            ['p_open', []],
64            ['cdata', ["\n"]],
65            ['emphasis_open', []],
66            ['cdata', ['one two three']],
67            ['emphasis_close', []],
68            ['cdata', ['']],
69            ['p_close', []],
70            ['document_end', []],
71        ];
72        $this->assertCalls($calls, $this->H->calls);
73    }
74
75    public function testIntrawordUnderscoreIsNotEmphasised()
76    {
77        // GFM's key word-boundary rule: underscores inside words stay literal.
78        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
79        $this->P->parse('this_is_not_an_emphasis');
80
81        $calls = [
82            ['document_start', []],
83            ['p_open', []],
84            ['cdata', ["\nthis_is_not_an_emphasis"]],
85            ['p_close', []],
86            ['document_end', []],
87        ];
88        $this->assertCalls($calls, $this->H->calls);
89    }
90
91    public function testOpenerFollowedBySpaceDoesNotEmphasise()
92    {
93        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
94        $this->P->parse('foo _ bar_ baz');
95
96        $calls = [
97            ['document_start', []],
98            ['p_open', []],
99            ['cdata', ["\nfoo _ bar_ baz"]],
100            ['p_close', []],
101            ['document_end', []],
102        ];
103        $this->assertCalls($calls, $this->H->calls);
104    }
105
106    public function testDoubleUnderscoreDoesNotEmphasise()
107    {
108        // `__foo__` must stay literal. At the first `_`, the lookahead
109        // `(?=[^\s_])` forbids entry (next char is another `_`). At the
110        // second `_`, the lookbehind also fails because `_` itself is not
111        // a "non-word" character (it's excluded from NON_WORD_CHAR so that
112        // `__foo` can't open emphasis at the inner underscore).
113        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
114        $this->P->parse('foo __bar__ baz');
115
116        $calls = [
117            ['document_start', []],
118            ['p_open', []],
119            ['cdata', ["\nfoo __bar__ baz"]],
120            ['p_close', []],
121            ['document_end', []],
122        ];
123        $this->assertCalls($calls, $this->H->calls);
124    }
125
126    public function testTwoSeparateEmphasisOnOneLine()
127    {
128        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
129        $this->P->parse('_one_ and _two_');
130
131        $calls = [
132            ['document_start', []],
133            ['p_open', []],
134            ['cdata', ["\n"]],
135            ['emphasis_open', []],
136            ['cdata', ['one']],
137            ['emphasis_close', []],
138            ['cdata', [' and ']],
139            ['emphasis_open', []],
140            ['cdata', ['two']],
141            ['emphasis_close', []],
142            ['cdata', ['']],
143            ['p_close', []],
144            ['document_end', []],
145        ];
146        $this->assertCalls($calls, $this->H->calls);
147    }
148
149    public function testMultilineEmphasis()
150    {
151        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
152        $this->P->parse("_line\nline\nline_");
153
154        $calls = [
155            ['document_start', []],
156            ['p_open', []],
157            ['cdata', ["\n"]],
158            ['emphasis_open', []],
159            ['cdata', ["line\nline\nline"]],
160            ['emphasis_close', []],
161            ['cdata', ['']],
162            ['p_close', []],
163            ['document_end', []],
164        ];
165        $this->assertCalls($calls, $this->H->calls);
166    }
167
168    public function testSortValue()
169    {
170        $mode = new GfmEmphasisUnderscore();
171        $this->assertSame(80, $mode->getSort());
172    }
173
174    public function testDoesNotSpanParagraphBoundary()
175    {
176        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
177        $this->P->parse("_open\n\nclose_");
178
179        $modes = array_column($this->H->calls, 0);
180        $this->assertNotContains('emphasis_open', $modes,
181            'GfmEmphasisUnderscore must not open when the closing `_` is past a blank line');
182    }
183
184    public function testAllowsSingleNewlineInsideMultiline()
185    {
186        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
187        $this->P->parse("_open\nclose_");
188
189        $modes = array_column($this->H->calls, 0);
190        $this->assertContains('emphasis_open', $modes,
191            'GfmEmphasisUnderscore must still match across a single newline');
192    }
193
194    /**
195     * The intraword rule must apply to multibyte letters, not just ASCII.
196     * This test is derived from CommonMark spec §6.2 example 418:
197     *
198     *     пристаням_стремятся_
199     *
200     * which must render as literal (no emphasis). The surrounding Cyrillic
201     * letters are word-like; the underscores are intraword and must not
202     * emphasize.
203     *
204     * The word-boundary constants (NO_WORD_BEFORE / NO_WORD_AFTER) are
205     * defined positively (matching explicit non-word chars) rather than
206     * negatively (not matching a-zA-Z0-9), so multibyte UTF-8 bytes — which
207     * are not in any ASCII class — are correctly treated as word-like.
208     *
209     * @dataProvider provideMultibyteIntrawordCases
210     */
211    public function testIntrawordUnderscoreInMultibyteText(string $input)
212    {
213        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
214        $this->P->parse($input);
215
216        $modes = array_column($this->H->calls, 0);
217        $this->assertNotContains(
218            'emphasis_open',
219            $modes,
220            "Intraword `_` in multibyte text must not emphasize: " . json_encode($input)
221        );
222    }
223
224    public static function provideMultibyteIntrawordCases(): array
225    {
226        return [
227            // CommonMark spec §6.2 ex. 418 — Cyrillic intraword
228            'cyrillic-trailing'  => ['пристаням_стремятся_'],
229            // CommonMark spec §6.2 ex. 420 — Cyrillic leading
230            'cyrillic-leading'   => ['_пристаням_стремятся'],
231            // German umlaut — no established spec example, but the expected
232            // behavior is uncontroversial: intraword `_` stays literal.
233            'german-umlaut'      => ['für_etwas_text'],
234            // CJK — same expectation
235            'cjk-intraword'      => ['日本_語_の'],
236            // Greek
237            'greek-intraword'    => ['αυτό_είναι_κείμενο'],
238        ];
239    }
240
241    /**
242     * A `_foo_` span surrounded by multibyte letters must NOT open at the
243     * first `_` (it would be intraword) AND must still NOT open if the
244     * following letters are multibyte. Verifies that both the lookbehind
245     * and the closing-delimiter lookahead reject multibyte word chars.
246     */
247    public function testMultibyteWordCharsAreNotTreatedAsBoundary()
248    {
249        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
250        // Intraword between Cyrillic on the left and Cyrillic on the right.
251        $this->P->parse('до_середины_текста');
252
253        $modes = array_column($this->H->calls, 0);
254        $this->assertNotContains('emphasis_open', $modes,
255            'Cyrillic-surrounded `_` must not emphasize');
256    }
257
258    /**
259     * Positive: when the surrounding non-word context is whitespace or
260     * punctuation, multibyte content *inside* the emphasis span is fine.
261     * `_für etwas_` surrounded by spaces should emphasize the multibyte text.
262     */
263    public function testMultibyteContentInsideEmphasisWorks()
264    {
265        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
266        $this->P->parse('foo _für etwas_ bar');
267
268        $modes = array_column($this->H->calls, 0);
269        $this->assertContains('emphasis_open', $modes,
270            'Multibyte text inside `_..._` must emphasize when boundaries are clear');
271        $this->assertContains('emphasis_close', $modes,
272            'Multibyte text inside `_..._` must emphasize when boundaries are clear');
273    }
274
275    public function testRejectedOpenerBeforeValidSpanStaysLiteral()
276    {
277        $this->P->addMode('gfm_emphasis_underscore', new GfmEmphasisUnderscore());
278        $this->P->parse('_ foo _bar_');
279        $this->assertContains('emphasis_open', array_column($this->H->calls, 0));
280        $this->assertStringContainsString('_ foo ', $this->H->calls[2][1][0]);
281    }
282}
283