xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmLinkTest.php (revision 4f32c45be746b2e49db6bcc6cc733a89bbd14d81)
1e89aeebdSAndreas Gohr<?php
2e89aeebdSAndreas Gohr
3e89aeebdSAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode;
4e89aeebdSAndreas Gohr
5e89aeebdSAndreas Gohruse dokuwiki\Parsing\ModeRegistry;
6e89aeebdSAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmLink;
7e89aeebdSAndreas Gohruse dokuwiki\Parsing\ParserMode\Internallink;
8e89aeebdSAndreas Gohr
9e89aeebdSAndreas Gohr/**
10e89aeebdSAndreas Gohr * Tests for GFM inline links `[text](url)` dispatching to DokuWiki's
11e89aeebdSAndreas Gohr * internal / external / interwiki / email / windowsshare / local link
12e89aeebdSAndreas Gohr * handler instructions.
13e89aeebdSAndreas Gohr */
14e89aeebdSAndreas Gohrclass GfmLinkTest extends ParserTestBase
15e89aeebdSAndreas Gohr{
16e89aeebdSAndreas Gohr    public function setUp(): void
17e89aeebdSAndreas Gohr    {
18e89aeebdSAndreas Gohr        parent::setUp();
19e89aeebdSAndreas Gohr        global $conf;
2013a62f81SAndreas Gohr        $conf['syntax'] = 'md';
21e89aeebdSAndreas Gohr        ModeRegistry::reset();
22e89aeebdSAndreas Gohr    }
23e89aeebdSAndreas Gohr
24e89aeebdSAndreas Gohr    public function tearDown(): void
25e89aeebdSAndreas Gohr    {
26e89aeebdSAndreas Gohr        ModeRegistry::reset();
27e89aeebdSAndreas Gohr        parent::tearDown();
28e89aeebdSAndreas Gohr    }
29e89aeebdSAndreas Gohr
30e89aeebdSAndreas Gohr    function testInternalPage()
31e89aeebdSAndreas Gohr    {
32e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
33e89aeebdSAndreas Gohr        $this->P->parse('Foo [text](page) Bar');
34e89aeebdSAndreas Gohr        $calls = [
35e89aeebdSAndreas Gohr            ['document_start', []],
36e89aeebdSAndreas Gohr            ['p_open', []],
37e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
38e89aeebdSAndreas Gohr            ['internallink', ['page', 'text']],
39e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
40e89aeebdSAndreas Gohr            ['p_close', []],
41e89aeebdSAndreas Gohr            ['document_end', []],
42e89aeebdSAndreas Gohr        ];
43e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
44e89aeebdSAndreas Gohr    }
45e89aeebdSAndreas Gohr
46e89aeebdSAndreas Gohr    function testInternalPageWithNamespace()
47e89aeebdSAndreas Gohr    {
48e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
49e89aeebdSAndreas Gohr        $this->P->parse('Foo [Syntax](wiki:syntax#internal) Bar');
50e89aeebdSAndreas Gohr        $calls = [
51e89aeebdSAndreas Gohr            ['document_start', []],
52e89aeebdSAndreas Gohr            ['p_open', []],
53e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
54e89aeebdSAndreas Gohr            ['internallink', ['wiki:syntax#internal', 'Syntax']],
55e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
56e89aeebdSAndreas Gohr            ['p_close', []],
57e89aeebdSAndreas Gohr            ['document_end', []],
58e89aeebdSAndreas Gohr        ];
59e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
60e89aeebdSAndreas Gohr    }
61e89aeebdSAndreas Gohr
62e89aeebdSAndreas Gohr    function testExternalLink()
63e89aeebdSAndreas Gohr    {
64e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
65e89aeebdSAndreas Gohr        $this->P->parse('Foo [Google](http://google.com) Bar');
66e89aeebdSAndreas Gohr        $calls = [
67e89aeebdSAndreas Gohr            ['document_start', []],
68e89aeebdSAndreas Gohr            ['p_open', []],
69e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
70e89aeebdSAndreas Gohr            ['externallink', ['http://google.com', 'Google']],
71e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
72e89aeebdSAndreas Gohr            ['p_close', []],
73e89aeebdSAndreas Gohr            ['document_end', []],
74e89aeebdSAndreas Gohr        ];
75e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
76e89aeebdSAndreas Gohr    }
77e89aeebdSAndreas Gohr
78e89aeebdSAndreas Gohr    function testInterwikiLink()
79e89aeebdSAndreas Gohr    {
80e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
81e89aeebdSAndreas Gohr        $this->P->parse('Foo [callbacks](wp>Callback) Bar');
82e89aeebdSAndreas Gohr        $calls = [
83e89aeebdSAndreas Gohr            ['document_start', []],
84e89aeebdSAndreas Gohr            ['p_open', []],
85e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
86e89aeebdSAndreas Gohr            ['interwikilink', ['wp>Callback', 'callbacks', 'wp', 'Callback']],
87e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
88e89aeebdSAndreas Gohr            ['p_close', []],
89e89aeebdSAndreas Gohr            ['document_end', []],
90e89aeebdSAndreas Gohr        ];
91e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
92e89aeebdSAndreas Gohr    }
93e89aeebdSAndreas Gohr
94e89aeebdSAndreas Gohr    function testInterwikiLinkCaseNormalized()
95e89aeebdSAndreas Gohr    {
96e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
97e89aeebdSAndreas Gohr        $this->P->parse('Foo [Page](IW>somepage) Bar');
98e89aeebdSAndreas Gohr        $calls = [
99e89aeebdSAndreas Gohr            ['document_start', []],
100e89aeebdSAndreas Gohr            ['p_open', []],
101e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
102e89aeebdSAndreas Gohr            ['interwikilink', ['IW>somepage', 'Page', 'iw', 'somepage']],
103e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
104e89aeebdSAndreas Gohr            ['p_close', []],
105e89aeebdSAndreas Gohr            ['document_end', []],
106e89aeebdSAndreas Gohr        ];
107e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
108e89aeebdSAndreas Gohr    }
109e89aeebdSAndreas Gohr
110e89aeebdSAndreas Gohr    function testEmailLink()
111e89aeebdSAndreas Gohr    {
112e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
113e89aeebdSAndreas Gohr        $this->P->parse('Foo [mail](user@example.com) Bar');
114e89aeebdSAndreas Gohr        $calls = [
115e89aeebdSAndreas Gohr            ['document_start', []],
116e89aeebdSAndreas Gohr            ['p_open', []],
117e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
118e89aeebdSAndreas Gohr            ['emaillink', ['user@example.com', 'mail']],
119e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
120e89aeebdSAndreas Gohr            ['p_close', []],
121e89aeebdSAndreas Gohr            ['document_end', []],
122e89aeebdSAndreas Gohr        ];
123e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
124e89aeebdSAndreas Gohr    }
125e89aeebdSAndreas Gohr
126e89aeebdSAndreas Gohr    function testLocalAnchor()
127e89aeebdSAndreas Gohr    {
128e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
129e89aeebdSAndreas Gohr        $this->P->parse('Foo [section](#anchor) Bar');
130e89aeebdSAndreas Gohr        $calls = [
131e89aeebdSAndreas Gohr            ['document_start', []],
132e89aeebdSAndreas Gohr            ['p_open', []],
133e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
134e89aeebdSAndreas Gohr            ['locallink', ['anchor', 'section']],
135e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
136e89aeebdSAndreas Gohr            ['p_close', []],
137e89aeebdSAndreas Gohr            ['document_end', []],
138e89aeebdSAndreas Gohr        ];
139e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
140e89aeebdSAndreas Gohr    }
141e89aeebdSAndreas Gohr
142e89aeebdSAndreas Gohr    function testWindowsShare()
143e89aeebdSAndreas Gohr    {
144e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
145e89aeebdSAndreas Gohr        $this->P->parse('Foo [share](\\\\server\\share) Bar');
146e89aeebdSAndreas Gohr        $calls = [
147e89aeebdSAndreas Gohr            ['document_start', []],
148e89aeebdSAndreas Gohr            ['p_open', []],
149e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
150e89aeebdSAndreas Gohr            ['windowssharelink', ['\\\\server\\share', 'share']],
151e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
152e89aeebdSAndreas Gohr            ['p_close', []],
153e89aeebdSAndreas Gohr            ['document_end', []],
154e89aeebdSAndreas Gohr        ];
155e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
156e89aeebdSAndreas Gohr    }
157e89aeebdSAndreas Gohr
158e89aeebdSAndreas Gohr    function testTitleInDoubleQuotesIsDiscarded()
159e89aeebdSAndreas Gohr    {
160e89aeebdSAndreas Gohr        // GFM allows [text](url "title") but DokuWiki's link handler
161e89aeebdSAndreas Gohr        // instructions have no title-attribute slot. The title parses
162e89aeebdSAndreas Gohr        // cleanly but is dropped; the resulting handler call is identical
163e89aeebdSAndreas Gohr        // to the no-title case.
164e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
165e89aeebdSAndreas Gohr        $this->P->parse('Foo [Google](http://google.com "Search engine") Bar');
166e89aeebdSAndreas Gohr        $calls = [
167e89aeebdSAndreas Gohr            ['document_start', []],
168e89aeebdSAndreas Gohr            ['p_open', []],
169e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
170e89aeebdSAndreas Gohr            ['externallink', ['http://google.com', 'Google']],
171e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
172e89aeebdSAndreas Gohr            ['p_close', []],
173e89aeebdSAndreas Gohr            ['document_end', []],
174e89aeebdSAndreas Gohr        ];
175e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
176e89aeebdSAndreas Gohr    }
177e89aeebdSAndreas Gohr
178e89aeebdSAndreas Gohr    function testTitleInSingleQuotesIsDiscarded()
179e89aeebdSAndreas Gohr    {
180e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
181e89aeebdSAndreas Gohr        $this->P->parse("Foo [page](target 'a title') Bar");
182e89aeebdSAndreas Gohr        $calls = [
183e89aeebdSAndreas Gohr            ['document_start', []],
184e89aeebdSAndreas Gohr            ['p_open', []],
185e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
186e89aeebdSAndreas Gohr            ['internallink', ['target', 'page']],
187e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
188e89aeebdSAndreas Gohr            ['p_close', []],
189e89aeebdSAndreas Gohr            ['document_end', []],
190e89aeebdSAndreas Gohr        ];
191e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
192e89aeebdSAndreas Gohr    }
193e89aeebdSAndreas Gohr
194e89aeebdSAndreas Gohr    function testSpaceBetweenBracketsAndParensIsNotALink()
195e89aeebdSAndreas Gohr    {
196e89aeebdSAndreas Gohr        // GFM explicitly forbids whitespace between `]` and `(`.
197e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
198e89aeebdSAndreas Gohr        $this->P->parse('[foo] (bar)');
199e89aeebdSAndreas Gohr        $modes = array_column($this->H->calls, 0);
200e89aeebdSAndreas Gohr        $this->assertNotContains('internallink', $modes);
201e89aeebdSAndreas Gohr        $this->assertNotContains('externallink', $modes);
202e89aeebdSAndreas Gohr    }
203e89aeebdSAndreas Gohr
204e89aeebdSAndreas Gohr    function testDwDoubleBracketNotConsumedByGfmLink()
205e89aeebdSAndreas Gohr    {
206e89aeebdSAndreas Gohr        // With both gfm_link and DW internallink loaded (mixed syntax),
207e89aeebdSAndreas Gohr        // `[[foo]]` must go to Internallink. GfmLink's `\[(?!\[)` guard
208e89aeebdSAndreas Gohr        // refuses single-bracket matches that are actually part of `[[`.
209e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
210e89aeebdSAndreas Gohr        $this->P->addMode('internallink', new Internallink());
211e89aeebdSAndreas Gohr        $this->P->parse('Foo [[bar]] Baz');
212e89aeebdSAndreas Gohr        $calls = [
213e89aeebdSAndreas Gohr            ['document_start', []],
214e89aeebdSAndreas Gohr            ['p_open', []],
215e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
216e89aeebdSAndreas Gohr            ['internallink', ['bar', null]],
217e89aeebdSAndreas Gohr            ['cdata', [' Baz']],
218e89aeebdSAndreas Gohr            ['p_close', []],
219e89aeebdSAndreas Gohr            ['document_end', []],
220e89aeebdSAndreas Gohr        ];
221e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
222e89aeebdSAndreas Gohr    }
223e89aeebdSAndreas Gohr
224e89aeebdSAndreas Gohr    function testMultibyteLinkText()
225e89aeebdSAndreas Gohr    {
226e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
227e89aeebdSAndreas Gohr        $this->P->parse('Foo [日本語](page) Bar');
228e89aeebdSAndreas Gohr        $calls = [
229e89aeebdSAndreas Gohr            ['document_start', []],
230e89aeebdSAndreas Gohr            ['p_open', []],
231e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
232e89aeebdSAndreas Gohr            ['internallink', ['page', '日本語']],
233e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
234e89aeebdSAndreas Gohr            ['p_close', []],
235e89aeebdSAndreas Gohr            ['document_end', []],
236e89aeebdSAndreas Gohr        ];
237e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
238e89aeebdSAndreas Gohr    }
239e89aeebdSAndreas Gohr
240e89aeebdSAndreas Gohr    function testReferenceStyleLinkNotMatched()
241e89aeebdSAndreas Gohr    {
242e89aeebdSAndreas Gohr        // `[foo][bar]` (reference-style) requires a reference definition
243e89aeebdSAndreas Gohr        // we do not support; each `[...]` should stay literal text.
244e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
245e89aeebdSAndreas Gohr        $this->P->parse('[foo][bar]');
246e89aeebdSAndreas Gohr        $modes = array_column($this->H->calls, 0);
247e89aeebdSAndreas Gohr        $this->assertNotContains('internallink', $modes);
248e89aeebdSAndreas Gohr        $this->assertNotContains('externallink', $modes);
249e89aeebdSAndreas Gohr    }
250e89aeebdSAndreas Gohr
251e89aeebdSAndreas Gohr    function testTwoLinksInOneLine()
252e89aeebdSAndreas Gohr    {
253e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
254e89aeebdSAndreas Gohr        $this->P->parse('Foo [one](a) and [two](b) Bar');
255e89aeebdSAndreas Gohr        $calls = [
256e89aeebdSAndreas Gohr            ['document_start', []],
257e89aeebdSAndreas Gohr            ['p_open', []],
258e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
259e89aeebdSAndreas Gohr            ['internallink', ['a', 'one']],
260e89aeebdSAndreas Gohr            ['cdata', [' and ']],
261e89aeebdSAndreas Gohr            ['internallink', ['b', 'two']],
262e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
263e89aeebdSAndreas Gohr            ['p_close', []],
264e89aeebdSAndreas Gohr            ['document_end', []],
265e89aeebdSAndreas Gohr        ];
266e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
267e89aeebdSAndreas Gohr    }
268e89aeebdSAndreas Gohr
269e89aeebdSAndreas Gohr    function testFragmentInExternalUrl()
270e89aeebdSAndreas Gohr    {
271e89aeebdSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
272e89aeebdSAndreas Gohr        $this->P->parse('Foo [x](http://example.com#fragment) Bar');
273e89aeebdSAndreas Gohr        $calls = [
274e89aeebdSAndreas Gohr            ['document_start', []],
275e89aeebdSAndreas Gohr            ['p_open', []],
276e89aeebdSAndreas Gohr            ['cdata', ["\nFoo "]],
277e89aeebdSAndreas Gohr            ['externallink', ['http://example.com#fragment', 'x']],
278e89aeebdSAndreas Gohr            ['cdata', [' Bar']],
279e89aeebdSAndreas Gohr            ['p_close', []],
280e89aeebdSAndreas Gohr            ['document_end', []],
281e89aeebdSAndreas Gohr        ];
282e89aeebdSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
283e89aeebdSAndreas Gohr    }
284e89aeebdSAndreas Gohr
2853440a8c0SAndreas Gohr    // ----- image-as-label (`[![alt](img)](target)`) -----
2863440a8c0SAndreas Gohr
2873440a8c0SAndreas Gohr    /**
2883440a8c0SAndreas Gohr     * Media descriptor shape GfmLink emits for image-as-label, matching
2893440a8c0SAndreas Gohr     * what Media::parseMedia() returns.
2903440a8c0SAndreas Gohr     */
2913440a8c0SAndreas Gohr    private function mediaArray(array $overrides): array
2923440a8c0SAndreas Gohr    {
2933440a8c0SAndreas Gohr        return array_merge([
2943440a8c0SAndreas Gohr            'type'    => 'internalmedia',
2953440a8c0SAndreas Gohr            'src'     => 'wiki:image.png',
2963440a8c0SAndreas Gohr            'title'   => 'alt',
2973440a8c0SAndreas Gohr            'align'   => null,
2983440a8c0SAndreas Gohr            'width'   => null,
2993440a8c0SAndreas Gohr            'height'  => null,
3003440a8c0SAndreas Gohr            'cache'   => 'cache',
3013440a8c0SAndreas Gohr            'linking' => 'details',
3023440a8c0SAndreas Gohr        ], $overrides);
3033440a8c0SAndreas Gohr    }
3043440a8c0SAndreas Gohr
3053440a8c0SAndreas Gohr    function testImageAsLabelInternalPageLink()
3063440a8c0SAndreas Gohr    {
3073440a8c0SAndreas Gohr        // The canonical case: image that links to a wiki page.
3083440a8c0SAndreas Gohr        // Markdown equivalent of DW's `[[test:link|{{wiki:image.png}}]]`.
3093440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3103440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png)](test:link) Bar');
3113440a8c0SAndreas Gohr        $calls = [
3123440a8c0SAndreas Gohr            ['document_start', []],
3133440a8c0SAndreas Gohr            ['p_open', []],
3143440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
3153440a8c0SAndreas Gohr            ['internallink', ['test:link', $this->mediaArray([])]],
3163440a8c0SAndreas Gohr            ['cdata', [' Bar']],
3173440a8c0SAndreas Gohr            ['p_close', []],
3183440a8c0SAndreas Gohr            ['document_end', []],
3193440a8c0SAndreas Gohr        ];
3203440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
3213440a8c0SAndreas Gohr    }
3223440a8c0SAndreas Gohr
3233440a8c0SAndreas Gohr    function testImageAsLabelExternalLink()
3243440a8c0SAndreas Gohr    {
3253440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3263440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png)](http://example.com) Bar');
3273440a8c0SAndreas Gohr        $calls = [
3283440a8c0SAndreas Gohr            ['document_start', []],
3293440a8c0SAndreas Gohr            ['p_open', []],
3303440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
3313440a8c0SAndreas Gohr            ['externallink', ['http://example.com', $this->mediaArray([])]],
3323440a8c0SAndreas Gohr            ['cdata', [' Bar']],
3333440a8c0SAndreas Gohr            ['p_close', []],
3343440a8c0SAndreas Gohr            ['document_end', []],
3353440a8c0SAndreas Gohr        ];
3363440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
3373440a8c0SAndreas Gohr    }
3383440a8c0SAndreas Gohr
3393440a8c0SAndreas Gohr    function testImageAsLabelWithExternalMedia()
3403440a8c0SAndreas Gohr    {
3413440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3423440a8c0SAndreas Gohr        $this->P->parse('Foo [![logo](https://example.com/logo.png)](test:link) Bar');
3433440a8c0SAndreas Gohr        $calls = [
3443440a8c0SAndreas Gohr            ['document_start', []],
3453440a8c0SAndreas Gohr            ['p_open', []],
3463440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
3473440a8c0SAndreas Gohr            ['internallink', ['test:link', $this->mediaArray([
3483440a8c0SAndreas Gohr                'type'  => 'externalmedia',
3493440a8c0SAndreas Gohr                'src'   => 'https://example.com/logo.png',
3503440a8c0SAndreas Gohr                'title' => 'logo',
3513440a8c0SAndreas Gohr            ])]],
3523440a8c0SAndreas Gohr            ['cdata', [' Bar']],
3533440a8c0SAndreas Gohr            ['p_close', []],
3543440a8c0SAndreas Gohr            ['document_end', []],
3553440a8c0SAndreas Gohr        ];
3563440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
3573440a8c0SAndreas Gohr    }
3583440a8c0SAndreas Gohr
3593440a8c0SAndreas Gohr    function testImageAsLabelInterwikiLink()
3603440a8c0SAndreas Gohr    {
3613440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3623440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png)](wp>Example) Bar');
3633440a8c0SAndreas Gohr        $calls = [
3643440a8c0SAndreas Gohr            ['document_start', []],
3653440a8c0SAndreas Gohr            ['p_open', []],
3663440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
3673440a8c0SAndreas Gohr            ['interwikilink', ['wp>Example', $this->mediaArray([]), 'wp', 'Example']],
3683440a8c0SAndreas Gohr            ['cdata', [' Bar']],
3693440a8c0SAndreas Gohr            ['p_close', []],
3703440a8c0SAndreas Gohr            ['document_end', []],
3713440a8c0SAndreas Gohr        ];
3723440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
3733440a8c0SAndreas Gohr    }
3743440a8c0SAndreas Gohr
3753440a8c0SAndreas Gohr    function testImageAsLabelEmailLink()
3763440a8c0SAndreas Gohr    {
3773440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3783440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png)](user@example.com) Bar');
3793440a8c0SAndreas Gohr        $calls = [
3803440a8c0SAndreas Gohr            ['document_start', []],
3813440a8c0SAndreas Gohr            ['p_open', []],
3823440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
3833440a8c0SAndreas Gohr            ['emaillink', ['user@example.com', $this->mediaArray([])]],
3843440a8c0SAndreas Gohr            ['cdata', [' Bar']],
3853440a8c0SAndreas Gohr            ['p_close', []],
3863440a8c0SAndreas Gohr            ['document_end', []],
3873440a8c0SAndreas Gohr        ];
3883440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
3893440a8c0SAndreas Gohr    }
3903440a8c0SAndreas Gohr
3913440a8c0SAndreas Gohr    function testImageAsLabelMediaParameters()
3923440a8c0SAndreas Gohr    {
3933440a8c0SAndreas Gohr        // Full DW parameter vocabulary works in the nested image slot.
3943440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
3953440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png?200x100&right&nolink)](test:link) Bar');
3963440a8c0SAndreas Gohr        $calls = [
3973440a8c0SAndreas Gohr            ['document_start', []],
3983440a8c0SAndreas Gohr            ['p_open', []],
3993440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
4003440a8c0SAndreas Gohr            ['internallink', ['test:link', $this->mediaArray([
4013440a8c0SAndreas Gohr                'align'   => 'right',
4023440a8c0SAndreas Gohr                'width'   => '200',
4033440a8c0SAndreas Gohr                'height'  => '100',
4043440a8c0SAndreas Gohr                'linking' => 'nolink',
4053440a8c0SAndreas Gohr            ])]],
4063440a8c0SAndreas Gohr            ['cdata', [' Bar']],
4073440a8c0SAndreas Gohr            ['p_close', []],
4083440a8c0SAndreas Gohr            ['document_end', []],
4093440a8c0SAndreas Gohr        ];
4103440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
4113440a8c0SAndreas Gohr    }
4123440a8c0SAndreas Gohr
4133440a8c0SAndreas Gohr    function testImageAsLabelEmptyAlt()
4143440a8c0SAndreas Gohr    {
4153440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
4163440a8c0SAndreas Gohr        $this->P->parse('Foo [![](wiki:image.png)](test:link) Bar');
4173440a8c0SAndreas Gohr        $calls = [
4183440a8c0SAndreas Gohr            ['document_start', []],
4193440a8c0SAndreas Gohr            ['p_open', []],
4203440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
4213440a8c0SAndreas Gohr            ['internallink', ['test:link', $this->mediaArray(['title' => null])]],
4223440a8c0SAndreas Gohr            ['cdata', [' Bar']],
4233440a8c0SAndreas Gohr            ['p_close', []],
4243440a8c0SAndreas Gohr            ['document_end', []],
4253440a8c0SAndreas Gohr        ];
4263440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
4273440a8c0SAndreas Gohr    }
4283440a8c0SAndreas Gohr
4293440a8c0SAndreas Gohr    function testImageAsLabelBothTitlesDiscarded()
4303440a8c0SAndreas Gohr    {
4313440a8c0SAndreas Gohr        // Titles on both URLs parse cleanly but are dropped — neither
4323440a8c0SAndreas Gohr        // DW's media nor link instructions have a title-attribute slot.
4333440a8c0SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
4343440a8c0SAndreas Gohr        $this->P->parse('Foo [![alt](wiki:image.png "img title")](test:link "link title") Bar');
4353440a8c0SAndreas Gohr        $calls = [
4363440a8c0SAndreas Gohr            ['document_start', []],
4373440a8c0SAndreas Gohr            ['p_open', []],
4383440a8c0SAndreas Gohr            ['cdata', ["\nFoo "]],
4393440a8c0SAndreas Gohr            ['internallink', ['test:link', $this->mediaArray([])]],
4403440a8c0SAndreas Gohr            ['cdata', [' Bar']],
4413440a8c0SAndreas Gohr            ['p_close', []],
4423440a8c0SAndreas Gohr            ['document_end', []],
4433440a8c0SAndreas Gohr        ];
4443440a8c0SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
4453440a8c0SAndreas Gohr    }
4463440a8c0SAndreas Gohr
44774031e46SAndreas Gohr    // ----- backslash-escape interaction (GFM §6.1) -----
44874031e46SAndreas Gohr
44974031e46SAndreas Gohr    function testBackslashEscapesInLabel()
45074031e46SAndreas Gohr    {
45174031e46SAndreas Gohr        // Plain-text label gets §6.1 unescape applied before it reaches
45274031e46SAndreas Gohr        // the link handler — `\*` collapses to a literal `*`.
45374031e46SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
45474031e46SAndreas Gohr        $this->P->parse('Foo [te\\*xt](page) Bar');
45574031e46SAndreas Gohr        $calls = [
45674031e46SAndreas Gohr            ['document_start', []],
45774031e46SAndreas Gohr            ['p_open', []],
45874031e46SAndreas Gohr            ['cdata', ["\nFoo "]],
45974031e46SAndreas Gohr            ['internallink', ['page', 'te*xt']],
46074031e46SAndreas Gohr            ['cdata', [' Bar']],
46174031e46SAndreas Gohr            ['p_close', []],
46274031e46SAndreas Gohr            ['document_end', []],
46374031e46SAndreas Gohr        ];
46474031e46SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
46574031e46SAndreas Gohr    }
46674031e46SAndreas Gohr
4670f694376SAndreas Gohr    function testBackslashEscapedBracketInLabel()
4680f694376SAndreas Gohr    {
4690f694376SAndreas Gohr        // Spec example #523: an escaped `[` inside the label is allowed
4700f694376SAndreas Gohr        // and unescapes to a literal bracket. The label class accepts
4710f694376SAndreas Gohr        // `\[` / `\]` so the outer match still finds its `]` close.
4720f694376SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
4730f694376SAndreas Gohr        $this->P->parse('Foo [link \\[bar](page) Baz');
4740f694376SAndreas Gohr        $calls = [
4750f694376SAndreas Gohr            ['document_start', []],
4760f694376SAndreas Gohr            ['p_open', []],
4770f694376SAndreas Gohr            ['cdata', ["\nFoo "]],
4780f694376SAndreas Gohr            ['internallink', ['page', 'link [bar']],
4790f694376SAndreas Gohr            ['cdata', [' Baz']],
4800f694376SAndreas Gohr            ['p_close', []],
4810f694376SAndreas Gohr            ['document_end', []],
4820f694376SAndreas Gohr        ];
4830f694376SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
4840f694376SAndreas Gohr    }
4850f694376SAndreas Gohr
4860f694376SAndreas Gohr    function testBackslashEscapedClosingBracketInLabel()
4870f694376SAndreas Gohr    {
4880f694376SAndreas Gohr        // The `\]` form is symmetric with `\[`. Both must be accepted by
4890f694376SAndreas Gohr        // the label class without ending the outer match early.
4900f694376SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
4910f694376SAndreas Gohr        $this->P->parse('Foo [a\\]b](page) Bar');
4920f694376SAndreas Gohr        $calls = [
4930f694376SAndreas Gohr            ['document_start', []],
4940f694376SAndreas Gohr            ['p_open', []],
4950f694376SAndreas Gohr            ['cdata', ["\nFoo "]],
4960f694376SAndreas Gohr            ['internallink', ['page', 'a]b']],
4970f694376SAndreas Gohr            ['cdata', [' Bar']],
4980f694376SAndreas Gohr            ['p_close', []],
4990f694376SAndreas Gohr            ['document_end', []],
5000f694376SAndreas Gohr        ];
5010f694376SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
5020f694376SAndreas Gohr    }
5030f694376SAndreas Gohr
50474031e46SAndreas Gohr    function testBackslashEscapesInUrl()
50574031e46SAndreas Gohr    {
50674031e46SAndreas Gohr        // §6.1 unescape fires on the URL after classify() picks the
50774031e46SAndreas Gohr        // handler — it lets users put a literal punctuation char in a
50874031e46SAndreas Gohr        // URL slot that would otherwise carry markup meaning.
50974031e46SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
51074031e46SAndreas Gohr        $this->P->parse('Foo [text](http://example.com/pa\\!ge) Bar');
51174031e46SAndreas Gohr        $calls = [
51274031e46SAndreas Gohr            ['document_start', []],
51374031e46SAndreas Gohr            ['p_open', []],
51474031e46SAndreas Gohr            ['cdata', ["\nFoo "]],
51574031e46SAndreas Gohr            ['externallink', ['http://example.com/pa!ge', 'text']],
51674031e46SAndreas Gohr            ['cdata', [' Bar']],
51774031e46SAndreas Gohr            ['p_close', []],
51874031e46SAndreas Gohr            ['document_end', []],
51974031e46SAndreas Gohr        ];
52074031e46SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
52174031e46SAndreas Gohr    }
52274031e46SAndreas Gohr
52374031e46SAndreas Gohr    function testWindowsShareUrlSkipsBackslashUnescape()
52474031e46SAndreas Gohr    {
52574031e46SAndreas Gohr        // Carve-out: a `\\host\path` URL must survive classify() and
52674031e46SAndreas Gohr        // stay intact as a windowssharelink. Applying §6.1 unescape
52774031e46SAndreas Gohr        // would collapse the leading `\\` to `\` and destroy the share
52874031e46SAndreas Gohr        // marker, so the unescape pass is skipped for this classifier.
52974031e46SAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
53074031e46SAndreas Gohr        $this->P->parse('Foo [share](\\\\server\\share\\sub) Bar');
53174031e46SAndreas Gohr        $calls = [
53274031e46SAndreas Gohr            ['document_start', []],
53374031e46SAndreas Gohr            ['p_open', []],
53474031e46SAndreas Gohr            ['cdata', ["\nFoo "]],
53574031e46SAndreas Gohr            ['windowssharelink', ['\\\\server\\share\\sub', 'share']],
53674031e46SAndreas Gohr            ['cdata', [' Bar']],
53774031e46SAndreas Gohr            ['p_close', []],
53874031e46SAndreas Gohr            ['document_end', []],
53974031e46SAndreas Gohr        ];
54074031e46SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
54174031e46SAndreas Gohr    }
54274031e46SAndreas Gohr
543*4f32c45bSAndreas Gohr    function testSoftLineBreakInLabel()
544*4f32c45bSAndreas Gohr    {
545*4f32c45bSAndreas Gohr        // CommonMark allows a soft line break inside link text. The `\n`
546*4f32c45bSAndreas Gohr        // is preserved in the label string and rendered as a space by
547*4f32c45bSAndreas Gohr        // HTML; the link still resolves to a single externallink call.
548*4f32c45bSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
549*4f32c45bSAndreas Gohr        $this->P->parse("A [link with\na newline](http://example.org)?");
550*4f32c45bSAndreas Gohr        $calls = [
551*4f32c45bSAndreas Gohr            ['document_start', []],
552*4f32c45bSAndreas Gohr            ['p_open', []],
553*4f32c45bSAndreas Gohr            ['cdata', ["\nA "]],
554*4f32c45bSAndreas Gohr            ['externallink', ['http://example.org', "link with\na newline"]],
555*4f32c45bSAndreas Gohr            ['cdata', ['?']],
556*4f32c45bSAndreas Gohr            ['p_close', []],
557*4f32c45bSAndreas Gohr            ['document_end', []],
558*4f32c45bSAndreas Gohr        ];
559*4f32c45bSAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
560*4f32c45bSAndreas Gohr    }
561*4f32c45bSAndreas Gohr
562*4f32c45bSAndreas Gohr    function testBlankLineEndsLabel()
563*4f32c45bSAndreas Gohr    {
564*4f32c45bSAndreas Gohr        // A blank line is not allowed inside link text — the regex
565*4f32c45bSAndreas Gohr        // declines to cross it, so the bracket sequence stays literal.
566*4f32c45bSAndreas Gohr        $this->P->addMode('gfm_link', new GfmLink());
567*4f32c45bSAndreas Gohr        $this->P->parse("[link with\n\nblank line](http://example.org)");
568*4f32c45bSAndreas Gohr        $modes = array_column($this->H->calls, 0);
569*4f32c45bSAndreas Gohr        $this->assertNotContains('externallink', $modes);
570*4f32c45bSAndreas Gohr        $this->assertNotContains('internallink', $modes);
571*4f32c45bSAndreas Gohr    }
572*4f32c45bSAndreas Gohr
573e89aeebdSAndreas Gohr    function testSortValue()
574e89aeebdSAndreas Gohr    {
575e89aeebdSAndreas Gohr        $this->assertSame(300, (new GfmLink())->getSort());
576e89aeebdSAndreas Gohr    }
577e89aeebdSAndreas Gohr}
578