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/© 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/A 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/©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/©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)© 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/)©)&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