xref: /dokuwiki/inc/Parsing/ParserMode/Preformatted.php (revision 945681fc9df0c43e5065e67d03a08d832e81e66c)
1<?php
2
3namespace dokuwiki\Parsing\ParserMode;
4
5use dokuwiki\Parsing\Handler;
6use dokuwiki\Parsing\Handler\Preformatted as PreformattedHandler;
7
8class Preformatted extends AbstractMode
9{
10    /** @inheritdoc */
11    public function getSort()
12    {
13        return 20;
14    }
15
16    /**
17     * Number of leading spaces that trigger a preformatted block.
18     *
19     * DokuWiki's historical value is 2 spaces; Markdown uses 4. When the
20     * active parse prefers Markdown (md or md+dw) we flip to 4 so indented
21     * code blocks match GFM. A single tab is always a trigger regardless
22     * of the space threshold.
23     */
24    protected function getIndentWidth(): int
25    {
26        return $this->registry->isMdPreferred() ? 4 : 2;
27    }
28
29    /** @inheritdoc */
30    public function connectTo($mode)
31    {
32        $indent = str_repeat(' ', $this->getIndentWidth());
33
34        // An indented line is ambiguous with a list item, so the entry
35        // patterns must not swallow a line whose indent begins a list - the
36        // list has to win. In base mode sort order already gives the list mode
37        // the tie, but table mode connects preformatted as a protected
38        // sub-mode without connecting any list mode, so this lookahead is the
39        // only thing that lets an indented list end the table there (in every
40        // syntax the table exists in, including the mixed dw+md / md+dw
41        // combinations). The guard tracks whichever list mode is actually
42        // loaded. DokuWiki's Listblock treats a bare * or - as a marker even
43        // with no following space, so the guard rejects those two characters
44        // outright. Markdown's GfmListblock only starts a list when one of its
45        // markers - a bullet -, *, + or a digit run ending in . or ) - is
46        // followed by whitespace or the end of the line, so the guard mirrors
47        // that and still lets an indented code block that merely starts with
48        // such a character (e.g. a *** line) through.
49        $listGuard = $this->registry->isMdPreferred()
50            ? '(?!(?:[-*+]|\d{1,9}[.)])[ \t\n])'
51            : '(?![\*\-])';
52
53        $this->Lexer->addEntryPattern('\n' . $indent . $listGuard, $mode, 'preformatted');
54        $this->Lexer->addEntryPattern('\n\t' . $listGuard, $mode, 'preformatted');
55
56        // match continuation lines inside the preformatted block
57        $this->Lexer->addPattern('\n' . $indent, 'preformatted');
58        $this->Lexer->addPattern('\n\t', 'preformatted');
59    }
60
61    /** @inheritdoc */
62    public function postConnect()
63    {
64        // Two exits: a zero-width lookahead when the next line starts with
65        // non-whitespace content (so the boundary \n stays in the stream
66        // and downstream block-level modes like GfmHr or GfmHeader can
67        // anchor on it), and a consuming \n fall-through for blank lines
68        // and end-of-input. The lookahead-only branch is registered first
69        // so PCRE's leftmost-first alternation prefers it whenever it
70        // applies; the consuming branch handles the cases where it cannot.
71        $this->Lexer->addExitPattern('(?=\n[^ \t\n])', 'preformatted');
72        $this->Lexer->addExitPattern('\n', 'preformatted');
73    }
74
75    /** @inheritdoc */
76    public function handle($match, $state, $pos, Handler $handler)
77    {
78        switch ($state) {
79            case DOKU_LEXER_ENTER:
80                $handler->setCallWriter(new PreformattedHandler($handler->getCallWriter()));
81                $handler->addCall('preformatted_start', [], $pos);
82                break;
83            case DOKU_LEXER_EXIT:
84                $handler->addCall('preformatted_end', [], $pos);
85                /** @var PreformattedHandler $reWriter */
86                $reWriter = $handler->getCallWriter();
87                $handler->setCallWriter($reWriter->process());
88                break;
89            case DOKU_LEXER_MATCHED:
90                $handler->addCall('preformatted_newline', [], $pos);
91                break;
92            case DOKU_LEXER_UNMATCHED:
93                $handler->addCall('preformatted_content', [$match], $pos);
94                break;
95        }
96
97        return true;
98    }
99}
100