1<?php 2 3namespace dokuwiki\Parsing\ParserMode; 4 5use dokuwiki\Parsing\Handler; 6use dokuwiki\Parsing\Lexer\Lexer; 7use dokuwiki\Parsing\ModeRegistry; 8 9/** 10 * Base class for inline formatting modes (bold, italic, underline, etc.) 11 * 12 * Each concrete subclass defines its entry/exit patterns, mode name, and sort order. 13 */ 14abstract class AbstractFormatting extends AbstractMode 15{ 16 /** 17 * Formatting modes accept other formatting, substitutions, and disabled modes. 18 * 19 * @inheritdoc 20 */ 21 protected function allowedCategories(): array 22 { 23 return [ 24 ModeRegistry::CATEGORY_FORMATTING, 25 ModeRegistry::CATEGORY_SUBSTITUTION, 26 ModeRegistry::CATEGORY_DISABLED, 27 ]; 28 } 29 30 /** 31 * Exclude self to prevent self-nesting (e.g. bold inside bold). 32 * 33 * @inheritdoc 34 */ 35 protected function filterAllowedModes(array $modes): array 36 { 37 $self = $this->getModeName(); 38 return array_values(array_filter($modes, static fn($mode) => $mode !== $self)); 39 } 40 41 /** @inheritdoc */ 42 public function connectTo($mode) 43 { 44 // Can't nest formatting in itself 45 if ($mode === $this->getModeName()) { 46 return; 47 } 48 49 $this->Lexer->addEntryPattern( 50 $this->getEntryPattern(), 51 $mode, 52 $this->getModeName() 53 ); 54 } 55 56 /** 57 * @return string The regex pattern that starts this formatting 58 */ 59 abstract protected function getEntryPattern(): string; 60 61 /** 62 * @return string The regex pattern that ends this formatting 63 */ 64 abstract protected function getExitPattern(): string; 65 66 /** 67 * Regex fragment matching a valid closer for this formatting: the 68 * closing delimiter itself, with flanking requirements expressed as 69 * lookarounds. A valid closer is exactly where the exit pattern would 70 * fire, so the default returns getExitPattern(). A subclass may 71 * override to demand more context than the exit does, or to return 72 * null for modes whose entry pattern already verifies its closer in 73 * linear time (e.g. a body that cannot contain the delimiter 74 * character, making the lookahead self-limiting). 75 * 76 * The pattern must match where the delimiter starts and must not 77 * consume flanking context: the lexer compares closer positions across 78 * modes to decide whether a nested delimiter may open, and a consumed 79 * flanking character would both skew that comparison and hide a closer 80 * that directly follows the opener (see Lexer::addCloserPattern()). 81 * 82 * When non-null, it is registered via Lexer::addCloserPattern() with the 83 * paragraph break as boundary: an opener candidate is accepted only when 84 * this fragment occurs between it and the next blank line, so an opener 85 * without a valid closer stays literal text and formatting never spans 86 * paragraphs. The entry pattern should then perform only cheap local 87 * checks (delimiter and flanking lookarounds); see addCloserPattern() 88 * for why the closer check must not live in the entry pattern itself. 89 * 90 * @return string|null 91 */ 92 protected function getCloserPattern(): ?string 93 { 94 return $this->getExitPattern(); 95 } 96 97 /** 98 * @return string The mode name used for lexer registration 99 */ 100 abstract protected function getModeName(): string; 101 102 /** 103 * @return string The name used for emitted open/close handler instructions 104 * 105 * Defaults to the mode name. Override in subclasses where the emitted 106 * instruction should differ from the lexer mode name (e.g. Gfm modes 107 * that share instructions with a DW counterpart). 108 */ 109 protected function getInstructionName(): string 110 { 111 return $this->getModeName(); 112 } 113 114 /** @inheritdoc */ 115 public function postConnect() 116 { 117 $this->Lexer->addExitPattern( 118 $this->getExitPattern(), 119 $this->getModeName() 120 ); 121 122 $closer = $this->getCloserPattern(); 123 if ($closer !== null) { 124 $this->Lexer->addCloserPattern($closer, $this->getModeName(), Lexer::PARA_BREAK); 125 } 126 } 127 128 /** @inheritdoc */ 129 public function handle($match, $state, $pos, Handler $handler) 130 { 131 $name = $this->getInstructionName(); 132 match ($state) { 133 DOKU_LEXER_ENTER => $handler->addCall($name . '_open', [], $pos), 134 DOKU_LEXER_EXIT => $handler->addCall($name . '_close', [], $pos), 135 DOKU_LEXER_UNMATCHED => $handler->addCall('cdata', [$match], $pos), 136 default => true, 137 }; 138 return true; 139 } 140} 141