How to create an extension to parse multiple line for thephpleague/commonmark

299 Views Asked by At

All,

I am trying to create an extension (to extend AbstractBlockParser, I believe) which will find an opening tag, move the lines following into a block, and then stop when a closing tag is found (on its own on a last line).

While looking at the examples provided, it is extremely hard to work out how they build a block composed of multiple lines, e.g. using a code fence, and the documentation does not cover this scenario.

The list parser code seems to show ListItems being added to a ListBlock, but how does it know when to stop?

Example Markdown

{{ Object ID
Any markdown goes here.
Some more
  * List 1
  * List 2
}}

And the output might be:

<div class="object">
    <span>Object ID</span>
    <p>Any markdown goes here.
    Some more</p>
    <ul>
        <li>List 1</li>
        <li>List 2</li>
    </ul>
</div>
1

There are 1 best solutions below

2
On BEST ANSWER

The trick is that your Block should have the canContain() and matchesNextLine() methods always return true; - these will ensure that subsequent lines always get added as child blocks. (Take a look at the FencedCode and ListBlock implementations.)

Here's some code which should work:

ObjectBlock.php:

class ObjectBlock extends AbstractBlock
{
    private $objectId;

    public function __construct($objectId)
    {
        $this->objectId = $objectId;
    }

    public function getObjectId()
    {
        return $this->objectId;
    }

    public function canContain(AbstractBlock $block)
    {
        return true;
    }

    public function acceptsLines()
    {
        return false;
    }

    public function isCode()
    {
        return false;
    }

    public function matchesNextLine(Cursor $cursor)
    {
        return true;
    }
}

ObjectParser.php:

class ObjectParser extends AbstractBlockParser
{
    public function parse(ContextInterface $context, Cursor $cursor)
    {
        // Look for the starting syntax
        if ($cursor->match('/^{{ /')) {
            $id = $cursor->getRemainder();
            $cursor->advanceToEnd();

            $context->addBlock(new ObjectBlock($id));

            return true;
        // Look for the ending syntax
        } elseif ($cursor->match('/^}} +$/')) {
            // TODO: I don't know if this is the best approach, but it should work
            // Basically, we're going to locate a parent ObjectBlock in the AST...
            $container = $context->getContainer();
            while ($container) {
                if ($container instanceof ObjectBlock) {
                    $cursor->advanceToEnd();

                    // Found it!  Now we'll close everything up to (and including) it
                    $context->getBlockCloser()->setLastMatchedContainer($container->parent());
                    $context->getBlockCloser()->closeUnmatchedBlocks();
                    $context->setBlocksParsed(true);

                    return true;
                }

                $container = $container->parent();
            }
        }

        return false;
    }
}

ObjectRenderer:

class ObjectRenderer implements BlockRendererInterface
{
    public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, $inTightList = false)
    {
        $span = sprintf('<span>%s</span>', $block->getObjectId());
        $contents = $htmlRenderer->renderBlocks($block->children());

        return new HtmlElement('div', ['class' => 'object'],
            $span . $contents
        );
    }
}

Disclaimer: Although I am the author of this library, the particular logic (containers, tips, and block-closing) was mostly forked as-is from the JS version and I only understand about 75% of it - just enough to keep my fork working and figure out approaches that work :)