<?php
/**
* @package s9e\TextFormatter
* @copyright Copyright (c) 2010-2021 The s9e authors
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
*/
namespace s9e\TextFormatter\Plugins\Litedown\Parser\Passes;
class Emphasis extends AbstractPass
{
/**
* @var bool Whether current EM span is being closed by current emphasis mark
*/
protected $closeEm;
/**
* @var bool Whether current EM span is being closed by current emphasis mark
*/
protected $closeStrong;
/**
* @var integer Starting position of the current EM span in the text
*/
protected $emPos;
/**
* @var integer Ending position of the current EM span in the text
*/
protected $emEndPos;
/**
* @var integer Number of emphasis characters unused in current span
*/
protected $remaining;
/**
* @var integer Starting position of the current STRONG span in the text
*/
protected $strongPos;
/**
* @var integer Ending position of the current STRONG span in the text
*/
protected $strongEndPos;
/**
* {@inheritdoc}
*/
public function parse()
{
$this->parseEmphasisByCharacter('*', '/\\*+/');
$this->parseEmphasisByCharacter('_', '/_+/');
}
/**
* Adjust the ending position of current EM and STRONG spans
*
* @return void
*/
protected function adjustEndingPositions()
{
if ($this->closeEm && $this->closeStrong)
{
if ($this->emPos < $this->strongPos)
{
$this->emEndPos += 2;
}
else
{
++$this->strongEndPos;
}
}
}
/**
* Adjust the starting position of current EM and STRONG spans
*
* If both EM and STRONG are set to start at the same position, we adjust their position
* to match the order they are closed. If they start and end at the same position, STRONG
* starts before EM to match Markdown's behaviour
*
* @return void
*/
protected function adjustStartingPositions()
{
if ($this->emPos >= 0 && $this->emPos === $this->strongPos)
{
if ($this->closeEm)
{
$this->emPos += 2;
}
else
{
++$this->strongPos;
}
}
}
/**
* End current valid EM and STRONG spans
*
* @return void
*/
protected function closeSpans()
{
if ($this->closeEm)
{
--$this->remaining;
$this->parser->addTagPair('EM', $this->emPos, 1, $this->emEndPos, 1);
$this->emPos = -1;
}
if ($this->closeStrong)
{
$this->remaining -= 2;
$this->parser->addTagPair('STRONG', $this->strongPos, 2, $this->strongEndPos, 2);
$this->strongPos = -1;
}
}
/**
* Parse emphasis and strong applied using given character
*
* @param string $character Markup character, either * or _
* @param string $regexp Regexp used to match the series of emphasis character
* @return void
*/
protected function parseEmphasisByCharacter($character, $regexp)
{
$pos = $this->text->indexOf($character);
if ($pos === false)
{
return;
}
foreach ($this->getEmphasisByBlock($regexp, $pos) as $block)
{
$this->processEmphasisBlock($block);
}
}
/**
* Get emphasis markup split by block
*
* @param string $regexp Regexp used to match emphasis
* @param integer $pos Position in the text of the first emphasis character
* @return array[] Each array contains a list of [matchPos, matchLen] pairs
*/
protected function getEmphasisByBlock($regexp, $pos)
{
$block = [];
$blocks = [];
$breakPos = $this->text->indexOf("\x17", $pos);
preg_match_all($regexp, $this->text, $matches, PREG_OFFSET_CAPTURE, $pos);
foreach ($matches[0] as $m)
{
$matchPos = $m[1];
$matchLen = strlen($m[0]);
// Test whether we've just passed the limits of a block
if ($matchPos > $breakPos)
{
$blocks[] = $block;
$block = [];
$breakPos = $this->text->indexOf("\x17", $matchPos);
}
// Test whether we should ignore this markup
if (!$this->ignoreEmphasis($matchPos, $matchLen))
{
$block[] = [$matchPos, $matchLen];
}
}
$blocks[] = $block;
return $blocks;
}
/**
* Test whether emphasis should be ignored at the given position in the text
*
* @param integer $matchPos Position of the emphasis in the text
* @param integer $matchLen Length of the emphasis
* @return bool
*/
protected function ignoreEmphasis($matchPos, $matchLen)
{
// Ignore single underscores between alphanumeric characters
return ($this->text->charAt($matchPos) === '_' && $matchLen === 1 && $this->text->isSurroundedByAlnum($matchPos, $matchLen));
}
/**
* Open EM and STRONG spans whose content starts at given position
*
* @param integer $pos
* @return void
*/
protected function openSpans($pos)
{
if ($this->remaining & 1)
{
$this->emPos = $pos - $this->remaining;
}
if ($this->remaining & 2)
{
$this->strongPos = $pos - $this->remaining;
}
}
/**
* Process a list of emphasis markup strings
*
* @param array[] $block List of [matchPos, matchLen] pairs
* @return void
*/
protected function processEmphasisBlock(array $block)
{
$this->emPos = -1;
$this->strongPos = -1;
foreach ($block as list($matchPos, $matchLen))
{
$this->processEmphasisMatch($matchPos, $matchLen);
}
}
/**
* Process an emphasis mark
*
* @param integer $matchPos
* @param integer $matchLen
* @return void
*/
protected function processEmphasisMatch($matchPos, $matchLen)
{
$canOpen = !$this->text->isBeforeWhitespace($matchPos + $matchLen - 1);
$canClose = !$this->text->isAfterWhitespace($matchPos);
$closeLen = ($canClose) ? min($matchLen, 3) : 0;
$this->closeEm = ($closeLen & 1) && $this->emPos >= 0;
$this->closeStrong = ($closeLen & 2) && $this->strongPos >= 0;
$this->emEndPos = $matchPos;
$this->strongEndPos = $matchPos;
$this->remaining = $matchLen;
$this->adjustStartingPositions();
$this->adjustEndingPositions();
$this->closeSpans();
// Adjust the length of unused markup remaining in current match
$this->remaining = ($canOpen) ? min($this->remaining, 3) : 0;
$this->openSpans($matchPos + $matchLen);
}
}