xref: /dokuwiki/inc/Parsing/ParserMode/GfmHeader.php (revision 8719732d06ab7306149725c7c5ea71deb8ff0382)
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