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'] = 'md'; 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