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