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