Я пытаюсь создать блок спойлера, используя лиги Пакет CommonMark.
Блок открывается тремя инвертированными восклицательными знаками, за которыми, возможно, следует сводка; три нормальных восклицательных знака завершают блок.
Это код, который я до сих пор:
<?php
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;
class Spoiler extends AbstractBlock {
private $summary;
public function __construct($summary = null) {
parent::__construct();
$this->summary = $summary;
}
public function getSummary() { return $this->summary; }
public function canContain(AbstractBlock $block) { return true; }
public function acceptsLines() { return true; }
public function isCode() { return false; }
public function matchesNextLine(Cursor $cursor) {
if ($cursor->match('(^!!!$)')) {
$this->lastLineBlank = true;
return false;
}
return true;
}
}
<?php
use League\CommonMark\Block\Parser\AbstractBlockParser;
use League\CommonMark\ContextInterface;
use League\CommonMark\Cursor;
class SpoilerParser extends AbstractBlockParser {
public function parse(ContextInterface $context, Cursor $cursor) {
if ($cursor->isIndented()) return false;
$previousState = $cursor->saveState();
$spoiler = $cursor->match('(^¡¡¡(\s*.+)?)');
if (is_null($spoiler)) {
$cursor->restoreState($previousState);
return false;
}
$summary = trim(mb_substr($spoiler, mb_strlen('¡¡¡')));
if ($summary !== '') {
$context->addBlock(new Spoiler($summary));
} else {
$context->addBlock(new Spoiler());
}
return true;
}
}
<?php
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\HtmlElement;
class SpoilerRenderer implements BlockRendererInterface {
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, $inTightList = false) {
if (!($block instanceof Spoiler)) throw new \InvalidArgumentException('Incompatible block type: ' . get_class($block));
$summary = new HtmlElement('summary', [], $block->getSummary() ?: 'Click to expand spoiler');
$content = $summary . "\n" . $htmlRenderer->renderBlocks($block->children());
return new HtmlElement('details', [], $content);
}
}
Проблема возникает, когда я вкладываю несколько блоков спойлера: первый терминатор закрывает все блоки.
¡¡¡
1
¡¡¡
2
¡¡¡
Hello
!!!
3
!!!
4
!!!
Это проанализированный AST:
League\CommonMark\Block\Element\Document
App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "1"App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "2"App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "Hello"League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "3"League\CommonMark\Inline\Element\Newline
League\CommonMark\Inline\Element\Text "!!!"League\CommonMark\Inline\Element\Newline
League\CommonMark\Inline\Element\Text "4"League\CommonMark\Inline\Element\Newline
League\CommonMark\Inline\Element\Text "!!!"
Это ожидаемый АСТ:
League\CommonMark\Block\Element\Document
App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "1"App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "2"App\Helpers\Formatting\Element\Spoiler
League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "Hello"League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "3"League\CommonMark\Block\Element\Paragraph
League\CommonMark\Inline\Element\Text "4"
В этом сценарии matchesNextLine()
всегда будет работать на верхнем уровне Spoiler
основанный на том, как DocParser::resetContainer()
перебирает AST. Вместо этого я бы рекомендовал использовать SpoilerParser::parse()
проверить на конечный синтаксис. Например, вы можете добавить что-то вроде этого в существующий парсер:
if ($cursor->match('/^!!!$/')) {
$container = $context->getContainer();
do {
if ($container instanceof Spoiler) {
$context->setContainer($container);
$context->setTip($container);
$context->getBlockCloser()->setLastMatchedContainer($container);
return true;
}
} while ($container = $container->parent());
}
Это, кажется, производит ожидаемый результат:
<details><summary>Click to expand spoiler</summary>
<p>1</p>
<details><summary>Click to expand spoiler</summary>
<p>2</p>
<details><summary>Click to expand spoiler</summary>
<p>Hello</p></details>
<p>3</p></details>
<p>4</p></details>
<p></p>
Отказ от ответственности: Хотя AST, вероятно, является правильным на основе этого вывода, я не проверял сам AST. Я также не проверял, отрицательно ли влияет мое предложение на процесс синтаксического анализа, что может вызвать проблемы с другими элементами или более глубокое вложение, поэтому вы можете захотеть отследить это. Но это общий подход (разбор !!!
в парсере и манипулировании контекстом / AST), вероятно, ваш лучший вариант.
Надеюсь, это поможет!
Других решений пока нет …