1<?php 2 3namespace dokuwiki\Parsing\ParserMode; 4 5use dokuwiki\Parsing\Handler; 6 7/** 8 * GFM ATX heading: 1-6 leading `#` characters, a mandatory space (or end of 9 * line for an empty heading), and optional body; emits the same 10 * header / section_open / section_close instructions as DokuWiki's Header 11 * so renderers and TOC treat it identically. 12 * 13 * Setext headings (=== / --- underlines) are deliberately not supported — 14 * they collide with DokuWiki's horizontal rule and heading delimiters. 15 * 16 * Leading indentation is also not supported: GFM allows 0-3 spaces before 17 * the opener, but DokuWiki uses 2-space indent for Preformatted blocks 18 * and that collision isn't worth untangling for a tolerance feature. The 19 * opener must sit at column 0. 20 */ 21class GfmHeader extends AbstractMode 22{ 23 /** @inheritdoc */ 24 public function getSort() 25 { 26 return 50; 27 } 28 29 /** @inheritdoc */ 30 public function connectTo($mode) 31 { 32 // Entry pattern breakdown: 33 // \n — line start (Parser prepends a newline) 34 // #{1,6}(?!#) — 1-6 `#` characters; the lookahead 35 // rejects 7+ so `####### foo` stays as 36 // paragraph text 37 // (?:[ \t][^\n]*)? — optional body starting with a space 38 // or tab; a hash touching a letter 39 // (`#hashtag`) has no body match and 40 // the `(?=\n)` below fails unless the 41 // whole line is just the hashes 42 // (?=\n) — must end the line 43 $this->Lexer->addSpecialPattern( 44 '\n#{1,6}(?!#)(?:[ \t][^\n]*)?(?=\n)', 45 $mode, 46 'gfm_header' 47 ); 48 } 49 50 /** @inheritdoc */ 51 public function handle($match, $state, $pos, Handler $handler) 52 { 53 $line = ltrim($match, "\n"); 54 $level = strspn($line, '#'); 55 $title = trim(substr($line, $level)); 56 57 // Optional closing `#` run. The sequence must be preceded by 58 // whitespace; a `#` touching the body (`# foo#`) is content. 59 // A body that is nothing but `#`s is a closer with no title. 60 if ($title !== '' && preg_match('/^#+$/', $title)) { 61 $title = ''; 62 } elseif (preg_match('/^(.*?)[ \t]+#+$/', $title, $m)) { 63 $title = rtrim($m[1]); 64 } 65 66 if ($handler->getStatus('section')) { 67 $handler->addCall('section_close', [], $pos); 68 } 69 $handler->addCall('header', [$title, $level, $pos], $pos); 70 $handler->addCall('section_open', [$level], $pos); 71 $handler->setStatus('section', true); 72 return true; 73 } 74} 75