Delimiter Processing

Delimiter processors allow you to implement delimiter runs the same way the core library implements emphasis.

Delimiter runs are a special type of inline:

This is an example of **emphasis**. Note how the text is *wrapped* with the same character(s) before and after. 

When implementing something with these characteristics you should consider leveraging delimiter runs; otherwise, a basic inline parser should be sufficient.

Delimiter Priority

Delimiter processors have a lower priority than inline parsers - if an inline parser successfully handles the same special character you’re interested in then your delimiter processor will not be called.

Implementing Standalone Delimiter Processors

Implement the DelimiterProcessorInterface and add it to your environment:

$environment->addDelimiterProcessor(new MyCustomDelimiterProcessor());

getOpeningCharacter() and getClosingCharacter()

These two methods tell the engine which characters are used to delineate your custom syntax. Generally these will be the same, such as when using *emphasis*, but they can be different; for example, maybe you want to use {this syntax}. Simply tell the engine which characters you’d like to use.

getMinimumLength()

This method tells the engine the minimum number of characters needed to match or “activate” your processor. For example, if you want to match {{example}} and not {example}, set this to 2.

getDelimiterUse()

public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int;

This method is used to tell the engine how many characters from the matching delimiters should be consumed. For simple processors you’ll likely return 1 (or whatever your minimum length is). In more advanced cases, you can examine the opening and closing delimiters and perform additional logic to determine whether they should be fully or partially consumed. You can also return 0 if you’d like.

process()

public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse);

This is where the magic happens. Once the engine determines it can use the delimiter it found (by looking at all the other methods above) it’ll call this method. Your job is to take everything between the $opener and $closer and wrap that in whatever custom inline element you’d like. Here’s a basic example of wrapping the inner contents inside a new Emphasis element:

// Create the outer element
$emphasis = new Emphasis();

// Add everything between $opener and $closer (exclusive) to the new outer element
$tmp = $opener->next();
while ($tmp !== null && $tmp !== $closer) {
    $next = $tmp->next();
    $emphasis->appendChild($tmp);
    $tmp = $next;
}

// Place the outer element into the AST
$opener->insertAfter($emphasis);

Note that $opener and $closer will be automatically removed for you after this function returns - no need to do that yourself.

Combining Inline Parsers with Delimiter Processors

Basic delimiter processors, as covered above, do not require any custom inline parsers - they’ll “just work”. But in some rare cases you may want to pair it with a custom inline parser: the inline parser will identify the delimiter, adding an entry to the delimiter stack for the processor to process later. Note that this is an advanced use case and you probably don’t need this. But if you do then read on.

Inline Parsers and the Delimiter Stack

As your identifies potential delimiter-based inlines, it should create a new AbstractStringContainer node (either Text or something custom) with the inner contents and also push a new DelimiterInterface onto the DelimiterStack:

$node = new Text($cursor->getPreviousText(), [
    'delim' => true,
]);
$inlineContext->getContainer()->appendChild($node);

// Add entry to stack to this opener
$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose);
$inlineContext->getDelimiterStack()->push($delimiter);

This basically tells the engine that text was found which might be emphasis, but due to the delimiter run rules we can’t make that determination just yet. That final determination is later on by a “delimiter processor”.

Your implementation of the delimiter processor won’t look any different in this approach - you’ll still need to implement all of the same methods especially process(). The difference is that you’ve identified where the delimiter is, instead of relying on the engine to do this for you.