xref: /dokuwiki/_test/tests/Parsing/ParserMode/ExternallinkTest.php (revision 465aec673bd9c2018b4df5364f08c816a70f03a7)
1*465aec67SAndreas Gohr<?php
2*465aec67SAndreas Gohr
3*465aec67SAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode;
4*465aec67SAndreas Gohr
5*465aec67SAndreas Gohruse dokuwiki\Parsing\ModeRegistry;
6*465aec67SAndreas Gohruse dokuwiki\Parsing\ParserMode\Externallink;
7*465aec67SAndreas Gohruse dokuwiki\Parsing\ParserMode\Internallink;
8*465aec67SAndreas Gohr
9*465aec67SAndreas Gohr/**
10*465aec67SAndreas Gohr * Tests for the {@see Externallink} parser mode.
11*465aec67SAndreas Gohr *
12*465aec67SAndreas Gohr * Covers the classic DokuWiki autolink behavior (bare URLs, www./ftp. shortcuts, IPv4/IPv6,
13*465aec67SAndreas Gohr * scheme allow-listing), the Markdown angle-bracket autolink form (CommonMark §6.5), and the
14*465aec67SAndreas Gohr * GFM autolink extension trim step (paren balancing, trailing entity-ref decoding).
15*465aec67SAndreas Gohr *
16*465aec67SAndreas Gohr * @group parser_links
17*465aec67SAndreas Gohr */
18*465aec67SAndreas Gohrclass ExternallinkTest extends ParserTestBase
19*465aec67SAndreas Gohr{
20*465aec67SAndreas Gohr    public function setUp(): void
21*465aec67SAndreas Gohr    {
22*465aec67SAndreas Gohr        parent::setUp();
23*465aec67SAndreas Gohr        global $conf;
24*465aec67SAndreas Gohr        $conf['syntax'] = 'md';
25*465aec67SAndreas Gohr        ModeRegistry::reset();
26*465aec67SAndreas Gohr    }
27*465aec67SAndreas Gohr
28*465aec67SAndreas Gohr    public function tearDown(): void
29*465aec67SAndreas Gohr    {
30*465aec67SAndreas Gohr        ModeRegistry::reset();
31*465aec67SAndreas Gohr        parent::tearDown();
32*465aec67SAndreas Gohr    }
33*465aec67SAndreas Gohr
34*465aec67SAndreas Gohr    // ----- basic bare-URL autolink -----
35*465aec67SAndreas Gohr
36*465aec67SAndreas Gohr    function testSimple() {
37*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
38*465aec67SAndreas Gohr        $this->P->parse("Foo http://www.google.com Bar");
39*465aec67SAndreas Gohr        $calls = [
40*465aec67SAndreas Gohr            ['document_start', []],
41*465aec67SAndreas Gohr            ['p_open', []],
42*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
43*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', null]],
44*465aec67SAndreas Gohr            ['cdata', [' Bar']],
45*465aec67SAndreas Gohr            ['p_close', []],
46*465aec67SAndreas Gohr            ['document_end', []],
47*465aec67SAndreas Gohr        ];
48*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
49*465aec67SAndreas Gohr    }
50*465aec67SAndreas Gohr
51*465aec67SAndreas Gohr    function testCase() {
52*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
53*465aec67SAndreas Gohr        $this->P->parse("Foo HTTP://WWW.GOOGLE.COM Bar");
54*465aec67SAndreas Gohr        $calls = [
55*465aec67SAndreas Gohr            ['document_start', []],
56*465aec67SAndreas Gohr            ['p_open', []],
57*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
58*465aec67SAndreas Gohr            ['externallink', ['HTTP://WWW.GOOGLE.COM', null]],
59*465aec67SAndreas Gohr            ['cdata', [' Bar']],
60*465aec67SAndreas Gohr            ['p_close', []],
61*465aec67SAndreas Gohr            ['document_end', []],
62*465aec67SAndreas Gohr        ];
63*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
64*465aec67SAndreas Gohr    }
65*465aec67SAndreas Gohr
66*465aec67SAndreas Gohr    function testIPv4() {
67*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
68*465aec67SAndreas Gohr        $this->P->parse("Foo http://123.123.3.21/foo Bar");
69*465aec67SAndreas Gohr        $calls = [
70*465aec67SAndreas Gohr            ['document_start', []],
71*465aec67SAndreas Gohr            ['p_open', []],
72*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
73*465aec67SAndreas Gohr            ['externallink', ['http://123.123.3.21/foo', null]],
74*465aec67SAndreas Gohr            ['cdata', [' Bar']],
75*465aec67SAndreas Gohr            ['p_close', []],
76*465aec67SAndreas Gohr            ['document_end', []],
77*465aec67SAndreas Gohr        ];
78*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
79*465aec67SAndreas Gohr    }
80*465aec67SAndreas Gohr
81*465aec67SAndreas Gohr    function testIPv6() {
82*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
83*465aec67SAndreas Gohr        $this->P->parse("Foo http://[3ffe:2a00:100:7031::1]/foo Bar");
84*465aec67SAndreas Gohr        $calls = [
85*465aec67SAndreas Gohr            ['document_start', []],
86*465aec67SAndreas Gohr            ['p_open', []],
87*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
88*465aec67SAndreas Gohr            ['externallink', ['http://[3ffe:2a00:100:7031::1]/foo', null]],
89*465aec67SAndreas Gohr            ['cdata', [' Bar']],
90*465aec67SAndreas Gohr            ['p_close', []],
91*465aec67SAndreas Gohr            ['document_end', []],
92*465aec67SAndreas Gohr        ];
93*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
94*465aec67SAndreas Gohr    }
95*465aec67SAndreas Gohr
96*465aec67SAndreas Gohr    function testMulti() {
97*465aec67SAndreas Gohr        $this->teardown();
98*465aec67SAndreas Gohr
99*465aec67SAndreas Gohr        $links = [
100*465aec67SAndreas Gohr            'http://www.google.com',
101*465aec67SAndreas Gohr            'HTTP://WWW.GOOGLE.COM',
102*465aec67SAndreas Gohr            'http://[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]:80/index.html',
103*465aec67SAndreas Gohr            'http://[1080:0:0:0:8:800:200C:417A]/index.html',
104*465aec67SAndreas Gohr            'http://[3ffe:2a00:100:7031::1]',
105*465aec67SAndreas Gohr            'http://[1080::8:800:200C:417A]/foo',
106*465aec67SAndreas Gohr            'http://[::192.9.5.5]/ipng',
107*465aec67SAndreas Gohr            'http://[::FFFF:129.144.52.38]:80/index.html',
108*465aec67SAndreas Gohr            'http://[2010:836B:4179::836B:4179]',
109*465aec67SAndreas Gohr        ];
110*465aec67SAndreas Gohr        $titles = [false, null, 'foo bar'];
111*465aec67SAndreas Gohr        foreach ($links as $link) {
112*465aec67SAndreas Gohr            foreach ($titles as $title) {
113*465aec67SAndreas Gohr                if ($title === false) {
114*465aec67SAndreas Gohr                    $source = $link;
115*465aec67SAndreas Gohr                    $name = null;
116*465aec67SAndreas Gohr                } elseif ($title === null) {
117*465aec67SAndreas Gohr                    $source = "[[$link]]";
118*465aec67SAndreas Gohr                    $name = null;
119*465aec67SAndreas Gohr                } else {
120*465aec67SAndreas Gohr                    $source = "[[$link|$title]]";
121*465aec67SAndreas Gohr                    $name = $title;
122*465aec67SAndreas Gohr                }
123*465aec67SAndreas Gohr                $this->setup();
124*465aec67SAndreas Gohr                $this->P->addMode('internallink', new Internallink());
125*465aec67SAndreas Gohr                $this->P->addMode('externallink', new Externallink());
126*465aec67SAndreas Gohr                $this->P->parse("Foo $source Bar");
127*465aec67SAndreas Gohr                $calls = [
128*465aec67SAndreas Gohr                    ['document_start', []],
129*465aec67SAndreas Gohr                    ['p_open', []],
130*465aec67SAndreas Gohr                    ['cdata', ["\n" . 'Foo ']],
131*465aec67SAndreas Gohr                    ['externallink', [$link, $name]],
132*465aec67SAndreas Gohr                    ['cdata', [' Bar']],
133*465aec67SAndreas Gohr                    ['p_close', []],
134*465aec67SAndreas Gohr                    ['document_end', []],
135*465aec67SAndreas Gohr                ];
136*465aec67SAndreas Gohr                $this->assertCalls($calls, $this->H->calls, $source);
137*465aec67SAndreas Gohr                $this->teardown();
138*465aec67SAndreas Gohr            }
139*465aec67SAndreas Gohr        }
140*465aec67SAndreas Gohr
141*465aec67SAndreas Gohr        $this->setup();
142*465aec67SAndreas Gohr    }
143*465aec67SAndreas Gohr
144*465aec67SAndreas Gohr    function testJavascriptScheme() {
145*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
146*465aec67SAndreas Gohr        $this->P->parse("Foo javascript:alert('XSS'); Bar");
147*465aec67SAndreas Gohr        $calls = [
148*465aec67SAndreas Gohr            ['document_start', []],
149*465aec67SAndreas Gohr            ['p_open', []],
150*465aec67SAndreas Gohr            ['cdata', ["\nFoo javascript:alert('XSS'); Bar"]],
151*465aec67SAndreas Gohr            ['p_close', []],
152*465aec67SAndreas Gohr            ['document_end', []],
153*465aec67SAndreas Gohr        ];
154*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
155*465aec67SAndreas Gohr    }
156*465aec67SAndreas Gohr
157*465aec67SAndreas Gohr    // ----- www. / ftp. shortcuts -----
158*465aec67SAndreas Gohr
159*465aec67SAndreas Gohr    function testWWWLink() {
160*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
161*465aec67SAndreas Gohr        $this->P->parse("Foo www.google.com Bar");
162*465aec67SAndreas Gohr        $calls = [
163*465aec67SAndreas Gohr            ['document_start', []],
164*465aec67SAndreas Gohr            ['p_open', []],
165*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
166*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
167*465aec67SAndreas Gohr            ['cdata', [' Bar']],
168*465aec67SAndreas Gohr            ['p_close', []],
169*465aec67SAndreas Gohr            ['document_end', []],
170*465aec67SAndreas Gohr        ];
171*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
172*465aec67SAndreas Gohr    }
173*465aec67SAndreas Gohr
174*465aec67SAndreas Gohr    function testWWWLinkStartOfLine() {
175*465aec67SAndreas Gohr        // Regression test for issue #2399
176*465aec67SAndreas Gohr        $calls = [
177*465aec67SAndreas Gohr            ['document_start', []],
178*465aec67SAndreas Gohr            ['p_open', []],
179*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
180*465aec67SAndreas Gohr            ['cdata', [' Bar']],
181*465aec67SAndreas Gohr            ['p_close', []],
182*465aec67SAndreas Gohr            ['document_end', []],
183*465aec67SAndreas Gohr        ];
184*465aec67SAndreas Gohr        $instructions = p_get_instructions("www.google.com Bar");
185*465aec67SAndreas Gohr        $this->assertCalls($calls, $instructions);
186*465aec67SAndreas Gohr    }
187*465aec67SAndreas Gohr
188*465aec67SAndreas Gohr    function testWWWLinkInRoundBrackets() {
189*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
190*465aec67SAndreas Gohr        $this->P->parse("Foo (www.google.com) Bar");
191*465aec67SAndreas Gohr        $calls = [
192*465aec67SAndreas Gohr            ['document_start', []],
193*465aec67SAndreas Gohr            ['p_open', []],
194*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo (']],
195*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'www.google.com']],
196*465aec67SAndreas Gohr            ['cdata', [') Bar']],
197*465aec67SAndreas Gohr            ['p_close', []],
198*465aec67SAndreas Gohr            ['document_end', []],
199*465aec67SAndreas Gohr        ];
200*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
201*465aec67SAndreas Gohr    }
202*465aec67SAndreas Gohr
203*465aec67SAndreas Gohr    function testWWWLinkInPath() {
204*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
205*465aec67SAndreas Gohr        // See issue #936. Should NOT generate a link!
206*465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/www.something.de/somedir/ Bar");
207*465aec67SAndreas Gohr        $calls = [
208*465aec67SAndreas Gohr            ['document_start', []],
209*465aec67SAndreas Gohr            ['p_open', []],
210*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/www.something.de/somedir/ Bar']],
211*465aec67SAndreas Gohr            ['p_close', []],
212*465aec67SAndreas Gohr            ['document_end', []],
213*465aec67SAndreas Gohr        ];
214*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
215*465aec67SAndreas Gohr    }
216*465aec67SAndreas Gohr
217*465aec67SAndreas Gohr    function testWWWLinkFollowingPath() {
218*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
219*465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ www.something.de/somedir/ Bar");
220*465aec67SAndreas Gohr        $calls = [
221*465aec67SAndreas Gohr            ['document_start', []],
222*465aec67SAndreas Gohr            ['p_open', []],
223*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ ']],
224*465aec67SAndreas Gohr            ['externallink', ['http://www.something.de/somedir/', 'www.something.de/somedir/']],
225*465aec67SAndreas Gohr            ['cdata', [' Bar']],
226*465aec67SAndreas Gohr            ['p_close', []],
227*465aec67SAndreas Gohr            ['document_end', []],
228*465aec67SAndreas Gohr        ];
229*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
230*465aec67SAndreas Gohr    }
231*465aec67SAndreas Gohr
232*465aec67SAndreas Gohr    function testFTPLink() {
233*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
234*465aec67SAndreas Gohr        $this->P->parse("Foo ftp.sunsite.com Bar");
235*465aec67SAndreas Gohr        $calls = [
236*465aec67SAndreas Gohr            ['document_start', []],
237*465aec67SAndreas Gohr            ['p_open', []],
238*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
239*465aec67SAndreas Gohr            ['externallink', ['ftp://ftp.sunsite.com', 'ftp.sunsite.com']],
240*465aec67SAndreas Gohr            ['cdata', [' Bar']],
241*465aec67SAndreas Gohr            ['p_close', []],
242*465aec67SAndreas Gohr            ['document_end', []],
243*465aec67SAndreas Gohr        ];
244*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
245*465aec67SAndreas Gohr    }
246*465aec67SAndreas Gohr
247*465aec67SAndreas Gohr    function testFTPLinkInPath() {
248*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
249*465aec67SAndreas Gohr        // See issue #936. Should NOT generate a link!
250*465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ftp.something.de/somedir/ Bar");
251*465aec67SAndreas Gohr        $calls = [
252*465aec67SAndreas Gohr            ['document_start', []],
253*465aec67SAndreas Gohr            ['p_open', []],
254*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ftp.something.de/somedir/ Bar']],
255*465aec67SAndreas Gohr            ['p_close', []],
256*465aec67SAndreas Gohr            ['document_end', []],
257*465aec67SAndreas Gohr        ];
258*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
259*465aec67SAndreas Gohr    }
260*465aec67SAndreas Gohr
261*465aec67SAndreas Gohr    function testFTPLinkFollowingPath() {
262*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
263*465aec67SAndreas Gohr        $this->P->parse("Foo /home/subdir/www/ ftp.something.de/somedir/ Bar");
264*465aec67SAndreas Gohr        $calls = [
265*465aec67SAndreas Gohr            ['document_start', []],
266*465aec67SAndreas Gohr            ['p_open', []],
267*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo /home/subdir/www/ ']],
268*465aec67SAndreas Gohr            ['externallink', ['ftp://ftp.something.de/somedir/', 'ftp.something.de/somedir/']],
269*465aec67SAndreas Gohr            ['cdata', [' Bar']],
270*465aec67SAndreas Gohr            ['p_close', []],
271*465aec67SAndreas Gohr            ['document_end', []],
272*465aec67SAndreas Gohr        ];
273*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
274*465aec67SAndreas Gohr    }
275*465aec67SAndreas Gohr
276*465aec67SAndreas Gohr    // ----- Markdown angle-bracket autolinks (§6.5) -----
277*465aec67SAndreas Gohr
278*465aec67SAndreas Gohr    function testAngleBracketAutolink() {
279*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
280*465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com> Bar");
281*465aec67SAndreas Gohr        $calls = [
282*465aec67SAndreas Gohr            ['document_start', []],
283*465aec67SAndreas Gohr            ['p_open', []],
284*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo ']],
285*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', 'http://www.google.com']],
286*465aec67SAndreas Gohr            ['cdata', [' Bar']],
287*465aec67SAndreas Gohr            ['p_close', []],
288*465aec67SAndreas Gohr            ['document_end', []],
289*465aec67SAndreas Gohr        ];
290*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
291*465aec67SAndreas Gohr    }
292*465aec67SAndreas Gohr
293*465aec67SAndreas Gohr    function testAngleBracketDisqualifiedByInternalWhitespace() {
294*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
295*465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com bim> Bar");
296*465aec67SAndreas Gohr        // Internal whitespace disqualifies the autolink. The whole envelope is consumed as cdata so the
297*465aec67SAndreas Gohr        // bare-URL detector cannot pick up http://www.google.com inside and leave dangling brackets.
298*465aec67SAndreas Gohr        $calls = [
299*465aec67SAndreas Gohr            ['document_start', []],
300*465aec67SAndreas Gohr            ['p_open', []],
301*465aec67SAndreas Gohr            ['cdata', ["\nFoo <http://www.google.com bim> Bar"]],
302*465aec67SAndreas Gohr            ['p_close', []],
303*465aec67SAndreas Gohr            ['document_end', []],
304*465aec67SAndreas Gohr        ];
305*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
306*465aec67SAndreas Gohr    }
307*465aec67SAndreas Gohr
308*465aec67SAndreas Gohr    function testAngleBracketDisqualifiedByLeadingWhitespace() {
309*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
310*465aec67SAndreas Gohr        $this->P->parse("Foo < http://www.google.com > Bar");
311*465aec67SAndreas Gohr        $calls = [
312*465aec67SAndreas Gohr            ['document_start', []],
313*465aec67SAndreas Gohr            ['p_open', []],
314*465aec67SAndreas Gohr            ['cdata', ["\nFoo < http://www.google.com > Bar"]],
315*465aec67SAndreas Gohr            ['p_close', []],
316*465aec67SAndreas Gohr            ['document_end', []],
317*465aec67SAndreas Gohr        ];
318*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
319*465aec67SAndreas Gohr    }
320*465aec67SAndreas Gohr
321*465aec67SAndreas Gohr    function testAngleBracketUnregisteredScheme() {
322*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
323*465aec67SAndreas Gohr        // mailto is not in the default conf/scheme.conf allow-list, so no per-scheme angle pattern is built
324*465aec67SAndreas Gohr        // for it. The brackets fall through to cdata, matching DokuWiki's bare-URL scheme policy.
325*465aec67SAndreas Gohr        $this->P->parse("Foo <mailto:foo@example.com> Bar");
326*465aec67SAndreas Gohr        $calls = [
327*465aec67SAndreas Gohr            ['document_start', []],
328*465aec67SAndreas Gohr            ['p_open', []],
329*465aec67SAndreas Gohr            ['cdata', ["\nFoo <mailto:foo@example.com> Bar"]],
330*465aec67SAndreas Gohr            ['p_close', []],
331*465aec67SAndreas Gohr            ['document_end', []],
332*465aec67SAndreas Gohr        ];
333*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
334*465aec67SAndreas Gohr    }
335*465aec67SAndreas Gohr
336*465aec67SAndreas Gohr    function testAngleBracketInactiveInDwMode() {
337*465aec67SAndreas Gohr        global $conf;
338*465aec67SAndreas Gohr        $conf['syntax'] = 'dw';
339*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
340*465aec67SAndreas Gohr        // In DW-only syntax, angle-bracket processing is intentionally not active. The bare-URL pattern still
341*465aec67SAndreas Gohr        // picks up the URL inside and the angle brackets fall through as literal text — matches the pre-merge
342*465aec67SAndreas Gohr        // behavior of DokuWiki's Externallink mode.
343*465aec67SAndreas Gohr        $this->P->parse("Foo <http://www.google.com> Bar");
344*465aec67SAndreas Gohr        $calls = [
345*465aec67SAndreas Gohr            ['document_start', []],
346*465aec67SAndreas Gohr            ['p_open', []],
347*465aec67SAndreas Gohr            ['cdata', ["\n" . 'Foo <']],
348*465aec67SAndreas Gohr            ['externallink', ['http://www.google.com', null]],
349*465aec67SAndreas Gohr            ['cdata', ['> Bar']],
350*465aec67SAndreas Gohr            ['p_close', []],
351*465aec67SAndreas Gohr            ['document_end', []],
352*465aec67SAndreas Gohr        ];
353*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
354*465aec67SAndreas Gohr    }
355*465aec67SAndreas Gohr
356*465aec67SAndreas Gohr    // ----- GFM autolink extension: paren balancing -----
357*465aec67SAndreas Gohr
358*465aec67SAndreas Gohr    function testBalancedParensInUrl() {
359*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
360*465aec67SAndreas Gohr        $this->P->parse('See www.example.com/path(foo) end');
361*465aec67SAndreas Gohr        $calls = [
362*465aec67SAndreas Gohr            ['document_start', []],
363*465aec67SAndreas Gohr            ['p_open', []],
364*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
365*465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
366*465aec67SAndreas Gohr            ['cdata', [' end']],
367*465aec67SAndreas Gohr            ['p_close', []],
368*465aec67SAndreas Gohr            ['document_end', []],
369*465aec67SAndreas Gohr        ];
370*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
371*465aec67SAndreas Gohr    }
372*465aec67SAndreas Gohr
373*465aec67SAndreas Gohr    function testTrailingUnbalancedParenExcluded() {
374*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
375*465aec67SAndreas Gohr        $this->P->parse('See (www.example.com/path(foo)) end');
376*465aec67SAndreas Gohr        $calls = [
377*465aec67SAndreas Gohr            ['document_start', []],
378*465aec67SAndreas Gohr            ['p_open', []],
379*465aec67SAndreas Gohr            ['cdata', ["\nSee ("]],
380*465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
381*465aec67SAndreas Gohr            ['cdata', [') end']],
382*465aec67SAndreas Gohr            ['p_close', []],
383*465aec67SAndreas Gohr            ['document_end', []],
384*465aec67SAndreas Gohr        ];
385*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
386*465aec67SAndreas Gohr    }
387*465aec67SAndreas Gohr
388*465aec67SAndreas Gohr    function testMultipleTrailingParensTrimmedUntilBalanced() {
389*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
390*465aec67SAndreas Gohr        // Inner `(foo)` is balanced and stays inside the URL; the two unbalanced trailing `)` are peeled off.
391*465aec67SAndreas Gohr        $this->P->parse('See www.example.com/path(foo))) end');
392*465aec67SAndreas Gohr        $calls = [
393*465aec67SAndreas Gohr            ['document_start', []],
394*465aec67SAndreas Gohr            ['p_open', []],
395*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
396*465aec67SAndreas Gohr            ['externallink', ['http://www.example.com/path(foo)', 'www.example.com/path(foo)']],
397*465aec67SAndreas Gohr            ['cdata', [')) end']],
398*465aec67SAndreas Gohr            ['p_close', []],
399*465aec67SAndreas Gohr            ['document_end', []],
400*465aec67SAndreas Gohr        ];
401*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
402*465aec67SAndreas Gohr    }
403*465aec67SAndreas Gohr
404*465aec67SAndreas Gohr    function testParenInsideUrlNoTrailing() {
405*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
406*465aec67SAndreas Gohr        $this->P->parse('See www.example.com/search?q=(business))+ok end');
407*465aec67SAndreas Gohr        $calls = [
408*465aec67SAndreas Gohr            ['document_start', []],
409*465aec67SAndreas Gohr            ['p_open', []],
410*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
411*465aec67SAndreas Gohr            ['externallink', [
412*465aec67SAndreas Gohr                'http://www.example.com/search?q=(business))+ok',
413*465aec67SAndreas Gohr                'www.example.com/search?q=(business))+ok'
414*465aec67SAndreas Gohr            ]],
415*465aec67SAndreas Gohr            ['cdata', [' end']],
416*465aec67SAndreas Gohr            ['p_close', []],
417*465aec67SAndreas Gohr            ['document_end', []],
418*465aec67SAndreas Gohr        ];
419*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
420*465aec67SAndreas Gohr    }
421*465aec67SAndreas Gohr
422*465aec67SAndreas Gohr    // ----- GFM autolink extension: trailing entity references -----
423*465aec67SAndreas Gohr
424*465aec67SAndreas Gohr    function testTrailingValidEntityDecodedToUnicode() {
425*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
426*465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&copy; end');
427*465aec67SAndreas Gohr        $calls = [
428*465aec67SAndreas Gohr            ['document_start', []],
429*465aec67SAndreas Gohr            ['p_open', []],
430*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
431*465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
432*465aec67SAndreas Gohr            ['cdata', ['© end']],
433*465aec67SAndreas Gohr            ['p_close', []],
434*465aec67SAndreas Gohr            ['document_end', []],
435*465aec67SAndreas Gohr        ];
436*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
437*465aec67SAndreas Gohr    }
438*465aec67SAndreas Gohr
439*465aec67SAndreas Gohr    function testTrailingUnknownEntityRoundTripsLiterally() {
440*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
441*465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&hl; end');
442*465aec67SAndreas Gohr        $calls = [
443*465aec67SAndreas Gohr            ['document_start', []],
444*465aec67SAndreas Gohr            ['p_open', []],
445*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
446*465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
447*465aec67SAndreas Gohr            ['cdata', ['&hl; end']],
448*465aec67SAndreas Gohr            ['p_close', []],
449*465aec67SAndreas Gohr            ['document_end', []],
450*465aec67SAndreas Gohr        ];
451*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
452*465aec67SAndreas Gohr    }
453*465aec67SAndreas Gohr
454*465aec67SAndreas Gohr    function testTrailingNumericEntityDecoded() {
455*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
456*465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&#65; end');
457*465aec67SAndreas Gohr        $calls = [
458*465aec67SAndreas Gohr            ['document_start', []],
459*465aec67SAndreas Gohr            ['p_open', []],
460*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
461*465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
462*465aec67SAndreas Gohr            ['cdata', ['A end']],
463*465aec67SAndreas Gohr            ['p_close', []],
464*465aec67SAndreas Gohr            ['document_end', []],
465*465aec67SAndreas Gohr        ];
466*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
467*465aec67SAndreas Gohr    }
468*465aec67SAndreas Gohr
469*465aec67SAndreas Gohr    function testNonTrailingEntityStaysInsideUrl() {
470*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
471*465aec67SAndreas Gohr        $this->P->parse('See http://example.com/&copy;more end');
472*465aec67SAndreas Gohr        $calls = [
473*465aec67SAndreas Gohr            ['document_start', []],
474*465aec67SAndreas Gohr            ['p_open', []],
475*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
476*465aec67SAndreas Gohr            ['externallink', ['http://example.com/&copy;more', null]],
477*465aec67SAndreas Gohr            ['cdata', [' end']],
478*465aec67SAndreas Gohr            ['p_close', []],
479*465aec67SAndreas Gohr            ['document_end', []],
480*465aec67SAndreas Gohr        ];
481*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
482*465aec67SAndreas Gohr    }
483*465aec67SAndreas Gohr
484*465aec67SAndreas Gohr    function testMixtureParenThenEntityPeelsBoth() {
485*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
486*465aec67SAndreas Gohr        $this->P->parse('See (http://example.com/path)&copy; end');
487*465aec67SAndreas Gohr        $calls = [
488*465aec67SAndreas Gohr            ['document_start', []],
489*465aec67SAndreas Gohr            ['p_open', []],
490*465aec67SAndreas Gohr            ['cdata', ["\nSee ("]],
491*465aec67SAndreas Gohr            ['externallink', ['http://example.com/path', null]],
492*465aec67SAndreas Gohr            ['cdata', [')© end']],
493*465aec67SAndreas Gohr            ['p_close', []],
494*465aec67SAndreas Gohr            ['document_end', []],
495*465aec67SAndreas Gohr        ];
496*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
497*465aec67SAndreas Gohr    }
498*465aec67SAndreas Gohr
499*465aec67SAndreas Gohr    function testMixtureMultipleEntitiesAndParens() {
500*465aec67SAndreas Gohr        $this->P->addMode('externallink', new Externallink());
501*465aec67SAndreas Gohr        $this->P->parse('See http://example.com/)&copy;)&hl; end');
502*465aec67SAndreas Gohr        $calls = [
503*465aec67SAndreas Gohr            ['document_start', []],
504*465aec67SAndreas Gohr            ['p_open', []],
505*465aec67SAndreas Gohr            ['cdata', ["\nSee "]],
506*465aec67SAndreas Gohr            ['externallink', ['http://example.com/', null]],
507*465aec67SAndreas Gohr            ['cdata', [')©)&hl; end']],
508*465aec67SAndreas Gohr            ['p_close', []],
509*465aec67SAndreas Gohr            ['document_end', []],
510*465aec67SAndreas Gohr        ];
511*465aec67SAndreas Gohr        $this->assertCalls($calls, $this->H->calls);
512*465aec67SAndreas Gohr    }
513*465aec67SAndreas Gohr}
514