1<?php 2 3declare(strict_types=1); 4 5namespace League\CommonMark\Delimiter; 6 7use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection; 8use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface; 9use League\CommonMark\Node\Inline\Text; 10use League\CommonMark\Parser\Inline\InlineParserInterface; 11use League\CommonMark\Parser\Inline\InlineParserMatch; 12use League\CommonMark\Parser\InlineParserContext; 13use League\CommonMark\Util\RegexHelper; 14 15/** 16 * Delimiter parsing is implemented as an Inline Parser with the lowest-possible priority 17 * 18 * @internal 19 */ 20final class DelimiterParser implements InlineParserInterface 21{ 22 private DelimiterProcessorCollection $collection; 23 24 public function __construct(DelimiterProcessorCollection $collection) 25 { 26 $this->collection = $collection; 27 } 28 29 public function getMatchDefinition(): InlineParserMatch 30 { 31 return InlineParserMatch::oneOf(...$this->collection->getDelimiterCharacters()); 32 } 33 34 public function parse(InlineParserContext $inlineContext): bool 35 { 36 $character = $inlineContext->getFullMatch(); 37 $numDelims = 0; 38 $cursor = $inlineContext->getCursor(); 39 $processor = $this->collection->getDelimiterProcessor($character); 40 41 \assert($processor !== null); // Delimiter processor should never be null here 42 43 $charBefore = $cursor->peek(-1); 44 if ($charBefore === null) { 45 $charBefore = "\n"; 46 } 47 48 while ($cursor->peek($numDelims) === $character) { 49 ++$numDelims; 50 } 51 52 if ($numDelims < $processor->getMinLength()) { 53 return false; 54 } 55 56 $cursor->advanceBy($numDelims); 57 58 $charAfter = $cursor->getCurrentCharacter(); 59 if ($charAfter === null) { 60 $charAfter = "\n"; 61 } 62 63 [$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $processor); 64 65 $node = new Text(\str_repeat($character, $numDelims), [ 66 'delim' => true, 67 ]); 68 $inlineContext->getContainer()->appendChild($node); 69 70 // Add entry to stack to this opener 71 if ($canOpen || $canClose) { 72 $delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose); 73 $inlineContext->getDelimiterStack()->push($delimiter); 74 } 75 76 return true; 77 } 78 79 /** 80 * @return bool[] 81 */ 82 private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array 83 { 84 $afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter); 85 $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter); 86 $beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore); 87 $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore); 88 89 $leftFlanking = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation); 90 $rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation); 91 92 if ($character === '_') { 93 $canOpen = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation); 94 $canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation); 95 } else { 96 $canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter(); 97 $canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter(); 98 } 99 100 return [$canOpen, $canClose]; 101 } 102} 103