DEV Community

James Moberg
James Moberg

Posted on

2 2

Auto-Generating aria-label External Links using ColdFusion + Jsoup

While I don't claim any legal expertise in the ever-evolving WCAG requirements, some of our clients have received a "business alert" from an agency notifying them that "lawsuits are being filed regarding the Americans with Disabilities Act non-compliance of their websites". We've been using WCAG 2.0 level AA, but some of the recent notices are claiming violations of 2.1 guidelines which aren't currently required by US government agencies or Section 508. Apparently WCAG 2.1 may be currently required required by the European Union. The EU's compliance deadline for public sector websites is by Sep 23, 2020 (3 days from now), but my US clients aren't required to follow any EU mandates.

My business partner asked me if we could automatically generate aria-label attributes for <a> tags using the following rules:

Aria-Label Generation Rules

  1. Default = aria-label="Link - opens in new window"
  2. If attr('title') exists, use it.
  3. If text() is a plain text string, use the text.
  4. If tag contains an img tag with an alt attribute, use the image's alt attribute.

We already perform many post-HTML generation optimizations using ColdFusion and Jsoup. Some of the optimizations include:

  • Jsoup auto-corrects invalid HTML. (Valid HTML is critical for passing WCAG.)
  • Add CSP rules to the HTTP header
    • Inject nonce attribute to safe/allowed script hosts
    • Enforce formaction=self rule on dedicated login pages
    • Report violations to internally hosted Taffy REST API
  • Add dns-prefetch HTTP headers for all 3rd-party hosts
  • Remove console.log for public visitors (blog entry)
  • Rewrite shared resources paths to enable/disable CDN usage
  • Auto-relocate inline CSS & JS scripts to head tag
  • Relocate flagged JS scripts to bottom of body (like GoogleAnalytics and FontAwesome)
  • Generate unique alt attributes for missing iframe & img tags (another a11y requirement).

Usage

Pass a string containing a whole or partial HTML fragment. Pass a suffix string (optional):

// addAriaLabeltoHTML(HTML, suffix=" - opens in new window");
UpdatedHTML = addAriaLabeltoHTML(myHTML);
Enter fullscreen mode Exit fullscreen mode

Input/Output Unit Test Results

<!-- <p>This is an <a href="/">HREF (default)</a> test.</p> -->
<p>This is an <a href="/">HREF (default)</a> test.</p> 

<!-- <p><a href="/" target="_self">HREF with '_self' target</a></p> -->
<p><a href="/" target="_self">HREF with '_self' target</a></p> 

<!-- <p><a href="/" target="_blank">anchor text test</a></p> -->
<p><a href="/" target="_blank" aria-label="anchor text test - opens in new window">anchor text test</a></p>

<!-- <p><a href="/" TARGET="_BLANK" title="My website">title test</a></p> -->
<p><a href="/" target="_BLANK" title="My website" aria-label="My website - opens in new window">title test</a></p> 

<!-- <p><a href="/" target="_blank" alt="HTML &amp; Entity">invalid alt + entity test</a></p> -->
<p><a href="/" target="_blank" alt="HTML &amp; Entity" aria-label="invalid alt + entity test - opens in new window">invalid alt + entity test</a></p> 

<!-- <p><a href="/" target="_blank">html <b>test</b></a></p>-->
<p><a href="/" target="_blank" aria-label="html test - opens in new window">html <b>test</b></a></p> 

<!-- <p><a href="/" target="_blank"><img src="test.gif" width="50" alt="Img alt test"></a></p> -->
<p><a href="/" target="_blank" aria-label="Img alt test - opens in new window"><img src="test.gif" width="50" alt="Img alt test"></a></p> 

<!-- <p><a href="/" target="_blank"><img src="test.gif" width="50" alt=""> IMG and text test</a></p> -->
<p><a href="/" target="_blank" aria-label="IMG and text test - opens in new window"><img src="test.gif" width="50" alt=""> IMG and text test</a></p> 

<!-- <p><a href="/" rel="nofollow noopener noreferrer external">rel: anchor text test</a></p> -->
<p><a href="/" rel="nofollow noopener noreferrer external" aria-label="rel: anchor text test - opens in new window">rel: anchor text test</a></p> 

<!-- <p><a href="/" rel="EXTERNAL" title="My website">rel: title test</a></p> -->
<p><a href="/" rel="EXTERNAL" title="My website" aria-label="My website - opens in new window">rel: title test</a></p>

Enter fullscreen mode Exit fullscreen mode

Source Code and CFML Demo

