Vanilla allows you to customize content formatting by adding HTML processors. These processors can modify the content of a post or article before it's rendered on the page.
HTML Processor Fundamentals
The first step to adding an HTML processor to Vanilla's content formatting pipeline is to...create the HTML processor. To do this, you'll need to create a new PHP class that extends Vanilla\Formatting\Html\Processor\HtmlProcessor
. This parent class will contain most of the functionality you'll need to get started. You'll primarily be responsible for implementing your own processDocument
method. This method is responsible for taking an object representation of an HTML document, modifying it and returning the updated value.
Registering a new HTML processor requires an addon. The HTML processor class will live in the addon directory. The addon's primary class will be used to register the HTML processor with Vanilla's formatting service via a hook.
Example HTML Processor
Here's an example of a basic HTML processor.
<?php
namespace Vanilla\MyAddon\Formatting\Processors;
use DOMText;
use Vanilla\Formatting\Html\HtmlDocument;
use Vanilla\Formatting\Html\Processor\HtmlProcessor;
class CustomProcessor extends HtmlProcessor {
private const REPLACE_PATTERN = "/\[giphy ([A-Z0-9]+)\]/i";
private const XPATH_QUERY = '/html/body//text()[not(ancestor::a) and (contains(.,"[giphy "))]';
/**
* @inheritDoc
*/
public function processDocument(HtmlDocument $document): HtmlDocument {
$nodes = $document->queryXPath(self::XPATH_QUERY);
/** @var DOMText $node */
foreach ($nodes as $node) {
$fragment = $document->getDom()->createDocumentFragment();
$fragment->appendXML(
preg_replace(
self::REPLACE_PATTERN,
'<img src="https://media.giphy.com/media/$1/giphy.gif" />',
$node->data
)
);
$node->parentNode->replaceChild($fragment, $node);
}
return $document;
}
}
The above processor will look for occurrences of [giphy {ID}]
in content, where {ID} is the unique alphanumeric ID of a GIPHY image, and replace them with an image tag of the GIF.
How It Works
As mentioned, the processor only needs one method: processDocument
. This method is automatically invoked for registered HTML processors as part of the content formatting pipeline. It takes one parameter: $document
. This is an instance of Vanilla\Formatting\Html\HtmlDocument
and represents a piece of content that has been rendered as HTML. A discussion written in Markdown would be rendered as HTML, then passed to this method as an instance of HtmlDocument
. The processDocument
would only be aware of the HTML representation. The original Markdown would be inaccessible to the HTML processor.
Breaking it down a little more, let's take a look at the first line of this method.
$nodes = $document->queryXPath(self::XPATH_QUERY);
We perform an XPath query to look for text nodes in the document containing our GIPHY tag code. Because we're explicitly querying for text nodes, the return value will be an array of DOMText
instances. Your own XPath query results may vary. Effectively utilizing XPath is beyond the scope of this document, but Devhints' XPath Cheatsheet is a great resource.
Now that we have some nodes to target, we can begin modifying them in a basic loop.
$fragment = $document->getDom()->createDocumentFragment();
The first operation in our loop is to create an empty DOMDocumentFragment
, attached in our HTML document. This will ultimately be used to replace the text node we're currently targeting.
$fragment->appendXML(
preg_replace(
self::REPLACE_PATTERN,
'<img src="https://media.giphy.com/media/$1/giphy.gif" />',
$node->data
)
);
After nabbing the content of our text node, using its data
property, we make our replacements using a simple regular expression and PHP's preg_replace
function. The result of this call is used to set the content of our newly-created DOMDocumentFragment
. At this point, we're ready to commit our substitution to the document.
$node->parentNode->replaceChild($fragment, $node);
In the final step, we replace the current DOMText
instance with our DOMDocumentFragment
in the document. This officially applies the image tag replacement to the content being formatted.
Manipulation of the DOM
The ability to fully harness the potential of HTML processors in Vanilla largely depends on your familiarity with PHP's DOM library, particularly DOMDocument
and its related classes. The Vanilla\Formatting\Html\HtmlDocument
class provides some utility and convenience methods to help developers more effectively utilize these DOM objects. Beyond reading up on PHP's own DOM library, you should checkout the public methods provided by the HtmlDocument
class and those provided by its traits. There is very likely to be some method there to reduce the effort required to build your HTML processor.
In addition to PHP's DOMDocument
and Vanilla's HtmlDocument
, Vanilla also has a utility class for working with the DOM: Vanilla\Formatting\Html\DomUtils
. This class is a collection of methods for performing common tasks on an instance of DOMDocument
.
Registering Your HTML Processor
We have our processor. All that's left to do is register it. This is a very simple process, using Vanilla's container to ensure the processor will be registered with the formatting service. If you don't already have one in your addon, you'll need to implement a container_init
hook. Here's a simplified example:
public function container_init(Container $dic): void {
$dic->rule(BaseFormat::class)
->addCall("addHtmlProcessor", [new Reference(\Vanilla\MyAddon\Formatting\Processors\CustomProcessor::class)]);
}
This rule adds a call to addHtmlProcessor
for all formatters instantiated by the container, providing our Vanilla\MyAddon\Formatting\Processors\CustomProcessor
class from the above example as the new processor. The formatters will automatically use the new processor on the HTML they generate.