18719732dSAndreas Gohr<?php 28719732dSAndreas Gohr 38719732dSAndreas Gohrnamespace dokuwiki\test\Parsing\ParserMode; 48719732dSAndreas Gohr 58719732dSAndreas Gohruse dokuwiki\Parsing\ParserMode\Eol; 68719732dSAndreas Gohruse dokuwiki\Parsing\ParserMode\GfmHeader; 78719732dSAndreas Gohr 88719732dSAndreas Gohr/** 98719732dSAndreas Gohr * Tests for GFM ATX headings (`# text` through `###### text`). 108719732dSAndreas Gohr */ 118719732dSAndreas Gohrclass GfmHeaderTest extends ParserTestBase 128719732dSAndreas Gohr{ 138719732dSAndreas Gohr public function setUp(): void 148719732dSAndreas Gohr { 158719732dSAndreas Gohr parent::setUp(); 1647a02a10SAndreas Gohr $this->setSyntax('md'); 178719732dSAndreas Gohr } 188719732dSAndreas Gohr 198719732dSAndreas Gohr function testLevelOne() 208719732dSAndreas Gohr { 218719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 228719732dSAndreas Gohr $this->P->parse("abc\n# Header\ndef"); 238719732dSAndreas Gohr $calls = [ 248719732dSAndreas Gohr ['document_start', []], 258719732dSAndreas Gohr ['p_open', []], 268719732dSAndreas Gohr ['cdata', ["\nabc"]], 278719732dSAndreas Gohr ['p_close', []], 28*4b31eadfSAndreas Gohr // pos points at the `#` (index 5), not the newline before it 29*4b31eadfSAndreas Gohr // that the entry pattern's lookbehind matches; see testPosPointsAtHash 30*4b31eadfSAndreas Gohr ['header', ['Header', 1, 5]], 318719732dSAndreas Gohr ['section_open', [1]], 328719732dSAndreas Gohr ['p_open', []], 338719732dSAndreas Gohr ['cdata', ["\ndef"]], 348719732dSAndreas Gohr ['p_close', []], 358719732dSAndreas Gohr ['section_close', []], 368719732dSAndreas Gohr ['document_end', []], 378719732dSAndreas Gohr ]; 388719732dSAndreas Gohr $this->assertCalls($calls, $this->H->calls); 398719732dSAndreas Gohr } 408719732dSAndreas Gohr 418719732dSAndreas Gohr function testAllLevels() 428719732dSAndreas Gohr { 438719732dSAndreas Gohr foreach ([1, 2, 3, 4, 5, 6] as $level) { 448719732dSAndreas Gohr $this->setUp(); 458719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 468719732dSAndreas Gohr $marker = str_repeat('#', $level); 478719732dSAndreas Gohr $this->P->parse("$marker foo"); 488719732dSAndreas Gohr $calls = array_column($this->H->calls, 0); 498719732dSAndreas Gohr $this->assertContains('header', $calls, "level $level must emit header"); 508719732dSAndreas Gohr 518719732dSAndreas Gohr $headerCall = array_values(array_filter( 528719732dSAndreas Gohr $this->H->calls, 538719732dSAndreas Gohr static fn($c) => $c[0] === 'header' 548719732dSAndreas Gohr ))[0]; 558719732dSAndreas Gohr $this->assertSame('foo', $headerCall[1][0], "level $level title"); 568719732dSAndreas Gohr $this->assertSame($level, $headerCall[1][1], "level $level level"); 578719732dSAndreas Gohr } 588719732dSAndreas Gohr } 598719732dSAndreas Gohr 608719732dSAndreas Gohr function testSevenHashesIsNotAHeading() 618719732dSAndreas Gohr { 628719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 638719732dSAndreas Gohr $this->P->parse('####### foo'); 648719732dSAndreas Gohr $modes = array_column($this->H->calls, 0); 658719732dSAndreas Gohr $this->assertNotContains('header', $modes, 668719732dSAndreas Gohr 'A run of 7 `#` must not open an ATX heading'); 678719732dSAndreas Gohr } 688719732dSAndreas Gohr 698719732dSAndreas Gohr function testHashTouchingTextIsNotAHeading() 708719732dSAndreas Gohr { 718719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 728719732dSAndreas Gohr $this->P->parse("#5 bolt\n\n#hashtag"); 738719732dSAndreas Gohr $modes = array_column($this->H->calls, 0); 748719732dSAndreas Gohr $this->assertNotContains('header', $modes, 758719732dSAndreas Gohr 'A `#` directly followed by a non-space char must not open a heading'); 768719732dSAndreas Gohr } 778719732dSAndreas Gohr 788719732dSAndreas Gohr function testEmptyHeading() 798719732dSAndreas Gohr { 808719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 818719732dSAndreas Gohr $this->P->parse("#\n"); 828719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 838719732dSAndreas Gohr $this->assertCount(1, $headerCalls, 'bare `#` must still emit a heading'); 848719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 858719732dSAndreas Gohr $this->assertSame('', $call[1][0]); 868719732dSAndreas Gohr $this->assertSame(1, $call[1][1]); 878719732dSAndreas Gohr } 888719732dSAndreas Gohr 898719732dSAndreas Gohr function testEmptyHeadingWithTrailingSpace() 908719732dSAndreas Gohr { 918719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 928719732dSAndreas Gohr $this->P->parse("## \n"); 938719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 948719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 958719732dSAndreas Gohr $this->assertSame('', $call[1][0]); 968719732dSAndreas Gohr $this->assertSame(2, $call[1][1]); 978719732dSAndreas Gohr } 988719732dSAndreas Gohr 998719732dSAndreas Gohr function testEmptyHeadingWithClosingHashes() 1008719732dSAndreas Gohr { 1018719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1028719732dSAndreas Gohr $this->P->parse("### ###\n"); 1038719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1048719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1058719732dSAndreas Gohr $this->assertSame('', $call[1][0]); 1068719732dSAndreas Gohr $this->assertSame(3, $call[1][1]); 1078719732dSAndreas Gohr } 1088719732dSAndreas Gohr 1098719732dSAndreas Gohr function testOptionalClosingHashesStripped() 1108719732dSAndreas Gohr { 1118719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1128719732dSAndreas Gohr $this->P->parse("## foo ##\n"); 1138719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1148719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1158719732dSAndreas Gohr $this->assertSame('foo', $call[1][0]); 1168719732dSAndreas Gohr $this->assertSame(2, $call[1][1]); 1178719732dSAndreas Gohr } 1188719732dSAndreas Gohr 1198719732dSAndreas Gohr function testClosingNeedNotMatchOpeningLength() 1208719732dSAndreas Gohr { 1218719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1228719732dSAndreas Gohr $this->P->parse("# foo ##################################\n"); 1238719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1248719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1258719732dSAndreas Gohr $this->assertSame('foo', $call[1][0]); 1268719732dSAndreas Gohr $this->assertSame(1, $call[1][1]); 1278719732dSAndreas Gohr } 1288719732dSAndreas Gohr 1298719732dSAndreas Gohr function testTrailingSpacesAfterClosing() 1308719732dSAndreas Gohr { 1318719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1328719732dSAndreas Gohr $this->P->parse("### foo ### \n"); 1338719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1348719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1358719732dSAndreas Gohr $this->assertSame('foo', $call[1][0]); 1368719732dSAndreas Gohr $this->assertSame(3, $call[1][1]); 1378719732dSAndreas Gohr } 1388719732dSAndreas Gohr 1398719732dSAndreas Gohr function testClosingRunFollowedByTextIsNotClosing() 1408719732dSAndreas Gohr { 1418719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1428719732dSAndreas Gohr $this->P->parse("### foo ### b\n"); 1438719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1448719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1458719732dSAndreas Gohr $this->assertSame('foo ### b', $call[1][0]); 1468719732dSAndreas Gohr $this->assertSame(3, $call[1][1]); 1478719732dSAndreas Gohr } 1488719732dSAndreas Gohr 1498719732dSAndreas Gohr function testClosingHashMustBePrecededBySpace() 1508719732dSAndreas Gohr { 1518719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1528719732dSAndreas Gohr $this->P->parse("# foo#\n"); 1538719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1548719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1558719732dSAndreas Gohr $this->assertSame('foo#', $call[1][0]); 1568719732dSAndreas Gohr $this->assertSame(1, $call[1][1]); 1578719732dSAndreas Gohr } 1588719732dSAndreas Gohr 1598719732dSAndreas Gohr function testIndentedHashIsNotAHeading() 1608719732dSAndreas Gohr { 1618719732dSAndreas Gohr // GFM tolerates 0-3 spaces of indent; we do not. Any leading 1628719732dSAndreas Gohr // whitespace makes the line a paragraph (or preformatted, if 1638719732dSAndreas Gohr // it meets that mode's rules). 1648719732dSAndreas Gohr foreach ([1, 2, 3] as $indent) { 1658719732dSAndreas Gohr $this->setUp(); 1668719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1678719732dSAndreas Gohr $this->P->parse(str_repeat(' ', $indent) . '### foo'); 1688719732dSAndreas Gohr $modes = array_column($this->H->calls, 0); 1698719732dSAndreas Gohr $this->assertNotContains('header', $modes, 1708719732dSAndreas Gohr "indent=$indent must NOT open a heading"); 1718719732dSAndreas Gohr } 1728719732dSAndreas Gohr } 1738719732dSAndreas Gohr 1748719732dSAndreas Gohr function testContentInlineWhitespaceCollapsed() 1758719732dSAndreas Gohr { 1768719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1778719732dSAndreas Gohr $this->P->parse("# foo \n"); 1788719732dSAndreas Gohr $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header'); 1798719732dSAndreas Gohr $call = array_values($headerCalls)[0]; 1808719732dSAndreas Gohr $this->assertSame('foo', $call[1][0]); 1818719732dSAndreas Gohr } 1828719732dSAndreas Gohr 1838719732dSAndreas Gohr function testHeadingCanInterruptParagraph() 1848719732dSAndreas Gohr { 1858719732dSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 1868719732dSAndreas Gohr $this->P->addMode('eol', new Eol()); 1878719732dSAndreas Gohr $this->P->parse("Foo bar\n# baz\nBar foo"); 1888719732dSAndreas Gohr $modes = array_column($this->H->calls, 0); 1898719732dSAndreas Gohr $this->assertContains('header', $modes, 1908719732dSAndreas Gohr 'ATX headings must interrupt paragraphs without requiring a blank line'); 1918719732dSAndreas Gohr } 1928719732dSAndreas Gohr 1938719732dSAndreas Gohr function testSortValue() 1948719732dSAndreas Gohr { 1958719732dSAndreas Gohr $mode = new GfmHeader(); 1968719732dSAndreas Gohr $this->assertSame(50, $mode->getSort()); 1978719732dSAndreas Gohr } 198*4b31eadfSAndreas Gohr 199*4b31eadfSAndreas Gohr /** 200*4b31eadfSAndreas Gohr * The entry pattern anchors the hashes to column 0 with a lookbehind on 201*4b31eadfSAndreas Gohr * the preceding newline, so the reported position points at the first `#` 202*4b31eadfSAndreas Gohr * rather than the newline. This keeps the blank line above the heading in 203*4b31eadfSAndreas Gohr * the previous section instead of eating it on section edit. See PR #4636. 204*4b31eadfSAndreas Gohr */ 205*4b31eadfSAndreas Gohr function testPosPointsAtHash() 206*4b31eadfSAndreas Gohr { 207*4b31eadfSAndreas Gohr // parse() prepends a newline, so with a blank line above the heading 208*4b31eadfSAndreas Gohr // the doc is "\n# top\n\n## sub\n". The `#` of "## sub" sits at index 8. 209*4b31eadfSAndreas Gohr $this->P->addMode('gfm_header', new GfmHeader()); 210*4b31eadfSAndreas Gohr $this->P->parse("# top\n\n## sub\n"); 211*4b31eadfSAndreas Gohr 212*4b31eadfSAndreas Gohr $headers = array_values(array_filter( 213*4b31eadfSAndreas Gohr $this->H->calls, 214*4b31eadfSAndreas Gohr static fn($c) => $c[0] === 'header' 215*4b31eadfSAndreas Gohr )); 216*4b31eadfSAndreas Gohr $this->assertCount(2, $headers); 217*4b31eadfSAndreas Gohr 218*4b31eadfSAndreas Gohr // first heading: the leading `#` follows only the prepended newline 219*4b31eadfSAndreas Gohr $this->assertSame('top', $headers[0][1][0]); 220*4b31eadfSAndreas Gohr $this->assertSame(1, $headers[0][2], 'pos must point at the first `#`'); 221*4b31eadfSAndreas Gohr 222*4b31eadfSAndreas Gohr // second heading: pos must skip both the line break and the blank 223*4b31eadfSAndreas Gohr // line, landing on the `#` rather than the blank line's newline 224*4b31eadfSAndreas Gohr $this->assertSame('sub', $headers[1][1][0]); 225*4b31eadfSAndreas Gohr $this->assertSame(8, $headers[1][2], 226*4b31eadfSAndreas Gohr 'pos must point at the `#`, not the blank line above it'); 227*4b31eadfSAndreas Gohr } 2288719732dSAndreas Gohr} 229