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