1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the league/commonmark package. 7 * 8 * (c) Colin O'Dell <colinodell@gmail.com> 9 * 10 * Original code based on the CommonMark JS reference parser (http://bitly.com/commonmark-js) 11 * - (c) John MacFarlane 12 * 13 * For the full copyright and license information, please view the LICENSE 14 * file that was distributed with this source code. 15 */ 16 17namespace League\CommonMark\Extension\SmartPunct; 18 19use League\CommonMark\Delimiter\Delimiter; 20use League\CommonMark\Parser\Inline\InlineParserInterface; 21use League\CommonMark\Parser\Inline\InlineParserMatch; 22use League\CommonMark\Parser\InlineParserContext; 23use League\CommonMark\Util\RegexHelper; 24 25final class QuoteParser implements InlineParserInterface 26{ 27 public const DOUBLE_QUOTES = [Quote::DOUBLE_QUOTE, Quote::DOUBLE_QUOTE_OPENER, Quote::DOUBLE_QUOTE_CLOSER]; 28 public const SINGLE_QUOTES = [Quote::SINGLE_QUOTE, Quote::SINGLE_QUOTE_OPENER, Quote::SINGLE_QUOTE_CLOSER]; 29 30 public function getMatchDefinition(): InlineParserMatch 31 { 32 return InlineParserMatch::oneOf(...\array_merge(self::DOUBLE_QUOTES, self::SINGLE_QUOTES)); 33 } 34 35 /** 36 * Normalizes any quote characters found and manually adds them to the delimiter stack 37 */ 38 public function parse(InlineParserContext $inlineContext): bool 39 { 40 $char = $inlineContext->getFullMatch(); 41 $cursor = $inlineContext->getCursor(); 42 43 $normalizedCharacter = $this->getNormalizedQuoteCharacter($char); 44 45 $charBefore = $cursor->peek(-1); 46 if ($charBefore === null) { 47 $charBefore = "\n"; 48 } 49 50 $cursor->advance(); 51 52 $charAfter = $cursor->getCurrentCharacter(); 53 if ($charAfter === null) { 54 $charAfter = "\n"; 55 } 56 57 [$leftFlanking, $rightFlanking] = $this->determineFlanking($charBefore, $charAfter); 58 $canOpen = $leftFlanking && ! $rightFlanking; 59 $canClose = $rightFlanking; 60 61 $node = new Quote($normalizedCharacter, ['delim' => true]); 62 $inlineContext->getContainer()->appendChild($node); 63 64 // Add entry to stack to this opener 65 $inlineContext->getDelimiterStack()->push(new Delimiter($normalizedCharacter, 1, $node, $canOpen, $canClose)); 66 67 return true; 68 } 69 70 private function getNormalizedQuoteCharacter(string $character): string 71 { 72 if (\in_array($character, self::DOUBLE_QUOTES, true)) { 73 return Quote::DOUBLE_QUOTE; 74 } 75 76 if (\in_array($character, self::SINGLE_QUOTES, true)) { 77 return Quote::SINGLE_QUOTE; 78 } 79 80 return $character; 81 } 82 83 /** 84 * @return bool[] 85 */ 86 private function determineFlanking(string $charBefore, string $charAfter): array 87 { 88 $afterIsWhitespace = \preg_match('/\pZ|\s/u', $charAfter); 89 $afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter); 90 $beforeIsWhitespace = \preg_match('/\pZ|\s/u', $charBefore); 91 $beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore); 92 93 $leftFlanking = ! $afterIsWhitespace && 94 ! ($afterIsPunctuation && 95 ! $beforeIsWhitespace && 96 ! $beforeIsPunctuation); 97 98 $rightFlanking = ! $beforeIsWhitespace && 99 ! ($beforeIsPunctuation && 100 ! $afterIsWhitespace && 101 ! $afterIsPunctuation); 102 103 return [$leftFlanking, $rightFlanking]; 104 } 105} 106