<cfscript>
/**
* addAriaLabeltoHTML (9/16/2020)
* Author: James Moberg / SunStar Media https://www.sunstarmedia.com/
* SOURCE: https://gist.github.com/JamoCA/24d0ad72aa1592060bd1b0c143c78599
* BLOG: https://dev.to/gamesover/auto-generating-aria-label-external-links-using-coldfusion-jsoup-4b71
* DEMO: N/A. Requires JSoup, so unable to be tested using either TryCF or CFFiddle.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* I auto-generate aria-label attributes for external links.
* requires jsoup
* @HTML The HTML string to update
* @suffix The text to append to aria labels
**/
string function addAriaLabeltoHTML(required string HTML="", string suffix=" - opens in new window") output="false" {
var temp = {
isFullHTML = findnocase("<html", arguments.HTML),
updateCount = 0,
entity = chr(31),
hasExternalHREF = findnocase("<a ", arguments.HTML) and (findnocase("target", arguments.HTML) or findnocase("external", arguments.HTML))
};
var A = {};
if (temp.hasExternalHREF) {
try {
temp.jsoup = CreateObject("java", "org.jsoup.Jsoup");
} catch(any e){
return arguments.HTML & "<!-- WARNING: jsoup not loaded; addAriaLabeltoHTML not performed -->";
}
arguments.html = javacast("string",arguments.html).replaceAll("&([^;]+?);", "#temp.entity##temp.entity#$1;");
if (temp.isFullHTML){
temp.htmlDom = temp.jsoup.parse(arguments.HTML);
} else {
temp.htmlDom = temp.jsoup.parseBodyFragment(arguments.HTML);
}
temp.ATags = temp.htmlDom.select('a[target=_blank], a[rel*=external]');
for (temp.ATag in temp.ATags){
A = [
"source" = temp.ATag.toString(),
"title" = temp.ATag.attr('title'),
"anchortext" = temp.ATag.text(),
"ariaLabel" = temp.ATag.attr('aria-label'),
"imgs" = temp.ATag.select('img[alt]')
];
if (not len(trim(A.ariaLabel))){
temp.updateCount = temp.updateCount + 1;
if (len(trim(A.title))){
A.ariaLabel = trim(A.title);
} else if (len(trim(A.anchortext))){
A.ariaLabel = trim(A.anchortext).replaceAll("\s+", " ");
} else if (arrayLen(A.imgs)){
A.ariaLabel = trim(A.imgs[1].attr('alt'));
}
if (not len(trim(A.ariaLabel))){
A.ariaLabel = "Link";
}
temp.ATag.attr('aria-label', A.ariaLabel & arguments.suffix);
}
}
if (temp.updateCount){
if (temp.isFullHTML){
arguments.HTML = temp.htmlDom.toString();
} else {
arguments.HTML = temp.htmlDom.body().html().toString();
}
}
arguments.HTML = arguments.HTML.replaceAll("\#temp.entity#\#temp.entity#([^;]+?);", "&$1;");
}
return arguments.HTML;
}
</cfscript>
<cfsavecontent variable="myHTML">
<p>1. This is an <a href="/">HREF (default)</a> test.</p>
<p>2. <a href="/" target="_self">HREF with '_self' target</a></p>
<p>3. <a href="/" target="_blank">anchor text test</a></p>
<p>4. <a href="/" TARGET="_BLANK" title="My website">title test</a></p>
<p>5. <a href="/" target="_blank" alt="HTML &amp; Entity">invalid alt + entity test</a></p>
<p>6. <a href="/" target="_blank">html <b>test</b></a></p>
<p>7. <a href="/" target="_blank"><img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" width="50" alt="Img alt test"></a></p>
<p>8. <a href="/" target="_blank"><img src="data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7" width="50" alt=""> IMG and text test</a></p>
<p>9. <a href="/" rel="nofollow noopener noreferrer external">rel: anchor text test</a></p>
<p>10. <a href="/" rel="EXTERNAL" title="My website">rel: title test</a></p>
</cfsavecontent>
<cftimer label="addAriaLabeltoHTML" type="outline">
<cfset myHTML_After = addAriaLabeltoHTML(myHTML)>
</cftimer>
<cfoutput>
<h3>HTML Before</h2>
<textarea style="width:90%; height:200px;">#htmlEditFormat(trim(myHTML))#</textarea>
<h3>HTML After</h2>
<textarea style="width:90%; height:200px;">#htmlEditFormat(trim(myHTML_After))#</textarea>
<h3>HTML Rendered</h2>
#trim(myHTML_After)#
</cfoutput>

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Immerse yourself in a wealth of knowledge with this piece, supported by the inclusive DEV Community—every developer, no matter where they are in their journey, is invited to contribute to our collective wisdom.

A simple “thank you” goes a long way—express your gratitude below in the comments!

Gathering insights enriches our journey on DEV and fortifies our community ties. Did you find this article valuable? Taking a moment to thank the author can have a significant impact.

Okay