xref: /dokuwiki/_test/tests/Parsing/ParserMode/ExternallinkTest.php (revision 47a02a102092be9e1e6f1ddaf158bdfffdb13d4f)
1465aec67SAndreas Gohr<?php
2465aec67SAndreas Gohr
3465aec67SAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode;
4465aec67SAndreas Gohr
5465aec67SAndreas Gohruse dokuwiki\Parsing\ParserMode\Externallink;
6465aec67SAndreas Gohruse dokuwiki\Parsing\ParserMode\Internallink;
7465aec67SAndreas Gohr
8465aec67SAndreas Gohr/**
9465aec67SAndreas Gohr * Tests for the {@see Externallink} parser mode.
10465aec67SAndreas Gohr *
11465aec67SAndreas Gohr * Covers the classic DokuWiki autolink behavior (bare URLs, www./ftp. shortcuts, IPv4/IPv6,
12465aec67SAndreas Gohr * scheme allow-listing), the Markdown angle-bracket autolink form (CommonMark §6.5), and the
13465aec67SAndreas Gohr * GFM autolink extension trim step (paren balancing, trailing entity-ref decoding).
14465aec67SAndreas Gohr *
15465aec67SAndreas Gohr * @group parser_links
16465aec67SAndreas Gohr */
17465aec67SAndreas Gohrclass ExternallinkTest extends ParserTestBase
18465aec67SAndreas Gohr{
19465aec67SAndreas Gohr    public function setUp(): void
20465aec67SAndreas Gohr    {
21465aec67SAndreas Gohr        parent::setUp();
22*47a02a10SAndreas Gohr        $this->setSyntax('md');
23465aec67SAndreas Gohr    }
24465aec67SAndreas Gohr
25465aec67SAndreas Gohr    // ----- basic bare-URL autolink -----
26465aec67SAndreas Gohr
27465aec67SAndreas Gohr    function testSimple() {
28465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
29465aec67SAndreas Gohr        $this->P->parse("Foo http://www.google.com Bar");
30465aec67SAndreas Gohr        $calls = [
31465aec67SAndreas Gohr            ['document_start', []],
32465aec67SAndreas Gohr            ['p_open', []],
33465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
34465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', null]],
35465aec67SAndreas Gohr            ['cdata', [' Bar']],
36465aec67SAndreas Gohr            ['p_close', []],
37465aec67SAndreas Gohr            ['document_end', []],
38465aec67SAndreas Gohr        ];
39465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
40465aec67SAndreas Gohr    }
41465aec67SAndreas Gohr
42465aec67SAndreas Gohr    function testCase() {
43465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
44465aec67SAndreas Gohr        $this->P->parse("Foo HTTP://WWW.GOOGLE.COM Bar");
45465aec67SAndreas Gohr        $calls = [
46465aec67SAndreas Gohr            ['document_start', []],
47465aec67SAndreas Gohr            ['p_open', []],
48465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
49465aec67SAndreas Gohr            ['externallink', ['HTTP://WWW.GOOGLE.COM', null]],
50465aec67SAndreas Gohr            ['cdata', [' Bar']],
51465aec67SAndreas Gohr            ['p_close', []],
52465aec67SAndreas Gohr            ['document_end', []],
53465aec67SAndreas Gohr        ];
54465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
55465aec67SAndreas Gohr    }
56465aec67SAndreas Gohr
57465aec67SAndreas Gohr    function testIPv4() {
58465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
59465aec67SAndreas Gohr        $this->P->parse("Foo http://123.123.3.21/foo Bar");
60465aec67SAndreas Gohr        $calls = [
61465aec67SAndreas Gohr            ['document_start', []],
62465aec67SAndreas Gohr            ['p_open', []],
63465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
64465aec67SAndreas Gohr            ['externallink', ['http://123.123.3.21/foo', null]],
65465aec67SAndreas Gohr            ['cdata', [' Bar']],
66465aec67SAndreas Gohr            ['p_close', []],
67465aec67SAndreas Gohr            ['document_end', []],
68465aec67SAndreas Gohr        ];
69465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
70465aec67SAndreas Gohr    }
71465aec67SAndreas Gohr
72465aec67SAndreas Gohr    function testIPv6() {
73465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
74465aec67SAndreas Gohr        $this->P->parse("Foo http://[3ffe:2a00:100:7031::1]/foo Bar");
75465aec67SAndreas Gohr        $calls = [
76465aec67SAndreas Gohr            ['document_start', []],
77465aec67SAndreas Gohr            ['p_open', []],
78465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
79465aec67SAndreas Gohr            ['externallink', ['http://[3ffe:2a00:100:7031::1]/foo', null]],
80465aec67SAndreas Gohr            ['cdata', [' Bar']],
81465aec67SAndreas Gohr            ['p_close', []],
82465aec67SAndreas Gohr            ['document_end', []],
83465aec67SAndreas Gohr        ];
84465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
85465aec67SAndreas Gohr    }
86465aec67SAndreas Gohr
87465aec67SAndreas Gohr    function testMulti() {
88465aec67SAndreas Gohr        $this->teardown();
89465aec67SAndreas Gohr
90465aec67SAndreas Gohr        $links = [
91465aec67SAndreas Gohr            'http://www.google.com',
92465aec67SAndreas Gohr            'HTTP://WWW.GOOGLE.COM',
93465aec67SAndreas Gohr            'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
94465aec67SAndreas Gohr            'http://[1080:0:0:0:8:800:200C:417A]/index.html',
95465aec67SAndreas Gohr            'http://[3ffe:2a00:100:7031::1]',
96465aec67SAndreas Gohr            'http://[1080::8:800:200C:417A]/foo',
97465aec67SAndreas Gohr            'http://[::192.9.5.5]/ipng',
98465aec67SAndreas Gohr            'http://[::FFFF:129.144.52.38]:80/index.html',
99465aec67SAndreas Gohr            'http://[2010:836B:4179::836B:4179]',
100465aec67SAndreas Gohr        ];
101465aec67SAndreas Gohr        $titles = [false, null, 'foo bar'];
102465aec67SAndreas Gohr        foreach ($links as $link) {
103465aec67SAndreas Gohr            foreach ($titles as $title) {
104465aec67SAndreas Gohr                if ($title === false) {
105465aec67SAndreas Gohr                    $source = $link;
106465aec67SAndreas Gohr                    $name = null;
107465aec67SAndreas Gohr                } elseif ($title === null) {
108465aec67SAndreas Gohr                    $source = "[[$link]]";
109465aec67SAndreas Gohr                    $name = null;
110465aec67SAndreas Gohr                } else {
111465aec67SAndreas Gohr                    $source = "[[$link|$title]]";
112465aec67SAndreas Gohr                    $name = $title;
113465aec67SAndreas Gohr                }
114465aec67SAndreas Gohr                $this->setup();
115465aec67SAndreas Gohr                $this->P->addMode('internallink', new Internallink());
116465aec67SAndreas Gohr                $this->P->addMode('externallink', new Externallink());
117465aec67SAndreas Gohr                $this->P->parse("Foo $source Bar");
118465aec67SAndreas Gohr                $calls = [
119465aec67SAndreas Gohr                    ['document_start', []],
120465aec67SAndreas Gohr                    ['p_open', []],
121465aec67SAndreas Gohr                    ['cdata', ["\n" . 'Foo ']],
122465aec67SAndreas Gohr                    ['externallink', [$link, $name]],
123465aec67SAndreas Gohr                    ['cdata', [' Bar']],
124465aec67SAndreas Gohr                    ['p_close', []],
125465aec67SAndreas Gohr                    ['document_end', []],
126465aec67SAndreas Gohr                ];
127465aec67SAndreas Gohr                $this->assertCalls($calls, $this->H->calls, $source);
128465aec67SAndreas Gohr                $this->teardown();
129465aec67SAndreas Gohr            }
130465aec67SAndreas Gohr        }
131465aec67SAndreas Gohr
132465aec67SAndreas Gohr        $this->setup();
133465aec67SAndreas Gohr    }
134465aec67SAndreas Gohr
135465aec67SAndreas Gohr    function testJavascriptScheme() {
136465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
137465aec67SAndreas Gohr        $this->P->parse("Foo javascript:alert('XSS'); Bar");
138465aec67SAndreas Gohr        $calls = [
139465aec67SAndreas Gohr            ['document_start', []],
140465aec67SAndreas Gohr            ['p_open', []],
141465aec67SAndreas Gohr            ['cdata', ["\nFoo javascript:alert('XSS'); Bar"]],
142465aec67SAndreas Gohr            ['p_close', []],
143465aec67SAndreas Gohr            ['document_end', []],
144465aec67SAndreas Gohr        ];
145465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
146465aec67SAndreas Gohr    }
147465aec67SAndreas Gohr
148465aec67SAndreas Gohr    // ----- www. / ftp. shortcuts -----
149465aec67SAndreas Gohr
150465aec67SAndreas Gohr    function testWWWLink() {
151465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
152465aec67SAndreas Gohr        $this->P->parse("Foo www.google.com Bar");
153465aec67SAndreas Gohr        $calls = [
154465aec67SAndreas Gohr            ['document_start', []],
155465aec67SAndreas Gohr            ['p_open', []],
156465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
157465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
158465aec67SAndreas Gohr            ['cdata', [' Bar']],
159465aec67SAndreas Gohr            ['p_close', []],
160465aec67SAndreas Gohr            ['document_end', []],
161465aec67SAndreas Gohr        ];
162465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
163465aec67SAndreas Gohr    }
164465aec67SAndreas Gohr
165465aec67SAndreas Gohr    function testWWWLinkStartOfLine() {
166465aec67SAndreas Gohr        // Regression test for issue #2399
167465aec67SAndreas Gohr        $calls = [
168465aec67SAndreas Gohr            ['document_start', []],
169465aec67SAndreas Gohr            ['p_open', []],
170465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
171465aec67SAndreas Gohr            ['cdata', [' Bar']],
172465aec67SAndreas Gohr            ['p_close', []],
173465aec67SAndreas Gohr            ['document_end', []],
174465aec67SAndreas Gohr        ];
175465aec67SAndreas Gohr        $instructions = p_get_instructions("www.google.com Bar");
176465aec67SAndreas Gohr        $this->assertCalls($calls, $instructions);
177465aec67SAndreas Gohr    }
178465aec67SAndreas Gohr
179465aec67SAndreas Gohr    function testWWWLinkInRoundBrackets() {
180465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
181465aec67SAndreas Gohr        $this->P->parse("Foo (www.google.com) Bar");
182465aec67SAndreas Gohr        $calls = [
183465aec67SAndreas Gohr            ['document_start', []],
184465aec67SAndreas Gohr            ['p_open', []],
185465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo (']],
186465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
187465aec67SAndreas Gohr            ['cdata', [') Bar']],
188465aec67SAndreas Gohr            ['p_close', []],
189465aec67SAndreas Gohr            ['document_end', []],
190465aec67SAndreas Gohr        ];
191465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
192465aec67SAndreas Gohr    }
193465aec67SAndreas Gohr
194465aec67SAndreas Gohr    function testWWWLinkInPath() {
195465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
196465aec67SAndreas Gohr        // See issue #936. Should NOT generate a link!
197465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/www.something.de/somedir/ Bar");
198465aec67SAndreas Gohr        $calls = [
199465aec67SAndreas Gohr            ['document_start', []],
200465aec67SAndreas Gohr            ['p_open', []],
201465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/www.something.de/somedir/ Bar']],
202465aec67SAndreas Gohr            ['p_close', []],
203465aec67SAndreas Gohr            ['document_end', []],
204465aec67SAndreas Gohr        ];
205465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
206465aec67SAndreas Gohr    }
207465aec67SAndreas Gohr
208465aec67SAndreas Gohr    function testWWWLinkFollowingPath() {
209465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
210465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ www.something.de/somedir/ Bar");
211465aec67SAndreas Gohr        $calls = [
212465aec67SAndreas Gohr            ['document_start', []],
213465aec67SAndreas Gohr            ['p_open', []],
214465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ ']],
215465aec67SAndreas Gohr            ['externallink', ['http://www.something.de/somedir/', 'www.something.de/somedir/']],
216465aec67SAndreas Gohr            ['cdata', [' Bar']],
217465aec67SAndreas Gohr            ['p_close', []],
218465aec67SAndreas Gohr            ['document_end', []],
219465aec67SAndreas Gohr        ];
220465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
221465aec67SAndreas Gohr    }
222465aec67SAndreas Gohr
223465aec67SAndreas Gohr    function testFTPLink() {
224465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
225465aec67SAndreas Gohr        $this->P->parse("Foo ftp.sunsite.com Bar");
226465aec67SAndreas Gohr        $calls = [
227465aec67SAndreas Gohr            ['document_start', []],
228465aec67SAndreas Gohr            ['p_open', []],
229465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
230465aec67SAndreas Gohr            ['externallink', ['ftp://ftp.sunsite.com', 'ftp.sunsite.com']],
231465aec67SAndreas Gohr            ['cdata', [' Bar']],
232465aec67SAndreas Gohr            ['p_close', []],
233465aec67SAndreas Gohr            ['document_end', []],
234465aec67SAndreas Gohr        ];
235465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
236465aec67SAndreas Gohr    }
237465aec67SAndreas Gohr
238465aec67SAndreas Gohr    function testFTPLinkInPath() {
239465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
240465aec67SAndreas Gohr        // See issue #936. Should NOT generate a link!
241465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ftp.something.de/somedir/ Bar");
242465aec67SAndreas Gohr        $calls = [
243465aec67SAndreas Gohr            ['document_start', []],
244465aec67SAndreas Gohr            ['p_open', []],
245465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ftp.something.de/somedir/ Bar']],
246465aec67SAndreas Gohr            ['p_close', []],
247465aec67SAndreas Gohr            ['document_end', []],
248465aec67SAndreas Gohr        ];
249465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
250465aec67SAndreas Gohr    }
251465aec67SAndreas Gohr
252465aec67SAndreas Gohr    function testFTPLinkFollowingPath() {
253465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
254465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ ftp.something.de/somedir/ Bar");
255465aec67SAndreas Gohr        $calls = [
256465aec67SAndreas Gohr            ['document_start', []],
257465aec67SAndreas Gohr            ['p_open', []],
258465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ ']],
259465aec67SAndreas Gohr            ['externallink', ['ftp://ftp.something.de/somedir/', 'ftp.something.de/somedir/']],
260465aec67SAndreas Gohr            ['cdata', [' Bar']],
261465aec67SAndreas Gohr            ['p_close', []],
262465aec67SAndreas Gohr            ['document_end', []],
263465aec67SAndreas Gohr        ];
264465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
265465aec67SAndreas Gohr    }
266465aec67SAndreas Gohr
267465aec67SAndreas Gohr    // ----- Markdown angle-bracket autolinks (§6.5) -----
268465aec67SAndreas Gohr
269465aec67SAndreas Gohr    function testAngleBracketAutolink() {
270465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
271465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com> Bar");
272465aec67SAndreas Gohr        $calls = [
273465aec67SAndreas Gohr            ['document_start', []],
274465aec67SAndreas Gohr            ['p_open', []],
275465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
276465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'http://www.google.com']],
277465aec67SAndreas Gohr            ['cdata', [' Bar']],
278465aec67SAndreas Gohr            ['p_close', []],
279465aec67SAndreas Gohr            ['document_end', []],
280465aec67SAndreas Gohr        ];
281465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
282465aec67SAndreas Gohr    }
283465aec67SAndreas Gohr
284465aec67SAndreas Gohr    function testAngleBracketDisqualifiedByInternalWhitespace() {
285465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
286465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com bim> Bar");
287465aec67SAndreas Gohr        // Internal whitespace disqualifies the autolink. The whole envelope is consumed as cdata so the
288465aec67SAndreas Gohr        // bare-URL detector cannot pick up http://www.google.com inside and leave dangling brackets.
289465aec67SAndreas Gohr        $calls = [
290465aec67SAndreas Gohr            ['document_start', []],
291465aec67SAndreas Gohr            ['p_open', []],
292465aec67SAndreas Gohr            ['cdata', ["\nFoo <http://www.google.com bim> Bar"]],
293465aec67SAndreas Gohr            ['p_close', []],
294465aec67SAndreas Gohr            ['document_end', []],
295465aec67SAndreas Gohr        ];
296465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
297465aec67SAndreas Gohr    }
298465aec67SAndreas Gohr
299465aec67SAndreas Gohr    function testAngleBracketDisqualifiedByLeadingWhitespace() {
300465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
301465aec67SAndreas Gohr        $this->P->parse("Foo < http://www.google.com > Bar");
302465aec67SAndreas Gohr        $calls = [
303465aec67SAndreas Gohr            ['document_start', []],
304465aec67SAndreas Gohr            ['p_open', []],
305465aec67SAndreas Gohr            ['cdata', ["\nFoo < http://www.google.com > Bar"]],
306465aec67SAndreas Gohr            ['p_close', []],
307465aec67SAndreas Gohr            ['document_end', []],
308465aec67SAndreas Gohr        ];
309465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
310465aec67SAndreas Gohr    }
311465aec67SAndreas Gohr
312465aec67SAndreas Gohr    function testAngleBracketUnregisteredScheme() {
313465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
314465aec67SAndreas Gohr        // mailto is not in the default conf/scheme.conf allow-list, so no per-scheme angle pattern is built
315465aec67SAndreas Gohr        // for it. The brackets fall through to cdata, matching DokuWiki's bare-URL scheme policy.
316465aec67SAndreas Gohr        $this->P->parse("Foo <mailto:foo@example.com> Bar");
317465aec67SAndreas Gohr        $calls = [
318465aec67SAndreas Gohr            ['document_start', []],
319465aec67SAndreas Gohr            ['p_open', []],
320465aec67SAndreas Gohr            ['cdata', ["\nFoo <mailto:foo@example.com> Bar"]],
321465aec67SAndreas Gohr            ['p_close', []],
322465aec67SAndreas Gohr            ['document_end', []],
323465aec67SAndreas Gohr        ];
324465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
325465aec67SAndreas Gohr    }
326465aec67SAndreas Gohr
327465aec67SAndreas Gohr    function testAngleBracketInactiveInDwMode() {
328*47a02a10SAndreas Gohr        $this->setSyntax('dw');
329465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
330465aec67SAndreas Gohr        // In DW-only syntax, angle-bracket processing is intentionally not active. The bare-URL pattern still
331465aec67SAndreas Gohr        // picks up the URL inside and the angle brackets fall through as literal text — matches the pre-merge
332465aec67SAndreas Gohr        // behavior of DokuWiki's Externallink mode.
333465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com> Bar");
334465aec67SAndreas Gohr        $calls = [
335465aec67SAndreas Gohr            ['document_start', []],
336465aec67SAndreas Gohr            ['p_open', []],
337465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo <']],
338465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', null]],
339465aec67SAndreas Gohr            ['cdata', ['> Bar']],
340465aec67SAndreas Gohr            ['p_close', []],
341465aec67SAndreas Gohr            ['document_end', []],
342465aec67SAndreas Gohr        ];
343465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
344465aec67SAndreas Gohr    }
345465aec67SAndreas Gohr
346465aec67SAndreas Gohr    // ----- GFM autolink extension: paren balancing -----
347465aec67SAndreas Gohr
348465aec67SAndreas Gohr    function testBalancedParensInUrl() {
349465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
350465aec67SAndreas Gohr        $this->P->parse('See www.example.com/path(foo) end');
351465aec67SAndreas Gohr        $calls = [
352465aec67SAndreas Gohr            ['document_start', []],
353465aec67SAndreas Gohr            ['p_open', []],
354465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
355465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
356465aec67SAndreas Gohr            ['cdata', [' end']],
357465aec67SAndreas Gohr            ['p_close', []],
358465aec67SAndreas Gohr            ['document_end', []],
359465aec67SAndreas Gohr        ];
360465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
361465aec67SAndreas Gohr    }
362465aec67SAndreas Gohr
363465aec67SAndreas Gohr    function testTrailingUnbalancedParenExcluded() {
364465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
365465aec67SAndreas Gohr        $this->P->parse('See (www.example.com/path(foo)) end');
366465aec67SAndreas Gohr        $calls = [
367465aec67SAndreas Gohr            ['document_start', []],
368465aec67SAndreas Gohr            ['p_open', []],
369465aec67SAndreas Gohr            ['cdata', ["\nSee ("]],
370465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
371465aec67SAndreas Gohr            ['cdata', [') end']],
372465aec67SAndreas Gohr            ['p_close', []],
373465aec67SAndreas Gohr            ['document_end', []],
374465aec67SAndreas Gohr        ];
375465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
376465aec67SAndreas Gohr    }
377465aec67SAndreas Gohr
378465aec67SAndreas Gohr    function testMultipleTrailingParensTrimmedUntilBalanced() {
379465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
380465aec67SAndreas Gohr        // Inner `(foo)` is balanced and stays inside the URL; the two unbalanced trailing `)` are peeled off.
381465aec67SAndreas Gohr        $this->P->parse('See www.example.com/path(foo))) end');
382465aec67SAndreas Gohr        $calls = [
383465aec67SAndreas Gohr            ['document_start', []],
384465aec67SAndreas Gohr            ['p_open', []],
385465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
386465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
387465aec67SAndreas Gohr            ['cdata', [')) end']],
388465aec67SAndreas Gohr            ['p_close', []],
389465aec67SAndreas Gohr            ['document_end', []],
390465aec67SAndreas Gohr        ];
391465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
392465aec67SAndreas Gohr    }
393465aec67SAndreas Gohr
394465aec67SAndreas Gohr    function testParenInsideUrlNoTrailing() {
395465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
396465aec67SAndreas Gohr        $this->P->parse('See www.example.com/search?q=(business))+ok end');
397465aec67SAndreas Gohr        $calls = [
398465aec67SAndreas Gohr            ['document_start', []],
399465aec67SAndreas Gohr            ['p_open', []],
400465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
401465aec67SAndreas Gohr            ['externallink', [
402465aec67SAndreas Gohr                'http://www.example.com/search?q=(business))+ok',
403465aec67SAndreas Gohr                'www.example.com/search?q=(business))+ok'
404465aec67SAndreas Gohr            ]],
405465aec67SAndreas Gohr            ['cdata', [' end']],
406465aec67SAndreas Gohr            ['p_close', []],
407465aec67SAndreas Gohr            ['document_end', []],
408465aec67SAndreas Gohr        ];
409465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
410465aec67SAndreas Gohr    }
411465aec67SAndreas Gohr
412465aec67SAndreas Gohr    // ----- GFM autolink extension: trailing entity references -----
413465aec67SAndreas Gohr
414465aec67SAndreas Gohr    function testTrailingValidEntityDecodedToUnicode() {
415465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
416465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&copy; end');
417465aec67SAndreas Gohr        $calls = [
418465aec67SAndreas Gohr            ['document_start', []],
419465aec67SAndreas Gohr            ['p_open', []],
420465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
421465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
422465aec67SAndreas Gohr            ['cdata', ['© end']],
423465aec67SAndreas Gohr            ['p_close', []],
424465aec67SAndreas Gohr            ['document_end', []],
425465aec67SAndreas Gohr        ];
426465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
427465aec67SAndreas Gohr    }
428465aec67SAndreas Gohr
429465aec67SAndreas Gohr    function testTrailingUnknownEntityRoundTripsLiterally() {
430465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
431465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&hl; end');
432465aec67SAndreas Gohr        $calls = [
433465aec67SAndreas Gohr            ['document_start', []],
434465aec67SAndreas Gohr            ['p_open', []],
435465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
436465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
437465aec67SAndreas Gohr            ['cdata', ['&hl; end']],
438465aec67SAndreas Gohr            ['p_close', []],
439465aec67SAndreas Gohr            ['document_end', []],
440465aec67SAndreas Gohr        ];
441465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
442465aec67SAndreas Gohr    }
443465aec67SAndreas Gohr
444465aec67SAndreas Gohr    function testTrailingNumericEntityDecoded() {
445465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
446465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&#65; end');
447465aec67SAndreas Gohr        $calls = [
448465aec67SAndreas Gohr            ['document_start', []],
449465aec67SAndreas Gohr            ['p_open', []],
450465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
451465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
452465aec67SAndreas Gohr            ['cdata', ['A end']],
453465aec67SAndreas Gohr            ['p_close', []],
454465aec67SAndreas Gohr            ['document_end', []],
455465aec67SAndreas Gohr        ];
456465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
457465aec67SAndreas Gohr    }
458465aec67SAndreas Gohr
459465aec67SAndreas Gohr    function testNonTrailingEntityStaysInsideUrl() {
460465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
461465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&copy;more end');
462465aec67SAndreas Gohr        $calls = [
463465aec67SAndreas Gohr            ['document_start', []],
464465aec67SAndreas Gohr            ['p_open', []],
465465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
466465aec67SAndreas Gohr            ['externallink', ['http://example.com/&copy;more', null]],
467465aec67SAndreas Gohr            ['cdata', [' end']],
468465aec67SAndreas Gohr            ['p_close', []],
469465aec67SAndreas Gohr            ['document_end', []],
470465aec67SAndreas Gohr        ];
471465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
472465aec67SAndreas Gohr    }
473465aec67SAndreas Gohr
474465aec67SAndreas Gohr    function testMixtureParenThenEntityPeelsBoth() {
475465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
476465aec67SAndreas Gohr        $this->P->parse('See (http://example.com/path)&copy; end');
477465aec67SAndreas Gohr        $calls = [
478465aec67SAndreas Gohr            ['document_start', []],
479465aec67SAndreas Gohr            ['p_open', []],
480465aec67SAndreas Gohr            ['cdata', ["\nSee ("]],
481465aec67SAndreas Gohr            ['externallink', ['http://example.com/path', null]],
482465aec67SAndreas Gohr            ['cdata', [')© end']],
483465aec67SAndreas Gohr            ['p_close', []],
484465aec67SAndreas Gohr            ['document_end', []],
485465aec67SAndreas Gohr        ];
486465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
487465aec67SAndreas Gohr    }
488465aec67SAndreas Gohr
489465aec67SAndreas Gohr    function testMixtureMultipleEntitiesAndParens() {
490465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
491465aec67SAndreas Gohr        $this->P->parse('See http://example.com/)&copy;)&hl; end');
492465aec67SAndreas Gohr        $calls = [
493465aec67SAndreas Gohr            ['document_start', []],
494465aec67SAndreas Gohr            ['p_open', []],
495465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
496465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
497465aec67SAndreas Gohr            ['cdata', [')©)&hl; end']],
498465aec67SAndreas Gohr            ['p_close', []],
499465aec67SAndreas Gohr            ['document_end', []],
500465aec67SAndreas Gohr        ];
501465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
502465aec67SAndreas Gohr    }
503465aec67SAndreas Gohr}
504