xref: /dokuwiki/_test/tests/Parsing/ParserMode/GfmHeaderTest.php (revision 2e43b79909f3bc04928779d886f68c1242b5d436)
1<?php
2
3namespace dokuwiki\test\Parsing\ParserMode;
4
5use dokuwiki\Parsing\ParserMode\Eol;
6use dokuwiki\Parsing\ParserMode\GfmHeader;
7
8/**
9 * Tests for GFM ATX headings (`# text` through `###### text`).
10 */
11class GfmHeaderTest extends ParserTestBase
12{
13    public function setUp(): void
14    {
15        parent::setUp();
16        $this->setSyntax('md');
17    }
18
19    function testLevelOne()
20    {
21        $this->P->addMode('gfm_header', new GfmHeader());
22        $this->P->parse("abc\n# Header\ndef");
23        $calls = [
24            ['document_start', []],
25            ['p_open', []],
26            ['cdata', ["\nabc"]],
27            ['p_close', []],
28            ['header', ['Header', 1, 4]],
29            ['section_open', [1]],
30            ['p_open', []],
31            ['cdata', ["\ndef"]],
32            ['p_close', []],
33            ['section_close', []],
34            ['document_end', []],
35        ];
36        $this->assertCalls($calls, $this->H->calls);
37    }
38
39    function testAllLevels()
40    {
41        foreach ([1, 2, 3, 4, 5, 6] as $level) {
42            $this->setUp();
43            $this->P->addMode('gfm_header', new GfmHeader());
44            $marker = str_repeat('#', $level);
45            $this->P->parse("$marker foo");
46            $calls = array_column($this->H->calls, 0);
47            $this->assertContains('header', $calls, "level $level must emit header");
48
49            $headerCall = array_values(array_filter(
50                $this->H->calls,
51                static fn($c) => $c[0] === 'header'
52            ))[0];
53            $this->assertSame('foo', $headerCall[1][0], "level $level title");
54            $this->assertSame($level, $headerCall[1][1], "level $level level");
55        }
56    }
57
58    function testSevenHashesIsNotAHeading()
59    {
60        $this->P->addMode('gfm_header', new GfmHeader());
61        $this->P->parse('####### foo');
62        $modes = array_column($this->H->calls, 0);
63        $this->assertNotContains('header', $modes,
64            'A run of 7 `#` must not open an ATX heading');
65    }
66
67    function testHashTouchingTextIsNotAHeading()
68    {
69        $this->P->addMode('gfm_header', new GfmHeader());
70        $this->P->parse("#5 bolt\n\n#hashtag");
71        $modes = array_column($this->H->calls, 0);
72        $this->assertNotContains('header', $modes,
73            'A `#` directly followed by a non-space char must not open a heading');
74    }
75
76    function testEmptyHeading()
77    {
78        $this->P->addMode('gfm_header', new GfmHeader());
79        $this->P->parse("#\n");
80        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
81        $this->assertCount(1, $headerCalls, 'bare `#` must still emit a heading');
82        $call = array_values($headerCalls)[0];
83        $this->assertSame('', $call[1][0]);
84        $this->assertSame(1, $call[1][1]);
85    }
86
87    function testEmptyHeadingWithTrailingSpace()
88    {
89        $this->P->addMode('gfm_header', new GfmHeader());
90        $this->P->parse("## \n");
91        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
92        $call = array_values($headerCalls)[0];
93        $this->assertSame('', $call[1][0]);
94        $this->assertSame(2, $call[1][1]);
95    }
96
97    function testEmptyHeadingWithClosingHashes()
98    {
99        $this->P->addMode('gfm_header', new GfmHeader());
100        $this->P->parse("### ###\n");
101        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
102        $call = array_values($headerCalls)[0];
103        $this->assertSame('', $call[1][0]);
104        $this->assertSame(3, $call[1][1]);
105    }
106
107    function testOptionalClosingHashesStripped()
108    {
109        $this->P->addMode('gfm_header', new GfmHeader());
110        $this->P->parse("## foo ##\n");
111        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
112        $call = array_values($headerCalls)[0];
113        $this->assertSame('foo', $call[1][0]);
114        $this->assertSame(2, $call[1][1]);
115    }
116
117    function testClosingNeedNotMatchOpeningLength()
118    {
119        $this->P->addMode('gfm_header', new GfmHeader());
120        $this->P->parse("# foo ##################################\n");
121        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
122        $call = array_values($headerCalls)[0];
123        $this->assertSame('foo', $call[1][0]);
124        $this->assertSame(1, $call[1][1]);
125    }
126
127    function testTrailingSpacesAfterClosing()
128    {
129        $this->P->addMode('gfm_header', new GfmHeader());
130        $this->P->parse("### foo ###     \n");
131        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
132        $call = array_values($headerCalls)[0];
133        $this->assertSame('foo', $call[1][0]);
134        $this->assertSame(3, $call[1][1]);
135    }
136
137    function testClosingRunFollowedByTextIsNotClosing()
138    {
139        $this->P->addMode('gfm_header', new GfmHeader());
140        $this->P->parse("### foo ### b\n");
141        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
142        $call = array_values($headerCalls)[0];
143        $this->assertSame('foo ### b', $call[1][0]);
144        $this->assertSame(3, $call[1][1]);
145    }
146
147    function testClosingHashMustBePrecededBySpace()
148    {
149        $this->P->addMode('gfm_header', new GfmHeader());
150        $this->P->parse("# foo#\n");
151        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
152        $call = array_values($headerCalls)[0];
153        $this->assertSame('foo#', $call[1][0]);
154        $this->assertSame(1, $call[1][1]);
155    }
156
157    function testIndentedHashIsNotAHeading()
158    {
159        // GFM tolerates 0-3 spaces of indent; we do not. Any leading
160        // whitespace makes the line a paragraph (or preformatted, if
161        // it meets that mode's rules).
162        foreach ([1, 2, 3] as $indent) {
163            $this->setUp();
164            $this->P->addMode('gfm_header', new GfmHeader());
165            $this->P->parse(str_repeat(' ', $indent) . '### foo');
166            $modes = array_column($this->H->calls, 0);
167            $this->assertNotContains('header', $modes,
168                "indent=$indent must NOT open a heading");
169        }
170    }
171
172    function testContentInlineWhitespaceCollapsed()
173    {
174        $this->P->addMode('gfm_header', new GfmHeader());
175        $this->P->parse("#                  foo                     \n");
176        $headerCalls = array_filter($this->H->calls, static fn($c) => $c[0] === 'header');
177        $call = array_values($headerCalls)[0];
178        $this->assertSame('foo', $call[1][0]);
179    }
180
181    function testHeadingCanInterruptParagraph()
182    {
183        $this->P->addMode('gfm_header', new GfmHeader());
184        $this->P->addMode('eol', new Eol());
185        $this->P->parse("Foo bar\n# baz\nBar foo");
186        $modes = array_column($this->H->calls, 0);
187        $this->assertContains('header', $modes,
188            'ATX headings must interrupt paragraphs without requiring a blank line');
189    }
190
191    function testSortValue()
192    {
193        $mode = new GfmHeader();
194        $this->assertSame(50, $mode->getSort());
195    }
196}
197