xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmHeaderTest.php (revision 4b31eadfd0dd82e519dd953a4cb0ad079114879d)
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