PageRenderTime 43ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 0ms

/vendor/s9e/text-formatter/src/Plugins/Litedown/Parser.php

https://bitbucket.org/cmwdosp/cmwbb3
PHP | 735 lines | 729 code | 1 blank | 5 comment | 116 complexity | 3f02313082e483dca6e86b29df9831f3 MD5 | raw file
Possible License(s): BSD-3-Clause
  1. <?php
  2. /*
  3. * @package s9e\TextFormatter
  4. * @copyright Copyright (c) 2010-2017 The s9e Authors
  5. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  6. */
  7. namespace s9e\TextFormatter\Plugins\Litedown;
  8. use s9e\TextFormatter\Parser as Rules;
  9. use s9e\TextFormatter\Parser\Tag;
  10. use s9e\TextFormatter\Plugins\ParserBase;
  11. class Parser extends ParserBase
  12. {
  13. protected $hasEscapedChars;
  14. protected $hasRefs;
  15. protected $refs;
  16. protected $text;
  17. public function parse($text, array $matches)
  18. {
  19. $this->init($text);
  20. $this->matchBlockLevelMarkup();
  21. $this->matchLinkReferences();
  22. $this->matchInlineCode();
  23. $this->matchImages();
  24. $this->matchLinks();
  25. $this->matchStrikethrough();
  26. $this->matchSuperscript();
  27. $this->matchEmphasis();
  28. $this->matchForcedLineBreaks();
  29. unset($this->text);
  30. }
  31. protected function addImageTag($startTagPos, $endTagPos, $endTagLen, $linkInfo, $alt)
  32. {
  33. $tag = $this->parser->addTagPair('IMG', $startTagPos, 2, $endTagPos, $endTagLen);
  34. $this->setLinkAttributes($tag, $linkInfo, 'src');
  35. $tag->setAttribute('alt', $this->decode($alt));
  36. $this->overwrite($startTagPos, $endTagPos + $endTagLen - $startTagPos);
  37. }
  38. protected function addInlineCodeTags($left, $right)
  39. {
  40. $startTagPos = $left['pos'];
  41. $startTagLen = $left['len'] + $left['trimAfter'];
  42. $endTagPos = $right['pos'] - $right['trimBefore'];
  43. $endTagLen = $right['len'] + $right['trimBefore'];
  44. $this->parser->addTagPair('C', $startTagPos, $startTagLen, $endTagPos, $endTagLen);
  45. $this->overwrite($startTagPos, $endTagPos + $endTagLen - $startTagPos);
  46. }
  47. protected function addLinkTag($startTagPos, $endTagPos, $endTagLen, $linkInfo)
  48. {
  49. $priority = ($endTagLen === 1) ? 1 : -1;
  50. $tag = $this->parser->addTagPair('URL', $startTagPos, 1, $endTagPos, $endTagLen, $priority);
  51. $this->setLinkAttributes($tag, $linkInfo, 'url');
  52. $this->overwrite($startTagPos, 1);
  53. $this->overwrite($endTagPos, $endTagLen);
  54. }
  55. protected function closeList(array $list, $textBoundary)
  56. {
  57. $this->parser->addEndTag('LIST', $textBoundary, 0)->pairWith($list['listTag']);
  58. $this->parser->addEndTag('LI', $textBoundary, 0)->pairWith($list['itemTag']);
  59. if ($list['tight'])
  60. foreach ($list['itemTags'] as $itemTag)
  61. $itemTag->removeFlags(Rules::RULE_CREATE_PARAGRAPHS);
  62. }
  63. protected function computeQuoteIgnoreLen($str, $maxQuoteDepth)
  64. {
  65. $remaining = $str;
  66. while (--$maxQuoteDepth >= 0)
  67. $remaining = \preg_replace('/^ *> ?/', '', $remaining);
  68. return \strlen($str) - \strlen($remaining);
  69. }
  70. protected function decode($str)
  71. {
  72. if ($this->config['decodeHtmlEntities'] && \strpos($str, '&') !== \false)
  73. $str = \html_entity_decode($str, \ENT_QUOTES, 'UTF-8');
  74. $str = \str_replace("\x1A", '', $str);
  75. if ($this->hasEscapedChars)
  76. $str = \strtr(
  77. $str,
  78. array(
  79. "\x1B0" => '!', "\x1B1" => '"', "\x1B2" => "'", "\x1B3" => '(',
  80. "\x1B4" => ')', "\x1B5" => '*', "\x1B6" => '[', "\x1B7" => '\\',
  81. "\x1B8" => ']', "\x1B9" => '^', "\x1BA" => '_', "\x1BB" => '`',
  82. "\x1BC" => '~'
  83. )
  84. );
  85. return $str;
  86. }
  87. protected function encode($str)
  88. {
  89. return \strtr(
  90. $str,
  91. array(
  92. '\\!' => "\x1B0", '\\"' => "\x1B1", "\\'" => "\x1B2", '\\(' => "\x1B3",
  93. '\\)' => "\x1B4", '\\*' => "\x1B5", '\\[' => "\x1B6", '\\\\' => "\x1B7",
  94. '\\]' => "\x1B8", '\\^' => "\x1B9", '\\_' => "\x1BA", '\\`' => "\x1BB",
  95. '\\~' => "\x1BC"
  96. )
  97. );
  98. }
  99. protected function getAtxHeaderEndTagLen($startPos, $endPos)
  100. {
  101. $content = \substr($this->text, $startPos, $endPos - $startPos);
  102. \preg_match('/[ \\t]*#*[ \\t]*$/', $content, $m);
  103. return \strlen($m[0]);
  104. }
  105. protected function getSetextLines()
  106. {
  107. $setextLines = array();
  108. if (\strpos($this->text, '-') === \false && \strpos($this->text, '=') === \false)
  109. return $setextLines;
  110. $regexp = '/^(?=[-=>])(?:> ?)*(?=[-=])(?:-+|=+) *$/m';
  111. if (\preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE))
  112. foreach ($matches[0] as $_f570d26d)
  113. {
  114. list($match, $matchPos) = $_f570d26d;
  115. $endTagPos = $matchPos - 1;
  116. while ($endTagPos > 0 && $this->text[$endTagPos - 1] === ' ')
  117. --$endTagPos;
  118. $setextLines[$matchPos - 1] = array(
  119. 'endTagLen' => $matchPos + \strlen($match) - $endTagPos,
  120. 'endTagPos' => $endTagPos,
  121. 'quoteDepth' => \substr_count($match, '>'),
  122. 'tagName' => ($match[0] === '=') ? 'H1' : 'H2'
  123. );
  124. }
  125. return $setextLines;
  126. }
  127. protected function getEmphasisByBlock($regexp, $pos)
  128. {
  129. $block = array();
  130. $blocks = array();
  131. $breakPos = \strpos($this->text, "\x17", $pos);
  132. \preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE, $pos);
  133. foreach ($matches[0] as $m)
  134. {
  135. $matchPos = $m[1];
  136. $matchLen = \strlen($m[0]);
  137. if ($matchPos > $breakPos)
  138. {
  139. $blocks[] = $block;
  140. $block = array();
  141. $breakPos = \strpos($this->text, "\x17", $matchPos);
  142. }
  143. if (!$this->ignoreEmphasis($matchPos, $matchLen))
  144. $block[] = array($matchPos, $matchLen);
  145. }
  146. $blocks[] = $block;
  147. return $blocks;
  148. }
  149. protected function getInlineCodeMarkers()
  150. {
  151. $pos = \strpos($this->text, '`');
  152. if ($pos === \false)
  153. return array();
  154. \preg_match_all(
  155. '/(`+)(\\s*)[^\\x17`]*/',
  156. \str_replace("\x1BB", '\\`', $this->text),
  157. $matches,
  158. \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER,
  159. $pos
  160. );
  161. $trimNext = 0;
  162. $markers = array();
  163. foreach ($matches as $m)
  164. {
  165. $markers[] = array(
  166. 'pos' => $m[0][1],
  167. 'len' => \strlen($m[1][0]),
  168. 'trimBefore' => $trimNext,
  169. 'trimAfter' => \strlen($m[2][0]),
  170. 'next' => $m[0][1] + \strlen($m[0][0])
  171. );
  172. $trimNext = \strlen($m[0][0]) - \strlen(\rtrim($m[0][0]));
  173. }
  174. return $markers;
  175. }
  176. protected function getLabels()
  177. {
  178. \preg_match_all(
  179. '/\\[((?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*)\\]/',
  180. $this->text,
  181. $matches,
  182. \PREG_OFFSET_CAPTURE
  183. );
  184. $labels = array();
  185. foreach ($matches[1] as $m)
  186. $labels[$m[1] - 1] = \strtolower($m[0]);
  187. return $labels;
  188. }
  189. protected function ignoreEmphasis($matchPos, $matchLen)
  190. {
  191. return ($this->text[$matchPos] === '_' && $matchLen === 1 && $this->isSurroundedByAlnum($matchPos, $matchLen));
  192. }
  193. protected function init($text)
  194. {
  195. if (\strpos($text, '\\') === \false || !\preg_match('/\\\\[!"\'()*[\\\\\\]^_`~]/', $text))
  196. $this->hasEscapedChars = \false;
  197. else
  198. {
  199. $this->hasEscapedChars = \true;
  200. $text = $this->encode($text);
  201. }
  202. $text .= "\n\n\x17";
  203. $this->text = $text;
  204. }
  205. protected function isAlnum($chr)
  206. {
  207. return (\strpos(' abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', $chr) > 0);
  208. }
  209. protected function isSurroundedByAlnum($matchPos, $matchLen)
  210. {
  211. return ($matchPos > 0 && $this->isAlnum($this->text[$matchPos - 1]) && $this->isAlnum($this->text[$matchPos + $matchLen]));
  212. }
  213. protected function markBoundary($pos)
  214. {
  215. $this->text[$pos] = "\x17";
  216. }
  217. protected function matchBlockLevelMarkup()
  218. {
  219. $codeFence = \null;
  220. $codeIndent = 4;
  221. $codeTag = \null;
  222. $lineIsEmpty = \true;
  223. $lists = array();
  224. $listsCnt = 0;
  225. $newContext = \false;
  226. $quotes = array();
  227. $quotesCnt = 0;
  228. $setextLines = $this->getSetextLines();
  229. $textBoundary = 0;
  230. $regexp = '/^(?:(?=[-*+\\d \\t>`~#_])((?: {0,3}> ?)+)?([ \\t]+)?(\\* *\\* *\\*[* ]*$|- *- *-[- ]*$|_ *_ *_[_ ]*$|=+$)?((?:[-*+]|\\d+\\.)[ \\t]+(?=\\S))?[ \\t]*(#{1,6}[ \\t]+|```+[^`\\n]*$|~~~+[^~\\n]*$)?)?/m';
  231. \preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
  232. foreach ($matches as $m)
  233. {
  234. $matchPos = $m[0][1];
  235. $matchLen = \strlen($m[0][0]);
  236. $ignoreLen = 0;
  237. $quoteDepth = 0;
  238. $continuation = !$lineIsEmpty;
  239. $lfPos = \strpos($this->text, "\n", $matchPos);
  240. $lineIsEmpty = ($lfPos === $matchPos + $matchLen && empty($m[3][0]) && empty($m[4][0]) && empty($m[5][0]));
  241. $breakParagraph = ($lineIsEmpty && $continuation);
  242. if (!empty($m[1][0]))
  243. {
  244. $quoteDepth = \substr_count($m[1][0], '>');
  245. $ignoreLen = \strlen($m[1][0]);
  246. if (isset($codeTag) && $codeTag->hasAttribute('quoteDepth'))
  247. {
  248. $quoteDepth = \min($quoteDepth, $codeTag->getAttribute('quoteDepth'));
  249. $ignoreLen = $this->computeQuoteIgnoreLen($m[1][0], $quoteDepth);
  250. }
  251. $this->overwrite($matchPos, $ignoreLen);
  252. }
  253. if ($quoteDepth < $quotesCnt && !$continuation)
  254. {
  255. $newContext = \true;
  256. do
  257. {
  258. $this->parser->addEndTag('QUOTE', $textBoundary, 0)
  259. ->pairWith(\array_pop($quotes));
  260. }
  261. while ($quoteDepth < --$quotesCnt);
  262. }
  263. if ($quoteDepth > $quotesCnt && !$lineIsEmpty)
  264. {
  265. $newContext = \true;
  266. do
  267. {
  268. $tag = $this->parser->addStartTag('QUOTE', $matchPos, 0, $quotesCnt - 999);
  269. $quotes[] = $tag;
  270. }
  271. while ($quoteDepth > ++$quotesCnt);
  272. }
  273. $indentWidth = 0;
  274. $indentPos = 0;
  275. if (!empty($m[2][0]) && !$codeFence)
  276. {
  277. $indentStr = $m[2][0];
  278. $indentLen = \strlen($indentStr);
  279. do
  280. {
  281. if ($indentStr[$indentPos] === ' ')
  282. ++$indentWidth;
  283. else
  284. $indentWidth = ($indentWidth + 4) & ~3;
  285. }
  286. while (++$indentPos < $indentLen && $indentWidth < $codeIndent);
  287. }
  288. if (isset($codeTag) && !$codeFence && $indentWidth < $codeIndent && !$lineIsEmpty)
  289. $newContext = \true;
  290. if ($newContext)
  291. {
  292. $newContext = \false;
  293. if (isset($codeTag))
  294. {
  295. $this->overwrite($codeTag->getPos(), $textBoundary - $codeTag->getPos());
  296. $endTag = $this->parser->addEndTag('CODE', $textBoundary, 0, -1);
  297. $endTag->pairWith($codeTag);
  298. $codeTag = \null;
  299. $codeFence = \null;
  300. }
  301. foreach ($lists as $list)
  302. $this->closeList($list, $textBoundary);
  303. $lists = array();
  304. $listsCnt = 0;
  305. if ($matchPos)
  306. $this->markBoundary($matchPos - 1);
  307. }
  308. if ($indentWidth >= $codeIndent)
  309. {
  310. if (isset($codeTag) || !$continuation)
  311. {
  312. $ignoreLen += $indentPos;
  313. if (!isset($codeTag))
  314. $codeTag = $this->parser->addStartTag('CODE', $matchPos + $ignoreLen, 0, -999);
  315. $m = array();
  316. }
  317. }
  318. else
  319. {
  320. $hasListItem = !empty($m[4][0]);
  321. if (!$indentWidth && !$continuation && !$hasListItem)
  322. $listIndex = -1;
  323. elseif ($continuation && !$hasListItem)
  324. $listIndex = $listsCnt - 1;
  325. elseif (!$listsCnt)
  326. if ($hasListItem && (!$continuation || $this->text[$matchPos - 1] === "\x17"))
  327. $listIndex = 0;
  328. else
  329. $listIndex = -1;
  330. else
  331. {
  332. $listIndex = 0;
  333. while ($listIndex < $listsCnt && $indentWidth > $lists[$listIndex]['maxIndent'])
  334. ++$listIndex;
  335. }
  336. while ($listIndex < $listsCnt - 1)
  337. {
  338. $this->closeList(\array_pop($lists), $textBoundary);
  339. --$listsCnt;
  340. }
  341. if ($listIndex === $listsCnt && !$hasListItem)
  342. --$listIndex;
  343. if ($hasListItem && $listIndex >= 0)
  344. {
  345. $breakParagraph = \true;
  346. $tagPos = $matchPos + $ignoreLen + $indentPos;
  347. $tagLen = \strlen($m[4][0]);
  348. $itemTag = $this->parser->addStartTag('LI', $tagPos, $tagLen);
  349. $this->overwrite($tagPos, $tagLen);
  350. if ($listIndex < $listsCnt)
  351. {
  352. $this->parser->addEndTag('LI', $textBoundary, 0)
  353. ->pairWith($lists[$listIndex]['itemTag']);
  354. $lists[$listIndex]['itemTag'] = $itemTag;
  355. $lists[$listIndex]['itemTags'][] = $itemTag;
  356. }
  357. else
  358. {
  359. ++$listsCnt;
  360. if ($listIndex)
  361. {
  362. $minIndent = $lists[$listIndex - 1]['maxIndent'] + 1;
  363. $maxIndent = \max($minIndent, $listIndex * 4);
  364. }
  365. else
  366. {
  367. $minIndent = 0;
  368. $maxIndent = $indentWidth;
  369. }
  370. $listTag = $this->parser->addStartTag('LIST', $tagPos, 0);
  371. if (\strpos($m[4][0], '.') !== \false)
  372. {
  373. $listTag->setAttribute('type', 'decimal');
  374. $start = (int) $m[4][0];
  375. if ($start !== 1)
  376. $listTag->setAttribute('start', $start);
  377. }
  378. $lists[] = array(
  379. 'listTag' => $listTag,
  380. 'itemTag' => $itemTag,
  381. 'itemTags' => array($itemTag),
  382. 'minIndent' => $minIndent,
  383. 'maxIndent' => $maxIndent,
  384. 'tight' => \true
  385. );
  386. }
  387. }
  388. if ($listsCnt && !$continuation && !$lineIsEmpty)
  389. if (\count($lists[0]['itemTags']) > 1 || !$hasListItem)
  390. {
  391. foreach ($lists as &$list)
  392. $list['tight'] = \false;
  393. unset($list);
  394. }
  395. $codeIndent = ($listsCnt + 1) * 4;
  396. }
  397. if (isset($m[5]))
  398. {
  399. if ($m[5][0][0] === '#')
  400. {
  401. $startTagLen = \strlen($m[5][0]);
  402. $startTagPos = $matchPos + $matchLen - $startTagLen;
  403. $endTagLen = $this->getAtxHeaderEndTagLen($matchPos + $matchLen, $lfPos);
  404. $endTagPos = $lfPos - $endTagLen;
  405. $this->parser->addTagPair('H' . \strspn($m[5][0], '#', 0, 6), $startTagPos, $startTagLen, $endTagPos, $endTagLen);
  406. $this->markBoundary($startTagPos);
  407. $this->markBoundary($lfPos);
  408. if ($continuation)
  409. $breakParagraph = \true;
  410. }
  411. elseif ($m[5][0][0] === '`' || $m[5][0][0] === '~')
  412. {
  413. $tagPos = $matchPos + $ignoreLen;
  414. $tagLen = $lfPos - $tagPos;
  415. if (isset($codeTag) && $m[5][0] === $codeFence)
  416. {
  417. $endTag = $this->parser->addEndTag('CODE', $tagPos, $tagLen, -1);
  418. $endTag->pairWith($codeTag);
  419. $this->parser->addIgnoreTag($textBoundary, $tagPos - $textBoundary);
  420. $this->overwrite($codeTag->getPos(), $tagPos + $tagLen - $codeTag->getPos());
  421. $codeTag = \null;
  422. $codeFence = \null;
  423. }
  424. elseif (!isset($codeTag))
  425. {
  426. $codeTag = $this->parser->addStartTag('CODE', $tagPos, $tagLen);
  427. $codeFence = \substr($m[5][0], 0, \strspn($m[5][0], '`~'));
  428. $codeTag->setAttribute('quoteDepth', $quoteDepth);
  429. $this->parser->addIgnoreTag($tagPos + $tagLen, 1);
  430. $lang = \trim(\trim($m[5][0], '`~'));
  431. if ($lang !== '')
  432. $codeTag->setAttribute('lang', $lang);
  433. }
  434. }
  435. }
  436. elseif (!empty($m[3][0]) && !$listsCnt && $this->text[$matchPos + $matchLen] !== "\x17")
  437. {
  438. $this->parser->addSelfClosingTag('HR', $matchPos + $ignoreLen, $matchLen - $ignoreLen);
  439. $breakParagraph = \true;
  440. $this->markBoundary($lfPos);
  441. }
  442. elseif (isset($setextLines[$lfPos]) && $setextLines[$lfPos]['quoteDepth'] === $quoteDepth && !$lineIsEmpty && !$listsCnt && !isset($codeTag))
  443. {
  444. $this->parser->addTagPair(
  445. $setextLines[$lfPos]['tagName'],
  446. $matchPos + $ignoreLen,
  447. 0,
  448. $setextLines[$lfPos]['endTagPos'],
  449. $setextLines[$lfPos]['endTagLen']
  450. );
  451. $this->markBoundary($setextLines[$lfPos]['endTagPos'] + $setextLines[$lfPos]['endTagLen']);
  452. }
  453. if ($breakParagraph)
  454. {
  455. $this->parser->addParagraphBreak($textBoundary);
  456. $this->markBoundary($textBoundary);
  457. }
  458. if (!$lineIsEmpty)
  459. $textBoundary = $lfPos;
  460. if ($ignoreLen)
  461. $this->parser->addIgnoreTag($matchPos, $ignoreLen, 1000);
  462. }
  463. }
  464. protected function matchEmphasis()
  465. {
  466. $this->matchEmphasisByCharacter('*', '/\\*+/');
  467. $this->matchEmphasisByCharacter('_', '/_+/');
  468. }
  469. protected function matchEmphasisByCharacter($character, $regexp)
  470. {
  471. $pos = \strpos($this->text, $character);
  472. if ($pos === \false)
  473. return;
  474. foreach ($this->getEmphasisByBlock($regexp, $pos) as $block)
  475. $this->processEmphasisBlock($block);
  476. }
  477. protected function matchForcedLineBreaks()
  478. {
  479. $pos = \strpos($this->text, " \n");
  480. while ($pos !== \false)
  481. {
  482. $this->parser->addBrTag($pos + 2);
  483. $pos = \strpos($this->text, " \n", $pos + 3);
  484. }
  485. }
  486. protected function matchImages()
  487. {
  488. $pos = \strpos($this->text, '![');
  489. if ($pos === \false)
  490. return;
  491. if (\strpos($this->text, '](', $pos) !== \false)
  492. $this->matchInlineImages();
  493. if ($this->hasRefs)
  494. $this->matchReferenceImages();
  495. }
  496. protected function matchInlineImages()
  497. {
  498. \preg_match_all(
  499. '/!\\[(?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*\\]\\(( *(?:[^\\x17\\s()]|\\([^\\x17\\s()]*\\))*(?=[ )]) *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))? *)\\)/',
  500. $this->text,
  501. $matches,
  502. \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
  503. );
  504. foreach ($matches as $m)
  505. {
  506. $linkInfo = $m[1][0];
  507. $startTagPos = $m[0][1];
  508. $endTagLen = 3 + \strlen($linkInfo);
  509. $endTagPos = $startTagPos + \strlen($m[0][0]) - $endTagLen;
  510. $alt = \substr($m[0][0], 2, \strlen($m[0][0]) - $endTagLen - 2);
  511. $this->addImageTag($startTagPos, $endTagPos, $endTagLen, $linkInfo, $alt);
  512. }
  513. }
  514. protected function matchReferenceImages()
  515. {
  516. \preg_match_all(
  517. '/!\\[((?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*)\\](?: ?\\[([^\\x17[\\]]+)\\])?/',
  518. $this->text,
  519. $matches,
  520. \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
  521. );
  522. foreach ($matches as $m)
  523. {
  524. $startTagPos = $m[0][1];
  525. $endTagPos = $startTagPos + 2 + \strlen($m[1][0]);
  526. $endTagLen = 1;
  527. $alt = $m[1][0];
  528. $id = $alt;
  529. if (isset($m[2][0], $this->refs[$m[2][0]]))
  530. {
  531. $endTagLen = \strlen($m[0][0]) - \strlen($alt) - 2;
  532. $id = $m[2][0];
  533. }
  534. elseif (!isset($this->refs[$id]))
  535. continue;
  536. $this->addImageTag($startTagPos, $endTagPos, $endTagLen, $this->refs[$id], $alt);
  537. }
  538. }
  539. protected function matchInlineCode()
  540. {
  541. $markers = $this->getInlineCodeMarkers();
  542. $i = -1;
  543. $cnt = \count($markers);
  544. while (++$i < ($cnt - 1))
  545. {
  546. $pos = $markers[$i]['next'];
  547. $j = $i;
  548. if ($this->text[$markers[$i]['pos']] !== '`')
  549. {
  550. ++$markers[$i]['pos'];
  551. --$markers[$i]['len'];
  552. }
  553. while (++$j < $cnt && $markers[$j]['pos'] === $pos)
  554. {
  555. if ($markers[$j]['len'] === $markers[$i]['len'])
  556. {
  557. $this->addInlineCodeTags($markers[$i], $markers[$j]);
  558. $i = $j;
  559. break;
  560. }
  561. $pos = $markers[$j]['next'];
  562. }
  563. }
  564. }
  565. protected function matchInlineLinks()
  566. {
  567. \preg_match_all(
  568. '/\\[(?:[^\\x17[\\]]|\\[[^\\x17[\\]]*\\])*\\]\\(( *(?:[^\\x17\\s()]|\\([^\\x17\\s()]*\\))*(?=[ )]) *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))? *)\\)/',
  569. $this->text,
  570. $matches,
  571. \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER
  572. );
  573. foreach ($matches as $m)
  574. {
  575. $linkInfo = $m[1][0];
  576. $startTagPos = $m[0][1];
  577. $endTagLen = 3 + \strlen($linkInfo);
  578. $endTagPos = $startTagPos + \strlen($m[0][0]) - $endTagLen;
  579. $this->addLinkTag($startTagPos, $endTagPos, $endTagLen, $linkInfo);
  580. }
  581. }
  582. protected function matchLinkReferences()
  583. {
  584. $this->hasRefs = \false;
  585. $this->refs = array();
  586. if (\strpos($this->text, ']:') === \false)
  587. return;
  588. $regexp = '/^\\x1A* {0,3}\\[([^\\x17\\]]+)\\]: *([^\\s\\x17]+ *(?:"[^\\x17]*?"|\'[^\\x17]*?\'|\\([^\\x17)]*\\))?)[^\\x17\\n]*\\n?/m';
  589. \preg_match_all($regexp, $this->text, $matches, \PREG_OFFSET_CAPTURE | \PREG_SET_ORDER);
  590. foreach ($matches as $m)
  591. {
  592. $this->parser->addIgnoreTag($m[0][1], \strlen($m[0][0]), -2);
  593. $id = \strtolower($m[1][0]);
  594. if (isset($this->refs[$id]))
  595. continue;
  596. $this->hasRefs = \true;
  597. $this->refs[$id] = $m[2][0];
  598. }
  599. }
  600. protected function matchLinks()
  601. {
  602. if (\strpos($this->text, '](') !== \false)
  603. $this->matchInlineLinks();
  604. if ($this->hasRefs)
  605. $this->matchReferenceLinks();
  606. }
  607. protected function matchReferenceLinks()
  608. {
  609. $labels = $this->getLabels();
  610. foreach ($labels as $startTagPos => $id)
  611. {
  612. $labelPos = $startTagPos + 2 + \strlen($id);
  613. $endTagPos = $labelPos - 1;
  614. $endTagLen = 1;
  615. if ($this->text[$labelPos] === ' ')
  616. ++$labelPos;
  617. if (isset($labels[$labelPos], $this->refs[$labels[$labelPos]]))
  618. {
  619. $id = $labels[$labelPos];
  620. $endTagLen = $labelPos + 2 + \strlen($id) - $endTagPos;
  621. }
  622. if (isset($this->refs[$id]))
  623. $this->addLinkTag($startTagPos, $endTagPos, $endTagLen, $this->refs[$id]);
  624. }
  625. }
  626. protected function matchStrikethrough()
  627. {
  628. $pos = \strpos($this->text, '~~');
  629. if ($pos === \false)
  630. return;
  631. \preg_match_all(
  632. '/~~[^\\x17]+?~~/',
  633. $this->text,
  634. $matches,
  635. \PREG_OFFSET_CAPTURE,
  636. $pos
  637. );
  638. foreach ($matches[0] as $_4b034d25)
  639. {
  640. list($match, $matchPos) = $_4b034d25;
  641. $matchLen = \strlen($match);
  642. $this->parser->addTagPair('DEL', $matchPos, 2, $matchPos + $matchLen - 2, 2);
  643. }
  644. }
  645. protected function matchSuperscript()
  646. {
  647. $pos = \strpos($this->text, '^');
  648. if ($pos === \false)
  649. return;
  650. \preg_match_all(
  651. '/\\^[^\\x17\\s]++/',
  652. $this->text,
  653. $matches,
  654. \PREG_OFFSET_CAPTURE,
  655. $pos
  656. );
  657. foreach ($matches[0] as $_4b034d25)
  658. {
  659. list($match, $matchPos) = $_4b034d25;
  660. $matchLen = \strlen($match);
  661. $startTagPos = $matchPos;
  662. $endTagPos = $matchPos + $matchLen;
  663. $parts = \explode('^', $match);
  664. unset($parts[0]);
  665. foreach ($parts as $part)
  666. {
  667. $this->parser->addTagPair('SUP', $startTagPos, 1, $endTagPos, 0);
  668. $startTagPos += 1 + \strlen($part);
  669. }
  670. }
  671. }
  672. protected function overwrite($pos, $len)
  673. {
  674. $this->text = \substr($this->text, 0, $pos) . \str_repeat("\x1A", $len) . \substr($this->text, $pos + $len);
  675. }
  676. protected function processEmphasisBlock(array $block)
  677. {
  678. $buffered = 0;
  679. $emPos = -1;
  680. $strongPos = -1;
  681. foreach ($block as $_aab3a45e)
  682. {
  683. list($matchPos, $matchLen) = $_aab3a45e;
  684. $closeLen = \min(3, $matchLen);
  685. $closeEm = $closeLen & $buffered & 1;
  686. $closeStrong = $closeLen & $buffered & 2;
  687. $emEndPos = $matchPos;
  688. $strongEndPos = $matchPos;
  689. if ($buffered > 2 && $emPos === $strongPos)
  690. if ($closeEm)
  691. $emPos += 2;
  692. else
  693. ++$strongPos;
  694. if ($closeEm && $closeStrong)
  695. if ($emPos < $strongPos)
  696. $emEndPos += 2;
  697. else
  698. ++$strongEndPos;
  699. $remaining = $matchLen;
  700. if ($closeEm)
  701. {
  702. --$buffered;
  703. --$remaining;
  704. $this->parser->addTagPair('EM', $emPos, 1, $emEndPos, 1);
  705. }
  706. if ($closeStrong)
  707. {
  708. $buffered -= 2;
  709. $remaining -= 2;
  710. $this->parser->addTagPair('STRONG', $strongPos, 2, $strongEndPos, 2);
  711. }
  712. $remaining = \min(3, $remaining);
  713. if ($remaining & 1)
  714. $emPos = $matchPos + $matchLen - $remaining;
  715. if ($remaining & 2)
  716. $strongPos = $matchPos + $matchLen - $remaining;
  717. $buffered += $remaining;
  718. }
  719. }
  720. protected function setLinkAttributes(Tag $tag, $linkInfo, $attrName)
  721. {
  722. $url = \trim($linkInfo);
  723. $title = '';
  724. $pos = \strpos($url, ' ');
  725. if ($pos !== \false)
  726. {
  727. $title = \substr(\trim(\substr($url, $pos)), 1, -1);
  728. $url = \substr($url, 0, $pos);
  729. }
  730. $tag->setAttribute($attrName, $this->decode($url));
  731. if ($title > '')
  732. $tag->setAttribute('title', $this->decode($title));
  733. }
  734. }