Как разобрать вложенные контейнеры с помощью Commonmark for PHP?

Я пытаюсь создать спойлер, используя пакет 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;
    }
}

Renderer

<?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"

1 ответ

Решение

В этом сценарии 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), вероятно, ваш лучший вариант.

Надеюсь, это поможет!

Другие вопросы по тегам