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