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