1

I'm looking for a way to handle links to entries in a Redactor (Rich text) field such that a different class gets added to the <a> tag depending on the type of the entry.

For example, a link to an "event" entry type would be modified to look like: <a class="event-link" href="events/event-one">Event name</a>.

I assume that some kind of plugin would be needed for this, but I have been able to find anything similar I could learn from. So any pointers to this end would be really helpful!

Many thanks.

11

This is a high-level question, with multiple problems to solve. First, you'll need to parse the HTML from the Rich Text field for <a> tags. Then, you'll need to find a way to match the href attributes for those <a> tags to entries, and finally, you'll need to set classnames to the <a> tags (depending on the matched entry's Entry Type) and spit the rewritten HTML back out (saving it back to the entry if you want the classnames to persist).

Please note that the code in this answer isn't really tested, and there are multiple ways to skin your particular cat (or cats, as it were). Hopefully you'll take something away from it, though.

First, in order to parse the HTML for <a> tags, I'd recommend using DOMDocument, which is a built-in HTML parser (PHP5+). DOMDocument makes it super easy to find all the <a> tags in your HTML, and is much safer than using regular expressions. Here's how that could look:

$dom = new \DOMDocument();
$dom->loadHTML($html);
$links = $dom->getElementsByTagName('a');

Next step is looping through all the <a> tags; attempting to pull Entries from their href attributes. Note that those href attributes might be reference tags (e.g. {entry:11:url}) or either a relative or absolute URL, and ideally, your code should account for all of those variations:

foreach ($links as $link) {

    $href = $link->getAttribute('href');

    if (strpos($href, '{entry') !== false && strpos($href, 'url}') !== false) {
        // Reference tag – get entry by ID
        $reference = explode(':', $href);
        $entryId = (int) $reference[1];
        $entry = craft()->entries->getEntryById($entryId);
    } else {
        // URL. Make sure the site url is stripped and get entry by URI
        $uri = ltrim(str_replace(craft()->config->get('siteUrl');, '', $link->getAttribute('href')), '/');
        $entry = craft()->elements->getElementByUri($uri);  
    }

}

Finally, DOMDocument makes it super easy to add a classname back to the <a> tags, depending on the Entry Type:

// Add classnames based on the Entry Type handle
switch ($entry->type) {
    case 'work' :
        $link->setAttribute('class', 'work-link');
        break;
    case 'news' :
        $link->setAttribute('class', 'news-link');
        break;                    
}

One minor gotcha to note, is that DOMDocument will add HTML structural tags ('html', 'head', and 'body') to your HTML. A bit of regex takes care of that:

$html = preg_replace('~<(?:!DOCTYPE|/?(?:html|head|body))[^>]*>\s*~i', '', $dom->saveHTML());

Here's the complete code, for context:

// Create a new DOMDocument instance and find all <a> tags. I like to use mb_convert_encoding to ensure that the HTML content is UTF-8 encoded, but your mileage may vary
$dom = new \DOMDocument();
$dom->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$links = $dom->getElementsByTagName('a'); 

// Loop through the <a> tags
if ($links && $links->length > 0) {

    foreach ($links as $link) {

        // Try to get entry from href attribute
        $href = $link->getAttribute('href');

        if (strpos($href, '{entry') !== false && strpos($href, 'url}') !== false) {
            // Reference tag – get entry ID
            $reference = explode(':', $href);
            $entryId = (int) $reference[1];
            $entry = craft()->entries->getEntryById($entryId);
        } else {
            // URL. Make sure the site url is stripped
            $uri = ltrim(str_replace(craft()->config->get('siteUrl'), '', $link->getAttribute('href')), '/');
            $entry = craft()->elements->getElementByUri($uri);  
        }

        if ($entry && $entry->elementType == ElementType::Entry) {

            // Add classnames based on the Entry Type handle
            switch ($entry->type) {
                case 'work' :
                    $link->setAttribute('class', 'work-link');
                    break;
                case 'news' :
                    $link->setAttribute('class', 'news-link');
                    break;                    
            }

        }

    }

}

Phew. That takes care of the HTML, but depending on your need, you'll have to decide on an approach for when and where you want to rewrite your Rich Text content – i.e., where and when you'll place and call the above code (or something like it).

You have two options (it isn't clear from your question which of these you want):

a) Add the classnames when the entry is saved (permanent)
b) Add the classnames when the content is output in a template (not permanent)

If you want to add the classnames to links in the Rich Text content permanently, I'd recommend creating an event listener for the entries.saveEntry event, rewriting the HTML before re-saving the entry. The below would go in your plugin's primary class:

public function init() {
    parent::init();
    craft()->on('entries.saveEntry', array($this, 'onSaveEntry');
}

public function onSaveEntry(Event $e) {
    $entry = $e->params['entry'];
    $html = isset($entry->richTextField) ? $entry->richTextField->getRawContent() : false;
    if ($html) {
        ...
        $entry->getContent()->setAttribute('richTextField', $html);
        if ($entry->validate()) {
            craft()->elements->saveElement($entry);
        }
    }
}

For option b), I'd suggest writing a Template Variable method or a custom Twig Filter. A Template Variable would be a little bit easier, and would enable you to do the following in your template to print the HTML with the classes added:

{{ craft.myPlugin.addEntryTypeClasses(entry.richTextField) }}

A custom Twig filter offers a marginally more elegant template syntax:

{{ entry.richTextField|addEntryTypeClasses }}

Here, addEntryTypeClasses is a custom function (a template variable method or a Twig filter, respectively) that you'd have to create, where you could add the above code. To avoid the need to use the |raw filter in your template, add the following when you return the $html variable:

public function addEntryTypeClasses($html) {
    ...
    return TemplateHelper::getRaw($html);
}

In all of the above examples, ... is where you'd put the DOMDocument code. Ideally, all of that code would go in a custom Service method, making it easy to perform the DOMDocument operation on any HTML content from basically anywhere (Twig Filters, Variable methods, hooks and/or event listeners), as well as keeping your plugin code tidy and nice, but it isn't a must.

Keep in mind though that with option b), it would be a very good idea to cache the output, as the operation can be costly if you have a lot of links in your Rich Text field (Craft will have to hit the database for each link, every pageload).

Finally, if you don't fancy the DOMDocument approach, you could look at different approaches entirely – for instance, you could write a custom Redactor plugin, which could do basically the same thing as the above DOMDocument solution, only with JavaScript (jQuery) instead of PHP. Here's an example of a Craft Redactor plugin you could take a look at to get started: https://github.com/jpdevries/styles

If you're interested in more examples regarding custom Twig filters and DOMDocument specifically, I wrote a plugin called Retcon HTML which streamlines a lot of common HTML rewriting operations. It won't help with your specific issue, but the attr filter comes pretty close :)

  • 1
    It's a shame I can only up-vote once. – Brad Bell Dec 21 '15 at 1:06
  • Oh my @mmikkel, this is an amazing answer!! A shame indeed that there's only one up-vote allowed. I'm going to start working with all this now and I will let you know when I get something. Hopefully I can turn this into a proper plugin... – bravokiloecho Dec 21 '15 at 10:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.