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