<p>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 <code>&lt;a&gt;</code> tags. Then, you'll need to find a way to match the <code>href</code> attributes for those <code>&lt;a&gt;</code> tags to entries, and finally, you'll need to set classnames to the <code>&lt;a&gt;</code> 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).</p>

<p>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.</p>

<p>First, in order to parse the HTML for <code>&lt;a&gt;</code> tags, I'd recommend using <a href="http://php.net/manual/en/class.domdocument.php" rel="noreferrer">DOMDocument</a>, which is a built-in HTML parser (PHP5+). DOMDocument makes it super easy to find all the <code>&lt;a&gt;</code> tags in your HTML, and is much safer than using regular expressions. Here's how that could look:</p>

<pre><code>$dom = new \DOMDocument();
$dom-&gt;loadHTML($html);
$links = $dom-&gt;getElementsByTagName('a');
</code></pre>

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

<pre><code>foreach ($links as $link) {

    $href = $link-&gt;getAttribute('href');

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

}
</code></pre>

<p>Finally, DOMDocument makes it super easy to add a classname back to the <code>&lt;a&gt;</code> tags, depending on the Entry Type:</p>

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

<p>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:</p>

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

<p>Here's the complete code, for context:</p>

<pre><code>// Create a new DOMDocument instance and find all &lt;a&gt; 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-&gt;loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$links = $dom-&gt;getElementsByTagName('a'); 

// Loop through the &lt;a&gt; tags
if ($links &amp;&amp; $links-&gt;length &gt; 0) {

    foreach ($links as $link) {

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

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

        if ($entry &amp;&amp; $entry-&gt;elementType == ElementType::Entry) {

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

        }

    }

}
</code></pre>

<p>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).</p>

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

<p>a) Add the classnames when the entry is saved (permanent)<br>
b) Add the classnames when the content is output in a template (not permanent)</p>

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

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

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

<p>For option b), I'd suggest writing a <a href="https://craftcms.com/docs/plugins/variables" rel="noreferrer">Template Variable</a> method or a custom <a href="https://craftcms.stackexchange.com/questions/9211/how-do-i-create-a-custom-twig-extension-which-includes-third-party-code">Twig Filter</a>. 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:</p>

<pre><code>{{ craft.myPlugin.addEntryTypeClasses(entry.richTextField) }}
</code></pre>

<p>A custom Twig filter offers a marginally more elegant template syntax:</p>

<pre><code>{{ entry.richTextField|addEntryTypeClasses }}
</code></pre>

<p>Here, <code>addEntryTypeClasses</code> 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 <code>|raw</code> filter in your template, add the following when you return the <code>$html</code> variable:</p>

<pre><code>public function addEntryTypeClasses($html) {
    ...
    return TemplateHelper::getRaw($html);
}
</code></pre>

<p>In all of the above examples, <code>...</code> is where you'd put the DOMDocument code. <em>Ideally</em>, all of that code would go in a custom <a href="https://craftcms.com/docs/plugins/services" rel="noreferrer">Service</a> 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.</p>

<p>Keep in mind though that with option b), it would be a very good idea to <a href="https://craftcms.com/docs/templating/cache" rel="noreferrer">cache</a> 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).</p>

<p>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: <a href="https://github.com/jpdevries/styles" rel="noreferrer">https://github.com/jpdevries/styles</a></p>

<p>If you're interested in more examples regarding custom Twig filters and DOMDocument specifically, I wrote a plugin called <a href="https://github.com/mmikkel/RetconHTML-Craft" rel="noreferrer">Retcon HTML</a> which streamlines a lot of common HTML rewriting operations. It won't help with your specific issue, but the <code>attr</code> filter comes pretty close :)</p>

CC BY-SA 3.0
2015-12-21T01:06:34.100Z
It's a shame I can only up-vote once.
Brad Bell
CC BY-SA 3.0
2015-12-21T10:21:45.710Z
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
CC BY-SA 3.0
2015-12-21T00:16:46.523Z
2015-12-21T00:36:40.877Z
2017-04-13T12:50:59.930Z
Mats Mikkel Rummelhoff
<p>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 <code>&lt;a&gt;</code> tag depending on the type of the entry.</p>

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

<p>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!</p>

<p>Many thanks.</p>

<plugin-development><redactor><richtext>
Treat links in redactor/rich text field differently depending on entry type
# Treat links in redactor/rich text field differently depending on entry type

(Asked by: username_0 on 2015-12-18)

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  tag depending on the type of the entry.

For example, a link to an "event" entry type would be modified to look like: `Event name`.

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.

**Tags:** plugin-development, redactor, richtext


---

## 1 answers

### Answer by username_1 on 2015-12-21. Score: 12 | Accepted answer

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  tags. Then, you'll need to find a way to match the href attributes for those  tags to entries, and finally, you'll need to set classnames to the  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  tags, I'd recommend using DOMDocument, which is a built-in HTML parser (PHP5+). DOMDocument makes it super easy to find all the  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  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  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  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  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 :)

> **Comments:**
> **username_2**: It's a shame I can only up-vote once.  
> **username_0**: 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...  

---

