1<?php 2 3namespace dokuwiki\Parsing\ParserMode; 4 5use dokuwiki\Parsing\Handler; 6 7/** 8 * GFM inline code span bounded by single backticks: `text`. 9 * 10 * A backtick span is both monospace-formatted and verbatim: the content 11 * is wrapped in monospace_open / monospace_close (the same instructions 12 * as DokuWiki's doubled-single-quote pair, rendered as an HTML <code> 13 * element) and the body is emitted through the unformatted handler 14 * rather than plain cdata, so renderers that distinguish the two 15 * (metadata, indexer, non-XHTML backends) treat it as literal. 16 * 17 * The entry pattern's lookahead only verifies three things: an opener, 18 * at least one body character, and a valid closer. It does NOT enforce 19 * non-whitespace body edges or a non-whitespace body interior. GFM's 20 * edge rules are applied in handle() after the body has been extracted: 21 * 22 * 1. Line endings become single spaces. 23 * 2. If the body both starts and ends with a space, and is not 24 * entirely whitespace, one space is stripped from each end. 25 * 26 * This lets the regex stay small while still producing GFM-correct 27 * output for the tricky cases: 28 * 29 * ` ` -> <code> </code> (all-whitespace body, no strip) 30 * ` a` -> <code> a</code> (asymmetric edge, no strip) 31 * ` `` ` -> <code>``</code> (run of 2 inside body, strip) 32 * 33 * Runs of two or more backticks on either delimiter are rejected by 34 * the length-boundary guards (?<!`)...(?!`), so this mode never steals 35 * input from GfmBacktickDouble. GfmBacktickDouble extends this class 36 * to reuse handle() and normalizeBody(). 37 * 38 * No other inline parsing runs inside a span; allowedModes is empty. 39 * 40 * @see GfmBacktickDouble 41 */ 42class GfmBacktickSingle extends AbstractMode 43{ 44 /** @inheritdoc */ 45 public function getSort() 46 { 47 return 165; 48 } 49 50 /** The lexer state / mode name. Subclasses override for n≥2. */ 51 protected function getModeName(): string 52 { 53 return 'gfm_backtick_single'; 54 } 55 56 /** 57 * Delimiter: a lone backtick. The length-boundary guards 58 * (?<!`)...(?!`) ensure a run of two or more backticks is never read 59 * as an n=1 opener or closer. 60 */ 61 protected function getDelimiterPattern(): string 62 { 63 return '(?<!`)`(?!`)'; 64 } 65 66 /** 67 * Span body. Admits runs of non-backticks, newlines that don't start 68 * a blank line, and runs of two-or-more backticks — the latter live 69 * inside the body since they cannot be valid n=1 closers. 70 * 71 * The alternatives start with mutually exclusive characters and all 72 * quantifiers are possessive, so a scan over the body never 73 * backtracks: it stops at the first lone backtick (or fails at the 74 * paragraph break) without accumulating backtracking state. 75 */ 76 protected function getBodyPattern(): string 77 { 78 return '(?:[^`\n]++|' . self::NOT_AT_PARA_BREAK . '\n|``++)++'; 79 } 80 81 /** 82 * Entry pattern: an opening delimiter whose lookahead verifies a body 83 * and a closing delimiter ahead. The lookahead is self-limiting — a 84 * lone backtick ahead is always a valid closer, so at most one scan 85 * per paragraph can fail — which keeps it linear without the memoized 86 * closer machinery of Lexer::addCloserPattern(). 87 */ 88 protected function getEntryPattern(): string 89 { 90 return $this->getDelimiterPattern() 91 . '(?=' . $this->getBodyPattern() . $this->getDelimiterPattern() . ')'; 92 } 93 94 /** Exit pattern: the same delimiter that opened the span. */ 95 protected function getExitPattern(): string 96 { 97 return $this->getDelimiterPattern(); 98 } 99 100 /** @inheritdoc */ 101 public function connectTo($mode) 102 { 103 $this->Lexer->addEntryPattern( 104 $this->getEntryPattern(), 105 $mode, 106 $this->getModeName() 107 ); 108 } 109 110 /** @inheritdoc */ 111 public function postConnect() 112 { 113 $this->Lexer->addExitPattern($this->getExitPattern(), $this->getModeName()); 114 } 115 116 /** @inheritdoc */ 117 public function handle($match, $state, $pos, Handler $handler) 118 { 119 match ($state) { 120 DOKU_LEXER_ENTER => $handler->addCall('monospace_open', [], $pos), 121 DOKU_LEXER_EXIT => $handler->addCall('monospace_close', [], $pos), 122 DOKU_LEXER_UNMATCHED => $handler->addCall( 123 'unformatted', 124 [$this->normalizeBody($match)], 125 $pos 126 ), 127 default => true, 128 }; 129 return true; 130 } 131 132 /** 133 * GFM code-span body normalization: newlines become spaces; if both 134 * ends are spaces and the body isn't entirely whitespace, strip one 135 * space from each end. 136 */ 137 protected function normalizeBody(string $body): string 138 { 139 $body = str_replace(["\r\n", "\r", "\n"], ' ', $body); 140 if ( 141 strlen($body) >= 2 142 && $body[0] === ' ' 143 && $body[-1] === ' ' 144 && trim($body) !== '' 145 ) { 146 $body = substr($body, 1, -1); 147 } 148 return $body; 149 } 150} 151