From f8efc4112b2191e6f21a4681b360ab3f1f4f27e1 Mon Sep 17 00:00:00 2001
From: "Kenneth G. Franqueiro"
Date: Mon, 8 Jul 2024 17:28:38 +0000
Subject: [PATCH 01/25] Implement Eleventy-based build system (#3917)
Re-implements the Ant/XSLT-based build system,
with no changes to source HTML files.
See PR for full list of fixes and a few behavioral changes,
and 11ty/README.md for running instructions.
---
.eleventyignore | 24 +
.gitignore | 7 +
.nvmrc | 1 +
.prettierignore | 3 +
.prettierrc | 4 +
11ty/CustomLiquid.ts | 561 +++
11ty/README.md | 70 +
11ty/cheerio.ts | 64 +
11ty/common.ts | 30 +
11ty/cp-cvs.ts | 50 +
11ty/guidelines.ts | 220 ++
11ty/techniques.ts | 244 ++
11ty/types.ts | 55 +
11ty/understanding.ts | 89 +
_includes/back-to-top.html | 7 +
_includes/head.html | 8 +
_includes/header.html | 46 +
_includes/help-improve.html | 17 +
_includes/sidebar.html | 8 +
_includes/site-footer.html | 29 +
_includes/techniques/about.html | 28 +
.../techniques/applicability-association.html | 15 +
_includes/techniques/applicability.html | 19 +
_includes/techniques/h1.html | 1 +
_includes/techniques/head.html | 1 +
_includes/techniques/intro/resources.html | 1 +
_includes/test-rules.html | 25 +
_includes/toc.html | 38 +
_includes/understanding/about.html | 9 +
_includes/understanding/h1.html | 10 +
_includes/understanding/head.html | 1 +
_includes/understanding/intro/advisory.html | 15 +
_includes/understanding/intro/failure.html | 3 +
_includes/understanding/intro/resources.html | 1 +
.../intro/sufficient-situation.html | 4 +
_includes/understanding/intro/techniques.html | 8 +
_includes/understanding/key-terms.html | 6 +
_includes/understanding/navigation-index.html | 19 +
_includes/understanding/navigation.html | 50 +
_includes/understanding/success-criteria.html | 10 +
_includes/wai-site-footer.html | 17 +
_includes/waiscript.html | 29 +
eleventy.config.ts | 208 ++
package-lock.json | 3045 +++++++++++++++++
package.json | 31 +
techniques/techniques.11tydata.js | 7 +
tsconfig.json | 11 +
understanding/understanding.11tydata.js | 7 +
48 files changed, 5156 insertions(+)
create mode 100644 .eleventyignore
create mode 100644 .nvmrc
create mode 100644 .prettierignore
create mode 100644 .prettierrc
create mode 100644 11ty/CustomLiquid.ts
create mode 100644 11ty/README.md
create mode 100644 11ty/cheerio.ts
create mode 100644 11ty/common.ts
create mode 100644 11ty/cp-cvs.ts
create mode 100644 11ty/guidelines.ts
create mode 100644 11ty/techniques.ts
create mode 100644 11ty/types.ts
create mode 100644 11ty/understanding.ts
create mode 100644 _includes/back-to-top.html
create mode 100644 _includes/head.html
create mode 100644 _includes/header.html
create mode 100644 _includes/help-improve.html
create mode 100644 _includes/sidebar.html
create mode 100644 _includes/site-footer.html
create mode 100644 _includes/techniques/about.html
create mode 100644 _includes/techniques/applicability-association.html
create mode 100644 _includes/techniques/applicability.html
create mode 100644 _includes/techniques/h1.html
create mode 100644 _includes/techniques/head.html
create mode 100644 _includes/techniques/intro/resources.html
create mode 100644 _includes/test-rules.html
create mode 100644 _includes/toc.html
create mode 100644 _includes/understanding/about.html
create mode 100644 _includes/understanding/h1.html
create mode 100644 _includes/understanding/head.html
create mode 100644 _includes/understanding/intro/advisory.html
create mode 100644 _includes/understanding/intro/failure.html
create mode 100644 _includes/understanding/intro/resources.html
create mode 100644 _includes/understanding/intro/sufficient-situation.html
create mode 100644 _includes/understanding/intro/techniques.html
create mode 100644 _includes/understanding/key-terms.html
create mode 100644 _includes/understanding/navigation-index.html
create mode 100644 _includes/understanding/navigation.html
create mode 100644 _includes/understanding/success-criteria.html
create mode 100644 _includes/wai-site-footer.html
create mode 100644 _includes/waiscript.html
create mode 100644 eleventy.config.ts
create mode 100644 package-lock.json
create mode 100644 package.json
create mode 100644 techniques/techniques.11tydata.js
create mode 100644 tsconfig.json
create mode 100644 understanding/understanding.11tydata.js
diff --git a/.eleventyignore b/.eleventyignore
new file mode 100644
index 0000000000..59386af4f7
--- /dev/null
+++ b/.eleventyignore
@@ -0,0 +1,24 @@
+*.*
+11ty/
+acknowledgements/
+conformance-challenges/
+guidelines/
+lib/
+requirements/
+script/
+wcag20/
+# working-examples is directly copied; it should not be processed as templates
+working-examples/
+xslt/
+
+# These files under understanding don't end up in output in the old build
+understanding/*/accessibility-support-documenting.html
+understanding/*/identify-changes.html
+understanding/*/interruptions-minimum.html
+understanding/*/seizures.html
+
+# Ignore templates used for creating new documents
+**/*-template.html
+
+# HTML files under img will be passthrough-copied
+**/img/*
diff --git a/.gitignore b/.gitignore
index 26d41dc54a..598250a1d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -221,6 +221,12 @@ pip-log.txt
build.properties
+#######
+## Node
+#######
+
+node_modules/
+
#############
## Output
#############
@@ -238,3 +244,4 @@ build.properties
/guidelines/wcag.xml
/guidelines/versions.xml
/guidelines/index-flat.html
+/_site/
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000000..209e3ef4b6
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+20
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000000..2c49d868d2
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1,3 @@
+*.*
+!*.ts
+!*.11tydata.js
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000..12c91d0119
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,4 @@
+{
+ "printWidth": 100,
+ "trailingComma": "es5"
+}
diff --git a/11ty/CustomLiquid.ts b/11ty/CustomLiquid.ts
new file mode 100644
index 0000000000..49e071fd7a
--- /dev/null
+++ b/11ty/CustomLiquid.ts
@@ -0,0 +1,561 @@
+import type { Cheerio, Element } from "cheerio";
+import { Liquid, type Template } from "liquidjs";
+import type { RenderOptions } from "liquidjs/dist/liquid-options";
+import compact from "lodash-es/compact";
+import uniq from "lodash-es/uniq";
+
+import { basename } from "path";
+
+import type { GlobalData } from "eleventy.config";
+
+import { flattenDom, load } from "./cheerio";
+import { generateId } from "./common";
+import { getTermsMap } from "./guidelines";
+import { resolveTechniqueIdFromHref, understandingToTechniqueLinkSelector } from "./techniques";
+import { techniqueToUnderstandingLinkSelector } from "./understanding";
+
+const titleSuffix = " | WAI | W3C";
+
+const indexPattern = /(techniques|understanding)\/(index|about)\.html$/;
+const techniquesPattern = /\btechniques\//;
+const understandingPattern = /\bunderstanding\//;
+
+const termsMap = await getTermsMap();
+const termLinkSelector = "a:not([href])";
+
+/** Generates {% include "foo.html" %} directives from 1 or more basenames */
+const generateIncludes = (...basenames: string[]) =>
+ `\n${basenames.map((basename) => `{% include "${basename}.html" %}`).join("\n")}\n`;
+
+/**
+ * Determines whether a given string is actually HTML,
+ * not e.g. a data value Eleventy sent to the templating engine.
+ */
+const isHtmlFileContent = (html: string) => !html.startsWith("(((11ty") && !html.endsWith(".html");
+
+/**
+ * Performs common cleanup of in-page headings, and by extension, table of contents links,
+ * for final output.
+ */
+const normalizeHeading = (label: string) =>
+ label
+ .trim()
+ .replace(/In brief/, "In Brief")
+ .replace(/^(\S+) (of|for) .*$/, "$1")
+ .replace(/^Techniques and Failures .*$/, "Techniques")
+ .replace(/^Specific Benefits .*$/, "Benefits")
+ .replace(/^.* Examples$/, "Examples")
+ .replace(/^(Related )?Resources$/i, "Related Resources");
+
+/**
+ * Performs additional common cleanup for table of contents links for final output.
+ * This is expected to piggyback off of normalizeHeading, which should be called on
+ * headings prior to processing the table of contents from them.
+ */
+const normalizeTocLabel = (label: string) =>
+ label.replace(/ for this Guideline$/, "").replace(/ \(SC\)$/, "");
+
+/**
+ * Replaces a link with a technique URL with a Liquid tag which will
+ * expand to a link with the full technique ID and title.
+ * @param $el a $()-wrapped link element
+ */
+function expandTechniqueLink($el: Cheerio) {
+ const href = $el.attr("href");
+ if (!href) throw new Error("expandTechniqueLink: non-link element encountered");
+ const id = resolveTechniqueIdFromHref(href);
+ // id will be empty string for links to index, which we don't need to modify
+ if (id) $el.replaceWith(`{{ "${id}" | linkTechniques }}`);
+}
+
+const stripHtmlComments = (html: string) => html.replace(//g, "");
+
+// Dev note: Eleventy doesn't expose typings for its template engines for us to neatly extend.
+// Fortunately, it passes both the content string and the file path through to Liquid#parse:
+// https://github.com/11ty/eleventy/blob/9c3a7619/src/Engines/Liquid.js#L253
+
+/**
+ * Liquid class extension that adds support for parts of the existing build process:
+ * - flattening data-include directives prior to parsing Liquid tags
+ * (permitting Liquid even inside data-included files)
+ * - inserting header/footer content within the body of pages
+ * - generating/expanding sections with auto-generated content
+ */
+export class CustomLiquid extends Liquid {
+ public parse(html: string, filepath?: string) {
+ // Filter out Liquid calls for computed data and includes themselves
+ if (filepath && !filepath.includes("_includes/") && isHtmlFileContent(html)) {
+ /** Matches paths that would go through process-index.xslt in previous process */
+ const isIndex = indexPattern.test(filepath);
+ const isTechniques = techniquesPattern.test(filepath);
+ const isUnderstanding = understandingPattern.test(filepath);
+
+ const $ = flattenDom(html, filepath);
+
+ // Clean out elements to be removed
+ // (e.g. editors.css & sources.css, and leftover template paragraphs)
+ // NOTE: some paragraphs with the "instructions" class actually have custom content,
+ // but for now this remains consistent with the XSLT process by stripping all of them.
+ $(".remove, p.instructions, section#meta, section.meta").remove();
+
+ const prependedIncludes = ["header"];
+ const appendedIncludes = ["wai-site-footer", "site-footer"];
+
+ if (isUnderstanding)
+ prependedIncludes.push(
+ isIndex ? "understanding/navigation-index" : "understanding/navigation"
+ );
+
+ if (isIndex) {
+ if (isTechniques) $("section#changelog li a").each((_, el) => expandTechniqueLink($(el)));
+ } else {
+ $("head").append(generateIncludes("head"));
+ appendedIncludes.push("waiscript");
+
+ // Remove resources section if it only has a placeholder item
+ const $resourcesOnlyItem = $("section#resources li:only-child");
+ if (
+ $resourcesOnlyItem.length &&
+ ($resourcesOnlyItem.html() === "Resource" || $resourcesOnlyItem.html() === "Link")
+ )
+ $("section#resources").remove();
+
+ // Fix incorrect level-2 and first-child level-3 headings
+ // (avoid changing h3s that are appropriate but aren't nested within a subsection)
+ $("body > section section h2").each((_, el) => {
+ el.tagName = "h3";
+ });
+ $("body > section > h3:first-child").each((_, el) => {
+ el.tagName = "h2";
+ });
+
+ if (isTechniques) {
+ // Remove any effectively-empty techniques/resources sections (from template)
+ $("section#related:not(:has(a))").remove();
+ $("section#resources:not(:has(a, li))").remove();
+ // Expand related technique links to include full title
+ // (the XSLT process didn't handle this in this particular context)
+ const siblingCode = basename(filepath).replace(/^([A-Z]+).*$/, "$1");
+ $("section#related li")
+ .find(`a[href^='../'], a[href^=${siblingCode}]`)
+ .each((_, el) => expandTechniqueLink($(el)));
+
+ // XSLT orders related and tests last, but they are not last in source files
+ $("body")
+ .append("\n", $(`body > section#related`))
+ .append("\n", $(`body > section#tests`));
+
+ $("h1")
+ .after(generateIncludes("techniques/about"))
+ .replaceWith(generateIncludes("techniques/h1"));
+
+ const sectionCounts: Record = {};
+ let hasDuplicates = false;
+ $("body > section[id]").each((_, el) => {
+ const id = el.attribs.id.toLowerCase();
+ // Fix non-lowercase top-level section IDs (e.g. H99)
+ el.attribs.id = id;
+ // Track duplicate sections, to be processed next
+ if (id in sectionCounts) {
+ hasDuplicates = true;
+ sectionCounts[id]++;
+ } else {
+ sectionCounts[id] = 1;
+ }
+ });
+
+ // Avoid loop altogether in majority of (correct) cases
+ if (hasDuplicates) {
+ for (const [id, count] of Object.entries(sectionCounts)) {
+ if (count === 1) continue;
+ console.warn(
+ `${filepath}: Merging duplicate ${id} sections; please fix this in the source file.`
+ );
+ const $sections = $(`section[id='${id}']`);
+ const $first = $sections.first();
+ $sections.each((i, el) => {
+ if (i === 0) return;
+ const $el = $(el);
+ $el.find("> h2:first-child").remove();
+ $first.append($el.contents());
+ $el.remove();
+ });
+ }
+ }
+
+ $("section#resources h2").after(generateIncludes("techniques/intro/resources"));
+ $("section#examples section.example").each((i, el) => {
+ const $el = $(el);
+ const exampleText = `Example ${i + 1}`;
+ // Check for multiple h3 under one example, which should be h4 (e.g. SCR33)
+ $el.find("h3:not(:only-of-type)").each((_, el) => {
+ el.tagName = "h4";
+ });
+
+ const $h3 = $el.find("h3");
+ if ($h3.length) {
+ const h3Text = $h3.text(); // Used for comparisons below
+ // Some examples really have an empty h3...
+ if (!h3Text) $h3.text(exampleText);
+ // Only prepend "Example N: " if it won't be redundant (e.g. C31, F83)
+ else if (!/example \d+$/i.test(h3Text) && !/^example \d+/i.test(h3Text))
+ $h3.prepend(`${exampleText}: `);
+ } else {
+ $el.prepend(`
${exampleText}
`);
+ }
+ });
+ } else if (isUnderstanding) {
+ // Add numbers to figcaptions
+ $("figcaption").each((i, el) => {
+ const $el = $(el);
+ if (!$el.find("p").length) $el.wrapInner("");
+ $el.prepend(`Figure ${i + 1}`);
+ });
+
+ // Remove spurious copy-pasted content in 2.5.3 that doesn't belong there
+ if ($("section#benefits").length > 1) $("section#benefits").first().remove();
+ // Some pages nest Benefits inside Intent; XSLT always pulls it back out
+ $("section#intent section#benefits")
+ .insertAfter("section#intent")
+ .find("h3:first-child")
+ .each((_, el) => {
+ el.tagName = "h2";
+ });
+
+ // XSLT orders resources then techniques last, opposite of source files
+ $("body")
+ .append("\n", $(`body > section#resources`))
+ .append("\n", $(`body > section#techniques`));
+
+ // Expand top-level heading and add box for guideline/SC pages
+ if ($("section#intent").length) $("h1").replaceWith(generateIncludes("understanding/h1"));
+ $("section#intent").before(generateIncludes("understanding/about"));
+
+ $("section#techniques h2").after(generateIncludes("understanding/intro/techniques"));
+ if ($("section#sufficient .situation").length) {
+ $("section#sufficient h3").after(
+ generateIncludes("understanding/intro/sufficient-situation")
+ );
+ }
+ // success-criteria section should be auto-generated;
+ // remove any handwritten ones (e.g. Input Modalities)
+ const $successCriteria = $("section#success-criteria");
+ if ($successCriteria.length) {
+ console.warn(
+ `${filepath}: success-criteria section will be replaced with ` +
+ "generated version; please remove this from the source file."
+ );
+ $successCriteria.remove();
+ }
+ // success-criteria template only renders content for guideline (not SC) pages
+ $("body").append(generateIncludes("understanding/success-criteria"));
+
+ // Remove unpopulated techniques subsections
+ for (const id of ["sufficient", "advisory", "failure"]) {
+ $(`section#${id}:not(:has(:not(h3)))`).remove();
+ }
+
+ // Normalize subsection names for Guidelines (h2) and/or SC (h3)
+ $("section#sufficient h3").text("Sufficient Techniques");
+ $("section#advisory").find("h2, h3").text("Advisory Techniques");
+ $("section#failure h3").text("Failures");
+
+ // Add intro prose to populated sections
+ $("section#advisory")
+ .find("h2, h3")
+ .after(generateIncludes("understanding/intro/advisory"));
+ $("section#failure h3").after(generateIncludes("understanding/intro/failure"));
+ $("section#resources h2").after(generateIncludes("understanding/intro/resources"));
+
+ // Expand techniques links to always include title
+ $(understandingToTechniqueLinkSelector).each((_, el) => expandTechniqueLink($(el)));
+
+ // Add key terms by default, to be removed in #parse if there are no terms
+ $("body").append(generateIncludes("understanding/key-terms"));
+ }
+
+ // Remove h2-level sections with no content other than heading
+ $("body > section:not(:has(:not(h2)))").remove();
+
+ $("body")
+ .attr("dir", "ltr") // Already included in index/about pages
+ .append(generateIncludes("test-rules", "back-to-top"))
+ .wrapInner(``)
+ .prepend(generateIncludes("sidebar"))
+ .append(generateIncludes("help-improve"))
+ // index/about pages already include this wrapping; others do not, and need it.
+ // This wraps around table of contents & help improve, but not other includes
+ .wrapInner(``);
+ }
+
+ $("body")
+ .prepend(generateIncludes(...prependedIncludes))
+ .append(generateIncludes(...appendedIncludes));
+
+ return super.parse($.html(), filepath);
+ }
+ return super.parse(html);
+ }
+
+ public async render(templates: Template[], scope: GlobalData, options?: RenderOptions) {
+ // html contains markup after Liquid tags/includes have been processed
+ const html = (await super.render(templates, scope, options)).toString();
+ if (!isHtmlFileContent(html) || !scope) return html;
+
+ const $ = load(html);
+
+ if (!indexPattern.test(scope.page.inputPath)) {
+ if (scope.isTechniques) {
+ $("title").text(`${scope.technique.id}: ${scope.technique.title}${titleSuffix}`);
+ const aboutBoxSelector = "section#technique .box-i";
+
+ // Strip applicability paragraphs with metadata IDs (e.g. H99)
+ $("section#applicability").find("p#id, p#technology, p#type").remove();
+ // Check for custom applicability paragraph before removing the section
+ const customApplicability = $("section#applicability p")
+ .html()
+ ?.trim()
+ .replace(/^th(e|is) (technique|failure)s? (is )?/i, "")
+ .replace(/^general( technique|ly applicable)?(\.|$).*$/i, "all technologies")
+ .replace(/^appropriate to use for /i, "")
+ .replace(/^use this technique on /i, "")
+ // Work around redundant sentences (e.g. F105)
+ .replace(/\.\s+This technique relates to Success Criterion [\d\.]+\d[^\.]+\.$/, "");
+ if (customApplicability) {
+ const appliesPattern = /^(?:appli(?:es|cable)|relates) (to|when(?:ever)?)\s*/i;
+ const rephrasedApplicability = customApplicability.replace(appliesPattern, "");
+
+ // Failure pages have no default applicability paragraph, so append one first
+ if (scope.technique.technology === "failures")
+ $("section#technique .box-i").append("");
+
+ const noun = scope.technique.technology === "failures" ? "failure" : "technique";
+ const appliesMatch = appliesPattern.exec(customApplicability);
+ const connector = /^not/.test(customApplicability)
+ ? "is"
+ : `applies ${appliesMatch?.[1] || "to"}`;
+ $("section#technique .box-i p:last-child").html(
+ `This ${noun} ${connector} ` +
+ // Uncapitalize original sentence, except for all-caps abbreviations or titles
+ (/^[A-Z]{2,}/.test(rephrasedApplicability) ||
+ /^([A-Z][a-z]+(\s+|\.?$))+(\/|$)/.test(rephrasedApplicability)
+ ? rephrasedApplicability
+ : rephrasedApplicability[0].toLowerCase() + rephrasedApplicability.slice(1)) +
+ (/(\.|:)$/.test(rephrasedApplicability) ? "" : ".")
+ );
+
+ // Append any relevant subsequent paragraphs or lists from applicability section
+ const $additionalApplicability = $("section#applicability").find(
+ "p:not(:first-of-type), ul, ol"
+ );
+ const additionalApplicabilityText = $additionalApplicability.text();
+ const excludes = [
+ "None listed.", // Template filler
+ "This technique relates to:", // Redundant of auto-generated content
+ ];
+ if (excludes.every((exclude) => !additionalApplicabilityText.includes(exclude)))
+ $additionalApplicability.appendTo(aboutBoxSelector);
+ }
+ $("section#applicability").remove();
+
+ if (scope.technique.technology === "flash") {
+ $(aboutBoxSelector).append(
+ "
Note: Adobe has plans to stop updating and distributing the Flash Player at the end of 2020, " +
+ "and encourages authors interested in creating accessible web content to use HTML.
Note: Microsoft has stopped updating and distributing Silverlight, " +
+ "and authors are encouraged to use HTML for accessible web content.
"
+ );
+ }
+
+ // Update understanding links to always use base URL
+ // (mainly to avoid any case-sensitivity issues)
+ $(techniqueToUnderstandingLinkSelector).each((_, el) => {
+ el.attribs.href = el.attribs.href.replace(/^.*\//, scope.understandingUrl);
+ });
+ } else if (scope.isUnderstanding) {
+ const $title = $("title");
+ if (scope.guideline) {
+ const type = scope.guideline.type === "SC" ? "Success Criterion" : scope.guideline.type;
+ $title.text(
+ `Understanding ${type} ${scope.guideline.num}: ${scope.guideline.name}${titleSuffix}`
+ );
+ } else {
+ $title.text(
+ $title.text().replace(/WCAG 2( |$)/, `WCAG ${scope.versionDecimal}$1`) + titleSuffix
+ );
+ }
+ }
+
+ // Process defined terms within #render,
+ // where we have access to global data and the about box's HTML
+ const $termLinks = $(termLinkSelector);
+ const extractTermName = ($el: Cheerio) => {
+ const name = $el.text().trim().toLowerCase();
+ const term = termsMap[name];
+ if (!term) {
+ console.warn(`${scope.page.inputPath}: Term not found: ${name}`);
+ return;
+ }
+ // Return standardized name for Key Terms definition lists
+ return term.name;
+ };
+
+ if (scope.isTechniques) {
+ $termLinks.each((_, el) => {
+ const $el = $(el);
+ const termName = extractTermName($el);
+ $el
+ .attr("href", `${scope.guidelinesUrl}#${termName ? termsMap[termName].trId : ""}`)
+ .attr("target", "terms");
+ });
+ } else if (scope.isUnderstanding) {
+ const $termsList = $("section#key-terms dl");
+ const extractTermNames = ($links: Cheerio) =>
+ compact(uniq($links.toArray().map((el) => extractTermName($(el)))));
+
+ if ($termLinks.length) {
+ let termNames = extractTermNames($termLinks);
+ // This is one loop but effectively multiple passes,
+ // since terms may reference other terms in their own definitions.
+ // Each iteration may append to termNames.
+ for (let i = 0; i < termNames.length; i++) {
+ const term = termsMap[termNames[i]];
+ if (!term) continue; // This will already warn via extractTermNames
+
+ const $definition = load(term.definition);
+ const $definitionTermLinks = $definition(termLinkSelector);
+ if ($definitionTermLinks.length) {
+ termNames = uniq(termNames.concat(extractTermNames($definitionTermLinks)));
+ }
+ }
+
+ // Iterate over sorted names to populate alphabetized Key Terms definition list
+ termNames.sort();
+ for (const name of termNames) {
+ const term = termsMap[name]; // Already verified existence in the earlier loop
+ $termsList.append(
+ `
${term.name}
` +
+ `
${term.definition}
`
+ );
+ }
+
+ // Iterate over non-href links once more in now-expanded document to add hrefs
+ $(termLinkSelector).each((_, el) => {
+ const name = extractTermName($(el));
+ el.attribs.href = `#${name ? termsMap[name].id : ""}`;
+ });
+ } else {
+ // No terms: remove skeleton that was placed in #parse
+ $("section#key-terms").remove();
+ }
+ }
+
+ // Remove items that end up empty due to invalid technique IDs during #parse
+ // (e.g. removed/deprecated)
+ if (scope.isTechniques) {
+ $("section#related li:empty").remove();
+ } else if (scope.isUnderstanding) {
+ // :empty doesn't work here since there may be whitespace
+ // (can't trim whitespace in the liquid tag since some links have more text after)
+ $(`section#techniques li`)
+ .filter((_, el) => !$(el).text().trim())
+ .remove();
+
+ // Prepend guidelines base URL to non-dfn anchor links in guidelines-derived content
+ // (including both the guideline/SC box at the top and Key Terms at the bottom)
+ $("#guideline, #success-criterion, #key-terms")
+ .find("a[href^='#']:not([href^='#dfn-'])")
+ .each((_, el) => {
+ el.attribs.href = scope.guidelinesUrl + el.attribs.href;
+ });
+ }
+ }
+
+ // Expand note paragraphs after parsing and rendering,
+ // after Guideline/SC content for Understanding pages is rendered.
+ // (This is also needed for techniques/about)
+ $("div.note").each((_, el) => {
+ const $el = $(el);
+ $el.replaceWith(`
+
Note
+
${$el.html()}
+
`);
+ });
+ // Handle p variant after div (the reverse would double-process)
+ $("p.note").each((_, el) => {
+ const $el = $(el);
+ $el.replaceWith(`
+
Note
+
${$el.html()}
+
`);
+ });
+
+ // We don't need to do any more processing for index/about pages other than stripping comments
+ if (indexPattern.test(scope.page.inputPath)) return stripHtmlComments($.html());
+
+ // Handle new-in-version content
+ $("[class^='wcag']").each((_, el) => {
+ // Just like the XSLT process, this naively assumes that version numbers are the same length
+ const classVersion = +el.attribs.class.replace(/^wcag/, "");
+ const buildVersion = +scope.version;
+ if (isNaN(classVersion)) throw new Error(`Invalid wcagXY class found: ${el.attribs.class}`);
+ if (classVersion > buildVersion) {
+ $(el).remove();
+ } else if (classVersion === buildVersion) {
+ $(el).prepend(`New in WCAG ${scope.versionDecimal}: `);
+ }
+ // Output as-is if content pertains to a version older than what's being built
+ });
+
+ if (!scope.isUnderstanding || scope.guideline) {
+ // Fix inconsistent heading labels
+ // (another pass is done on top of this for table of contents links below)
+ $("h2").each((_, el) => {
+ const $el = $(el);
+ $el.text(normalizeHeading($el.text()));
+ });
+ }
+
+ // Allow autogenerating missing top-level section IDs in understanding docs,
+ // but don't pick up incorrectly-nested sections in some techniques pages (e.g. H91)
+ const sectionSelector = scope.isUnderstanding ? "section" : "section[id]";
+ const sectionH2Selector = "h2:first-child";
+ const $h2Sections = $(`${sectionSelector}:has(${sectionH2Selector})`);
+ if ($h2Sections.length) {
+ // Generate table of contents after parsing and rendering,
+ // when we have sections and sidebar skeleton already reordered
+ const $tocList = $(".sidebar nav ul");
+ $h2Sections.each((_, el) => {
+ if (!el.attribs.id) el.attribs.id = generateId($(el).find(sectionH2Selector).text());
+ $("")
+ .attr("href", `#${el.attribs.id}`)
+ .text(normalizeTocLabel($(el).find(sectionH2Selector).text()))
+ .appendTo($tocList)
+ .wrap("");
+ $tocList.append("\n");
+ });
+ } else {
+ // Remove ToC sidebar that was added in #parse if there's nothing to list in it
+ $(".sidebar").remove();
+ }
+
+ // Autogenerate remaining IDs after constructing table of contents.
+ // NOTE: This may overwrite some IDs set in HTML (for techniques examples),
+ // and may result in duplicates; this is consistent with the XSLT process.
+ const sectionHeadingSelector = ["h3", "h4", "h5"]
+ .map((tag) => `> ${tag}:first-child`)
+ .join(", ");
+ const autoIdSectionSelectors = ["section:not([id])"];
+ if (scope.isTechniques) autoIdSectionSelectors.push("section.example");
+ $(autoIdSectionSelectors.join(", "))
+ .filter(`:has(${sectionHeadingSelector})`)
+ .each((_, el) => {
+ el.attribs.id = generateId($(el).find(sectionHeadingSelector).text());
+ });
+
+ return stripHtmlComments($.html());
+ }
+}
diff --git a/11ty/README.md b/11ty/README.md
new file mode 100644
index 0000000000..35b9ca85d8
--- /dev/null
+++ b/11ty/README.md
@@ -0,0 +1,70 @@
+# Eleventy Infrastructure for WCAG Techniques and Understanding
+
+This subdirectory contains ES Modules re-implementing pieces of the
+XSLT-based build process using Eleventy.
+
+## Usage
+
+Make sure you have Node.js installed. This has primarily been tested with v20,
+the current LTS at time of writing.
+
+If you use [fnm](https://github.com/Schniz/fnm) or [nvm](https://github.com/nvm-sh/nvm) to manage multiple Node.js versions,
+you can switch to the recommended version by typing `fnm use` or `nvm use`
+(with no additional arguments) while in the repository directory.
+
+Otherwise, you can download an installer from [nodejs.org](https://nodejs.org/).
+
+First, run `npm i` in the root directory of the repository to install dependencies.
+
+Common tasks:
+
+- `npm run build` runs a one-time build
+- `npm start` runs a local server with hot-reloading to preview changes as you make them:
+ - http://localhost:8080/techniques
+ - http://localhost:8080/understanding
+
+Maintenance tasks (for working with Eleventy config and supporting files under this subdirectory):
+
+- `npm run check` checks for TypeScript errors
+- `npm run fmt` formats all TypeScript files
+
+## Environment Variables
+
+### `WCAG_CVSDIR`
+
+**Usage context:** `publish-w3c` script only
+
+Indicates top-level path of W3C CVS checkout, for WAI site updates (via `publish-w3c` script).
+
+**Default:** `../../../w3ccvs` (same as in Ant/XSLT build process)
+
+### `WCAG_VERSION`
+
+**Usage context:** `publish-w3c` script only;
+this should currently not be changed, pending future improvements to `21` support.
+
+Indicates WCAG version being built, in `XY` format (i.e. no `.`)
+
+**Default:** `22`
+
+### `WCAG_MODE`
+
+**Usage context:** should not need to be used manually except in specific testing scenarios
+
+Influences base URLs for links to guidelines, techniques, and understanding pages.
+Typically set by specific npm scripts or CI processes.
+
+Possible values:
+
+- Unset **(default)** - Sets base URLs appropriate for local testing
+- `editors` - Sets base URLs appropriate for `gh-pages` publishing; used by deploy action
+- `publication` - Sets base URLs appropriate for WAI site publishing; used by `publish-w3c` script
+
+## Other points of interest
+
+- The main configuration can be found in top-level `eleventy.config.ts`
+- Build commands are defined in top-level `package.json` under `scripts`,
+ and can be run via `npm run `
+- If you see files named `*.11tydata.js`, these contribute data to the Eleventy build
+ (see Template and Directory Data files under
+ [Sources of Data](https://www.11ty.dev/docs/data/#sources-of-data))
diff --git a/11ty/cheerio.ts b/11ty/cheerio.ts
new file mode 100644
index 0000000000..f1292be14b
--- /dev/null
+++ b/11ty/cheerio.ts
@@ -0,0 +1,64 @@
+import { load, type CheerioOptions } from "cheerio";
+import { readFileSync } from "fs";
+import { readFile } from "fs/promises";
+import { dirname, resolve } from "path";
+
+export { load } from "cheerio";
+
+/** Convenience function that combines readFile and load. */
+export const loadFromFile = async (
+ inputPath: string,
+ options?: CheerioOptions | null,
+ isDocument?: boolean
+) => load(await readFile(inputPath, "utf8"), options, isDocument);
+
+/**
+ * Retrieves content for a data-include, either from _includes,
+ * or relative to the input file.
+ * Operates synchronously for simplicity of use within Cheerio callbacks.
+ *
+ * @param includePath A data-include attribute value
+ * @param inputPath Path (relative to repo root) to file containing the directive
+ * @returns
+ */
+function readInclude(includePath: string, inputPath: string) {
+ const relativePath = resolve(dirname(inputPath), includePath);
+ if (includePath.startsWith("..")) return readFileSync(relativePath, "utf8");
+
+ try {
+ // Prioritize any match under _includes (e.g. over local toc.html built via XSLT)
+ return readFileSync(resolve("_includes", includePath), "utf8");
+ } catch (error) {
+ return readFileSync(relativePath, "utf8");
+ }
+}
+
+/**
+ * Resolves data-include directives in the given file, a la flatten-document.xslt.
+ * This is a lower-level version for use in Eleventy configuration;
+ * you'd probably rather use flattenDomFromFile in other cases.
+ *
+ * @param content String containing HTML to process
+ * @param inputPath Path (relative to repo root) to file containing the HTML
+ * (needed for data-include resolution)
+ * @returns Cheerio instance containing "flattened" DOM
+ */
+export function flattenDom(content: string, inputPath: string) {
+ const $ = load(content);
+
+ $("body [data-include]").each((_, el) => {
+ const replacement = readInclude(el.attribs["data-include"], inputPath);
+ // Replace entire element or children, depending on data-include-replace
+ if (el.attribs["data-include-replace"]) $(el).replaceWith(replacement);
+ else $(el).removeAttr("data-include").html(replacement);
+ });
+
+ return $;
+}
+
+/**
+ * Convenience version of flattenDom that requires only inputPath to be passed.
+ * @see flattenDom
+ */
+export const flattenDomFromFile = async (inputPath: string) =>
+ flattenDom(await readFile(inputPath, "utf8"), inputPath);
diff --git a/11ty/common.ts b/11ty/common.ts
new file mode 100644
index 0000000000..ade5d87f14
--- /dev/null
+++ b/11ty/common.ts
@@ -0,0 +1,30 @@
+/** @fileoverview Common functions used by multiple parts of the build process */
+
+import type { Guideline, Principle, SuccessCriterion } from "./guidelines";
+
+/** Generates an ID for heading permalinks. Equivalent to wcag:generate-id in base.xslt. */
+export function generateId(title: string) {
+ if (title === "Parsing (Obsolete and removed)") return "parsing";
+ return title
+ .replace(/\s+/g, "-")
+ .replace(/[,\():]+/g, "")
+ .toLowerCase();
+}
+
+/** Given a string "xy", returns "x.y" */
+export const resolveDecimalVersion = (version: `${number}`) => version.split("").join(".");
+
+/** Sort function for ordering WCAG principle/guideline/SC numbers ascending */
+export function wcagSort(
+ a: Principle | Guideline | SuccessCriterion,
+ b: Principle | Guideline | SuccessCriterion
+) {
+ const aParts = a.num.split(".").map((n) => +n);
+ const bParts = b.num.split(".").map((n) => +n);
+
+ for (let i = 0; i < 3; i++) {
+ if (aParts[i] > bParts[i] || (aParts[i] && !bParts[i])) return 1;
+ if (aParts[i] < bParts[i] || (bParts[i] && !aParts[i])) return -1;
+ }
+ return 0;
+}
diff --git a/11ty/cp-cvs.ts b/11ty/cp-cvs.ts
new file mode 100644
index 0000000000..2bae08c4e7
--- /dev/null
+++ b/11ty/cp-cvs.ts
@@ -0,0 +1,50 @@
+/** @fileoverview script to copy already-built output to CVS subfolders */
+
+import { copyFile, unlink } from "fs/promises";
+import { glob } from "glob";
+import { mkdirp } from "mkdirp";
+
+import { dirname, join } from "path";
+
+const outputBase = "_site";
+const cvsBase = process.env.WCAG_CVSDIR || "../../../w3ccvs";
+const wcagVersion = process.env.WCAG_VERSION || "22";
+const wcagBase = `${cvsBase}/WWW/WAI/WCAG${wcagVersion}`;
+
+// Map (git) sources to (CVS) destinations, since some don't match case-sensitively
+const dirs = {
+ techniques: "Techniques",
+ understanding: "Understanding",
+ "working-examples": "working-examples",
+};
+
+for (const [srcDir, destDir] of Object.entries(dirs)) {
+ const cleanPaths = await glob(`**`, {
+ cwd: join(wcagBase, destDir),
+ ignore: ["**/CVS/**"],
+ nodir: true,
+ });
+
+ for (const path of cleanPaths) await unlink(join(wcagBase, destDir, path));
+
+ const indexPaths = await glob(`**/index.html`, { cwd: join(outputBase, srcDir) });
+ const nonIndexPaths = await glob(`**`, {
+ cwd: join(outputBase, srcDir),
+ ignore: ["**/index.html"],
+ nodir: true,
+ });
+
+ for (const path of indexPaths) {
+ const srcPath = join(outputBase, srcDir, path);
+ const destPath = join(wcagBase, destDir, path.replace(/index\.html$/, "Overview.html"));
+ await mkdirp(dirname(destPath));
+ await copyFile(srcPath, destPath);
+ }
+
+ for (const path of nonIndexPaths) {
+ const srcPath = join(outputBase, srcDir, path);
+ const destPath = join(wcagBase, destDir, path);
+ await mkdirp(dirname(destPath));
+ await copyFile(srcPath, destPath);
+ }
+}
diff --git a/11ty/guidelines.ts b/11ty/guidelines.ts
new file mode 100644
index 0000000000..5b081fc6bb
--- /dev/null
+++ b/11ty/guidelines.ts
@@ -0,0 +1,220 @@
+import type { Cheerio, Element } from "cheerio";
+import { glob } from "glob";
+
+import { readFile } from "fs/promises";
+import { basename } from "path";
+
+import { flattenDomFromFile, load } from "./cheerio";
+import { generateId } from "./common";
+
+export type WcagVersion = "20" | "21" | "22";
+function assertIsWcagVersion(v: string): asserts v is WcagVersion {
+ if (!/^2[012]$/.test(v)) throw new Error(`Unexpected version found: ${v}`);
+}
+
+/**
+ * Interface describing format of entries in guidelines/act-mapping.json
+ */
+interface ActRule {
+ deprecated: boolean;
+ permalink: string;
+ proposed: boolean;
+ successCriteria: string[];
+ title: string;
+ wcagTechniques: string[];
+}
+
+type ActMapping = {
+ "act-rules": ActRule[];
+};
+
+/** Data used for test-rules sections, from act-mapping.json */
+export const actRules = (
+ JSON.parse(await readFile("guidelines/act-mapping.json", "utf8")) as ActMapping
+)["act-rules"];
+
+/**
+ * Returns an object with keys for each existing WCAG 2 version,
+ * each mapping to an array of basenames of HTML files under understanding/
+ * (Functionally equivalent to "guidelines-versions" target in build.xml)
+ */
+export async function getGuidelinesVersions() {
+ const paths = await glob("*/*.html", { cwd: "understanding" });
+ const versions: Record = { "20": [], "21": [], "22": [] };
+
+ for (const path of paths) {
+ const [version, filename] = path.split("/");
+ assertIsWcagVersion(version);
+ versions[version].push(basename(filename, ".html"));
+ }
+
+ for (const version of Object.keys(versions)) {
+ assertIsWcagVersion(version);
+ versions[version].sort();
+ }
+ return versions;
+}
+
+/**
+ * Like getGuidelinesVersions, but mapping each basename to the version it appears in
+ */
+export async function getInvertedGuidelinesVersions() {
+ const versions = await getGuidelinesVersions();
+ const invertedVersions: Record = {};
+ for (const [version, basenames] of Object.entries(versions)) {
+ for (const basename of basenames) {
+ invertedVersions[basename] = version;
+ }
+ }
+ return invertedVersions;
+}
+
+export interface DocNode {
+ id: string;
+ name: string;
+ /** Helps distinguish entity type when passed out-of-context; used for navigation */
+ type?: "Principle" | "Guideline" | "SC";
+}
+
+export interface Principle extends DocNode {
+ content: string;
+ num: `${number}`; // typed as string for consistency with guidelines/SC
+ version: "WCAG20";
+ guidelines: Guideline[];
+}
+
+export interface Guideline extends DocNode {
+ content: string;
+ num: `${Principle["num"]}.${number}`;
+ version: `WCAG${"20" | "21"}`;
+ successCriteria: SuccessCriterion[];
+}
+
+export interface SuccessCriterion extends DocNode {
+ content: string;
+ num: `${Guideline["num"]}.${number}`;
+ /** Level may be empty for obsolete criteria */
+ level: "A" | "AA" | "AAA" | "";
+ version: `WCAG${WcagVersion}`;
+}
+
+export function isSuccessCriterion(criterion: any): criterion is SuccessCriterion {
+ return !!(criterion?.type === "SC" && "level" in criterion);
+}
+
+/**
+ * Returns HTML content used for Understanding guideline/SC boxes.
+ * @param $el Cheerio element of the full section from flattened guidelines/index.html
+ */
+const getContentHtml = ($el: Cheerio) => {
+ // Load HTML into a new instance, remove elements we don't want, then return the remainder
+ const $ = load($el.html()!, null, false);
+ $("h1, h2, h3, h4, h5, h6, section, .change, .conformance-level").remove();
+ return $.html();
+};
+
+/**
+ * Resolves information from guidelines/index.html;
+ * comparable to the principles section of wcag.xml from the guidelines-xml Ant task.
+ */
+export async function getPrinciples() {
+ const versions = await getInvertedGuidelinesVersions();
+ const $ = await flattenDomFromFile("guidelines/index.html");
+
+ const principles: Principle[] = [];
+ $(".principle").each((i, el) => {
+ const guidelines: Guideline[] = [];
+ $(".guideline", el).each((j, guidelineEl) => {
+ const successCriteria: SuccessCriterion[] = [];
+ $(".sc", guidelineEl).each((k, scEl) => {
+ const resolvedVersion = versions[scEl.attribs.id];
+ assertIsWcagVersion(resolvedVersion);
+
+ successCriteria.push({
+ content: getContentHtml($(scEl)),
+ id: scEl.attribs.id,
+ name: $("h4", scEl).text().trim(),
+ num: `${i + 1}.${j + 1}.${k + 1}`,
+ level: $("p.conformance-level", scEl).text().trim() as SuccessCriterion["level"],
+ type: "SC",
+ version: `WCAG${resolvedVersion}`,
+ });
+ });
+
+ guidelines.push({
+ content: getContentHtml($(guidelineEl)),
+ id: guidelineEl.attribs.id,
+ name: $("h3", guidelineEl).text().trim(),
+ num: `${i + 1}.${j + 1}`,
+ type: "Guideline",
+ version: guidelineEl.attribs.id === "input-modalities" ? "WCAG21" : "WCAG20",
+ successCriteria,
+ });
+ });
+
+ principles.push({
+ content: getContentHtml($(el)),
+ id: el.attribs.id,
+ name: $("h2", el).text().trim(),
+ num: `${i + 1}`,
+ type: "Principle",
+ version: "WCAG20",
+ guidelines,
+ });
+ });
+
+ return principles;
+}
+
+/**
+ * Returns a flattened object hash, mapping shortcodes to each principle/guideline/SC.
+ */
+export function getFlatGuidelines(principles: Principle[]) {
+ const map: Record = {};
+ for (const principle of principles) {
+ map[principle.id] = principle;
+ for (const guideline of principle.guidelines) {
+ map[guideline.id] = guideline;
+ for (const criterion of guideline.successCriteria) {
+ map[criterion.id] = criterion;
+ }
+ }
+ }
+ return map;
+}
+export type FlatGuidelinesMap = ReturnType;
+
+interface Term {
+ definition: string;
+ /** generated id for use in Understanding pages */
+ id: string;
+ name: string;
+ /** id of dfn in TR, which matches original id in terms file */
+ trId: string;
+}
+
+/**
+ * Resolves term definitions from guidelines/index.html organized for lookup by name;
+ * comparable to the term elements in wcag.xml from the guidelines-xml Ant task.
+ */
+export async function getTermsMap() {
+ const $ = await flattenDomFromFile("guidelines/index.html");
+ const terms: Record = {};
+
+ $("dfn").each((_, el) => {
+ const $el = $(el);
+ const term: Term = {
+ // Note: All applicable s have explicit id attributes for TR,
+ // but the XSLT process generates id from the element's text which is not always the same
+ id: `dfn-${generateId($el.text())}`,
+ definition: getContentHtml($el.parent().next()),
+ name: $el.text().toLowerCase(),
+ trId: el.attribs.id,
+ };
+
+ const names = [term.name].concat((el.attribs["data-lt"] || "").toLowerCase().split("|"));
+ for (const name of names) terms[name] = term;
+ });
+
+ return terms;
+}
diff --git a/11ty/techniques.ts b/11ty/techniques.ts
new file mode 100644
index 0000000000..afa87ec3d7
--- /dev/null
+++ b/11ty/techniques.ts
@@ -0,0 +1,244 @@
+import type { Cheerio } from "cheerio";
+import { glob } from "glob";
+import capitalize from "lodash-es/capitalize";
+import uniqBy from "lodash-es/uniqBy";
+
+import { readFile } from "fs/promises";
+import { basename } from "path";
+
+import { load, loadFromFile } from "./cheerio";
+import { isSuccessCriterion, type FlatGuidelinesMap, type SuccessCriterion } from "./guidelines";
+import { wcagSort } from "./common";
+
+/** Maps each technology to its title for index.html */
+export const technologyTitles = {
+ aria: "ARIA Techniques",
+ "client-side-script": "Client-Side Script Techniques",
+ css: "CSS Techniques",
+ failures: "Common Failures",
+ flash: "Flash Techniques", // Deprecated in 2020
+ general: "General Techniques",
+ html: "HTML Techniques",
+ pdf: "PDF Techniques",
+ "server-side-script": "Server-Side Script Techniques",
+ smil: "SMIL Techniques",
+ silverlight: "Silverlight Techniques", // Deprecated in 2020
+ text: "Plain-Text Techniques",
+};
+type Technology = keyof typeof technologyTitles;
+export const technologies = Object.keys(technologyTitles) as Technology[];
+
+function assertIsTechnology(
+ technology: string
+): asserts technology is keyof typeof technologyTitles {
+ if (!(technology in technologyTitles)) throw new Error(`Invalid technology name: ${technology}`);
+}
+
+const associationTypes = ["sufficient", "advisory", "failure"] as const;
+type AssociationType = (typeof associationTypes)[number];
+
+interface TechniqueAssociation {
+ criterion: SuccessCriterion;
+ type: Capitalize;
+ /** Indicates this technique must be paired with specific "child" techniques to fulfill SC */
+ hasUsageChildren: boolean;
+ /**
+ * Technique ID of "parent" technique(s) this is paired with to fulfill SC.
+ * This is typically 0 or 1 technique, but may be multiple in rare cases.
+ */
+ usageParentIds: string[];
+ /**
+ * Text description of "parent" association, if it does not reference a specific technique;
+ * only populated if usageParentIds is empty.
+ */
+ usageParentDescription: string;
+ /** Technique IDs this technique must be implemented with to fulfill SC, if any */
+ with: string[];
+}
+
+function assertIsAssociationType(type?: string): asserts type is AssociationType {
+ if (!associationTypes.includes(type as AssociationType))
+ throw new Error(`Association processed for unexpected section ${type}`);
+}
+
+/**
+ * Pulls the basename out of a technique link href.
+ * This intentionally returns empty string (falsy) if a directory link happens to be passed.
+ */
+export const resolveTechniqueIdFromHref = (href: string) =>
+ href.replace(/^.*\//, "").replace(/\.html$/, "");
+
+/**
+ * Selector that can detect relative and absolute technique links from understanding docs
+ */
+export const understandingToTechniqueLinkSelector = [
+ "[href^='../Techniques/' i]",
+ "[href^='../../techniques/' i]",
+ "[href^='https://www.w3.org/WAI/WCAG' i][href*='/Techniques/' i]",
+]
+ .map((value) => `a${value}`)
+ .join(", ") as "a";
+
+/**
+ * Returns object mapping technique IDs to SCs that reference it;
+ * comparable to technique-associations.xml but in a more ergonomic format.
+ */
+export async function getTechniqueAssociations(guidelines: FlatGuidelinesMap) {
+ const associations: Record = {};
+ const itemSelector = associationTypes.map((type) => `section#${type} li`).join(", ");
+
+ const paths = await glob("understanding/*/*.html");
+ for (const path of paths) {
+ const criterion = guidelines[basename(path, ".html")];
+ if (!isSuccessCriterion(criterion)) continue;
+
+ const $ = await loadFromFile(path);
+ $(itemSelector).each((_, liEl) => {
+ const $liEl = $(liEl);
+ const $parentListItem = $liEl.closest("ul, ol").closest("li");
+ // Identify which expected section the list was found under
+ const associationType = $liEl
+ .closest(associationTypes.map((type) => `section#${type}`).join(", "))
+ .attr("id");
+ assertIsAssociationType(associationType);
+
+ /** Finds matches only within the given list item (not under child lists) */
+ const queryNonNestedChildren = ($el: Cheerio, selector: string) =>
+ $el.find(selector).filter((_, aEl) => $(aEl).closest("li")[0] === $el[0]);
+
+ const $techniqueLinks = queryNonNestedChildren($liEl, understandingToTechniqueLinkSelector);
+ $techniqueLinks.each((_, aEl) => {
+ const usageParentIds = queryNonNestedChildren(
+ $parentListItem,
+ understandingToTechniqueLinkSelector
+ )
+ .toArray()
+ .map((el) => resolveTechniqueIdFromHref(el.attribs.href));
+
+ // Capture the "X" in "X or more" phrasing, to include a phrase about
+ // combining with other techniques if more than one is required.
+ const descriptionDependencyPattern =
+ /(?:^|,?\s+)(?:by )?using\s+(?:(\w+) (?:or more )?of )?the\s+(?:following )?techniques(?: below)?(?::|\.)?\s*$/i;
+ const parentHtml = usageParentIds.length
+ ? null
+ : queryNonNestedChildren($parentListItem, "p").html();
+ const match = parentHtml && descriptionDependencyPattern.exec(parentHtml);
+ const parentDescription = parentHtml
+ ? parentHtml.replace(
+ descriptionDependencyPattern,
+ !match?.[1] || match?.[1] === "one" ? "" : "when combined with other techniques"
+ )
+ : "";
+ const usageParentDescription =
+ parentDescription &&
+ (parentDescription.startsWith("when")
+ ? parentDescription
+ : `when used for ${parentDescription[0].toLowerCase()}${parentDescription.slice(1)}`);
+
+ const association: TechniqueAssociation = {
+ criterion,
+ type: capitalize(associationType) as Capitalize,
+ hasUsageChildren: !!$liEl.find("ul, ol").length,
+ usageParentIds,
+ usageParentDescription,
+ with: $techniqueLinks
+ .toArray()
+ .filter((el) => el !== aEl)
+ .map((el) => resolveTechniqueIdFromHref(el.attribs.href)),
+ };
+
+ const id = resolveTechniqueIdFromHref(aEl.attribs.href);
+ if (!(id in associations)) associations[id] = [association];
+ else associations[id].push(association);
+ });
+ });
+ }
+
+ // Remove duplicates (due to similar shape across understanding docs) and sort by SC number
+ for (const [key, list] of Object.entries(associations))
+ associations[key] = uniqBy(list, (v) => JSON.stringify(v)).sort((a, b) =>
+ wcagSort(a.criterion, b.criterion)
+ );
+
+ return associations;
+}
+
+interface Technique {
+ /** Letter(s)-then-number technique code; corresponds to source HTML filename */
+ id: string;
+ /** Technology this technique is filed under */
+ technology: Technology;
+ /** Title derived from each technique page's h1 */
+ title: string;
+ /** Title derived from each technique page's h1, with HTML preserved */
+ titleHtml: string;
+ /**
+ * Like title, but preserving the XSLT process behavior of truncating
+ * text on intermediate lines between the first and last for long headings.
+ * (This was probably accidental, but helps avoid long link text.)
+ */
+ truncatedTitle: string;
+}
+
+/**
+ * Returns an object mapping each technology category to an array of Techniques.
+ * Used to generate index table of contents.
+ * (Functionally equivalent to "techniques-list" target in build.xml)
+ */
+export async function getTechniquesByTechnology() {
+ const paths = await glob("*/*.html", { cwd: "techniques" });
+ const techniques = technologies.reduce(
+ (map, technology) => ({
+ ...map,
+ [technology]: [] as string[],
+ }),
+ {} as Record
+ );
+
+ for (const path of paths) {
+ const [technology, filename] = path.split("/");
+ assertIsTechnology(technology);
+
+ // Isolate h1 from each file before feeding into Cheerio to save ~300ms total
+ const match = (await readFile(`techniques/${path}`, "utf8")).match(/
]*>([\s\S]+?)<\/h1>/);
+ if (!match || !match[1]) throw new Error(`No h1 found in techniques/${path}`);
+ const $h1 = load(match[1], null, false);
+
+ const title = $h1.text();
+ techniques[technology].push({
+ id: basename(filename, ".html"),
+ technology,
+ title,
+ titleHtml: $h1.html(),
+ truncatedTitle: title.replace(/\s*\n[\s\S]*\n\s*/, " … "),
+ });
+ }
+
+ for (const technology of technologies) {
+ techniques[technology].sort((a, b) => {
+ const aId = +a.id.replace(/\D/g, "");
+ const bId = +b.id.replace(/\D/g, "");
+ if (aId < bId) return -1;
+ if (aId > bId) return 1;
+ return 0;
+ });
+ }
+
+ return techniques;
+}
+
+/**
+ * Returns a flattened object hash, mapping each technique ID directly to its data.
+ */
+export const getFlatTechniques = (
+ techniques: Awaited>
+) =>
+ Object.values(techniques)
+ .flat()
+ .reduce(
+ (map, technique) => {
+ map[technique.id] = technique;
+ return map;
+ },
+ {} as Record
+ );
diff --git a/11ty/types.ts b/11ty/types.ts
new file mode 100644
index 0000000000..9bdd86c9f1
--- /dev/null
+++ b/11ty/types.ts
@@ -0,0 +1,55 @@
+/** @fileoverview Typings for common Eleventy entities */
+
+interface EleventyPage {
+ date: Date;
+ filePathStem: string;
+ fileSlug: string;
+ inputPath: string;
+ outputFileExtension: string;
+ outputPath: string;
+ rawInput: string;
+ templateSyntax: string;
+ url: string;
+}
+
+interface EleventyDirectories {
+ data: string;
+ includes: string;
+ input: string;
+ layouts?: string;
+ output: string;
+}
+
+type EleventyRunMode = "build" | "serve" | "watch";
+
+interface EleventyMeta {
+ directories: EleventyDirectories;
+ env: {
+ config: string;
+ root: string;
+ runMode: EleventyRunMode;
+ source: "cli" | "script";
+ };
+ generator: string;
+ version: string;
+}
+
+/** Limited 11ty data available when defining filters and shortcodes. */
+export interface EleventyContext {
+ eleventy: EleventyMeta;
+ page: EleventyPage;
+}
+
+/** Eleventy-supplied data available to templates. */
+export interface EleventyData extends EleventyContext {
+ content: string;
+ // Allow access to anything else in data cascade
+ [index: string]: any;
+}
+
+/** Properties available in Eleventy event callbacks (eleventyConfig.on(...)) */
+export interface EleventyEvent {
+ dir: EleventyDirectories;
+ outputMode: "fs" | "json" | "ndjson";
+ runMode: EleventyRunMode;
+}
diff --git a/11ty/understanding.ts b/11ty/understanding.ts
new file mode 100644
index 0000000000..c9b414b63f
--- /dev/null
+++ b/11ty/understanding.ts
@@ -0,0 +1,89 @@
+import { resolveDecimalVersion } from "./common";
+import type { DocNode, Principle, WcagVersion } from "./guidelines";
+
+/**
+ * Selector that can detect relative and absolute understanding links from techniques docs
+ */
+export const techniqueToUnderstandingLinkSelector = [
+ "[href^='../../Understanding/' i]",
+ "[href^='https://www.w3.org/WAI/WCAG' i][href*='/Understanding/' i]",
+]
+ .map((value) => `a${value}`)
+ .join(", ") as "a";
+
+/**
+ * Resolves information for top-level understanding pages;
+ * ported from generate-structure-xml.xslt
+ */
+export async function getUnderstandingDocs(version: WcagVersion): Promise {
+ const decimalVersion = resolveDecimalVersion(version);
+ return [
+ {
+ id: "intro",
+ name: `Introduction to Understanding WCAG ${decimalVersion}`,
+ },
+ {
+ id: "understanding-techniques",
+ name: "Understanding Techniques for WCAG Success Criteria",
+ },
+ {
+ id: "understanding-act-rules",
+ name: "Understanding Test Rules for WCAG Success Criteria",
+ },
+ {
+ id: "conformance",
+ name: "Understanding Conformance",
+ },
+ {
+ id: "refer-to-wcag",
+ name: `How to Refer to WCAG ${decimalVersion} from Other Documents`,
+ },
+ {
+ id: "documenting-accessibility-support",
+ name: "Documenting Accessibility Support for Uses of a Web Technology",
+ },
+ {
+ id: "understanding-metadata",
+ name: "Understanding Metadata",
+ },
+ ];
+}
+
+interface NavData {
+ parent?: DocNode;
+ previous?: DocNode;
+ next?: DocNode;
+}
+
+/**
+ * Generates mappings from guideline/SC/understanding doc IDs to next/previous/parent information,
+ * for efficient lookup when rendering navigation banner
+ */
+export function generateUnderstandingNavMap(principles: Principle[], understandingDocs: DocNode[]) {
+ const allGuidelines = Object.values(principles).flatMap(({ guidelines }) => guidelines);
+ const map: Record = {};
+
+ // Guideline navigation wraps across principles, so iterate over flattened list
+ allGuidelines.forEach((guideline, i) => {
+ map[guideline.id] = {
+ ...(i > 0 && { previous: allGuidelines[i - 1] }),
+ ...(i < allGuidelines.length - 1 && { next: allGuidelines[i + 1] }),
+ };
+ guideline.successCriteria.forEach((criterion, j) => {
+ map[criterion.id] = {
+ parent: guideline,
+ ...(j > 0 && { previous: guideline.successCriteria[j - 1] }),
+ ...(j < guideline.successCriteria.length - 1 && { next: guideline.successCriteria[j + 1] }),
+ };
+ });
+ });
+
+ understandingDocs.forEach((doc, i) => {
+ map[doc.id] = {
+ ...(i > 0 && { previous: understandingDocs[i - 1] }),
+ ...(i < understandingDocs.length - 1 && { next: understandingDocs[i + 1] }),
+ };
+ });
+
+ return map;
+}
diff --git a/_includes/back-to-top.html b/_includes/back-to-top.html
new file mode 100644
index 0000000000..da38ab5fbc
--- /dev/null
+++ b/_includes/back-to-top.html
@@ -0,0 +1,7 @@
+
+
+ Back to Top
+
+
diff --git a/_includes/head.html b/_includes/head.html
new file mode 100644
index 0000000000..55e7145ca2
--- /dev/null
+++ b/_includes/head.html
@@ -0,0 +1,8 @@
+{% # common tags inserted into all non-index/about pages %}
+
+
+{% if isTechniques %}
+ {% include "techniques/head.html" %}
+{% elsif isUnderstanding %}
+ {% include "understanding/head.html" %}
+{% endif %}
diff --git a/_includes/header.html b/_includes/header.html
new file mode 100644
index 0000000000..7d687e9019
--- /dev/null
+++ b/_includes/header.html
@@ -0,0 +1,46 @@
+{% comment %}
+Expected inputs from directory data for techniques and understanding:
+headerUrl and headerLabel (set in both folders)
+isUnderstanding and isTechniques (each set to true in respective folder)
+{% endcomment %}
+Skip to content
+
+ The following are Test Rules
+ {% if isTechniques -%}
+ related to this Technique.
+ {%- elsif isUnderstanding -%}
+ for certain aspects of this Success Criterion.
+ {%- endif %}
+ It is not necessary to use these particular Test Rules to check for conformance with WCAG, but they are defined and approved test methods.
+ For information on using Test Rules, see Understanding Test Rules for WCAG Success Criteria.
+
diff --git a/_includes/understanding/head.html b/_includes/understanding/head.html
new file mode 100644
index 0000000000..a4e625abff
--- /dev/null
+++ b/_includes/understanding/head.html
@@ -0,0 +1 @@
+
diff --git a/_includes/understanding/intro/advisory.html b/_includes/understanding/intro/advisory.html
new file mode 100644
index 0000000000..3c64c9a91c
--- /dev/null
+++ b/_includes/understanding/intro/advisory.html
@@ -0,0 +1,15 @@
+{%- if guideline.type == "Guideline" -%}
+
+ Specific techniques for meeting each Success Criterion for this guideline
+ are listed in the understanding sections for each Success Criterion (listed below).
+ If there are techniques, however, for addressing this guideline that do not fall under
+ any of the success criteria, they are listed here.
+ These techniques are not required or sufficient for meeting any success criteria,
+ but can make certain types of Web content more accessible to more people.
+
+{%- else -%}
+
+ Although not required for conformance, the following additional techniques should be considered in order to make content more accessible.
+ Not all techniques can be used or would be effective in all situations.
+
+ The following are common mistakes that are considered failures of this Success Criterion by the WCAG Working Group.
+
diff --git a/_includes/understanding/intro/resources.html b/_includes/understanding/intro/resources.html
new file mode 100644
index 0000000000..a782d78bdc
--- /dev/null
+++ b/_includes/understanding/intro/resources.html
@@ -0,0 +1 @@
+
Resources are for information purposes only, no endorsement implied.
diff --git a/_includes/understanding/intro/sufficient-situation.html b/_includes/understanding/intro/sufficient-situation.html
new file mode 100644
index 0000000000..fabc003240
--- /dev/null
+++ b/_includes/understanding/intro/sufficient-situation.html
@@ -0,0 +1,4 @@
+
+ Select the situation below that matches your content.
+ Each situation includes techniques or combinations of techniques that are known and documented to be sufficient for that situation.
+
diff --git a/_includes/understanding/intro/techniques.html b/_includes/understanding/intro/techniques.html
new file mode 100644
index 0000000000..bfb3ffa5e9
--- /dev/null
+++ b/_includes/understanding/intro/techniques.html
@@ -0,0 +1,8 @@
+
+ Each numbered item in this section represents a technique or combination of techniques
+ that the WCAG Working Group deems sufficient for meeting this Success Criterion.
+ However, it is not necessary to use these particular techniques.
+ For information on using other techniques, see
+ Understanding Techniques for WCAG Success Criteria,
+ particularly the "Other Techniques" section.
+
another visual means ensures users who cannot see color can still perceive the information.
-
Color is an important asset in design of Web content, enhancing its aesthetic appeal,
+
Color is an important asset in the design of Web content, enhancing its aesthetic appeal,
its usability, and its accessibility. However, some users have difficulty perceiving
color. People with partial sight often experience limited color vision, and many older
users do not see color well. In addition, people using limited-color or
From 17b1503d01c35adac6be94a09a0105c5b2c0aab8 Mon Sep 17 00:00:00 2001
From: Detlev Fischer
Date: Thu, 11 Jul 2024 00:17:00 +0200
Subject: [PATCH 03/25] Update test for H71.html (#3096)
Modify test section to include the requirement that legend is first
child in fieldset, as is technically required
Closes #3090
---------
Co-authored-by: Francis Storr
Co-authored-by: Patrick H. Lauke
---
techniques/html/H71.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/techniques/html/H71.html b/techniques/html/H71.html
index f80a0df693..4615b1dddc 100644
--- a/techniques/html/H71.html
+++ b/techniques/html/H71.html
@@ -112,7 +112,7 @@
Procedure
For groups of related controls where the individual labels for each control do not provide a sufficient description, and an additional group level description is needed:
Check that the group of logically related input or select elements are contained within fieldset elements.
-
Check that each fieldset has a legend element that includes a description for that group.
+
Check that each fieldset has a legend element that is the first child in the fieldset and includes a description for that group.
From b9c26dc1f25c9dd3f09788c8e54043141cc6d3b3 Mon Sep 17 00:00:00 2001
From: Francis Storr
Date: Wed, 10 Jul 2024 15:17:39 -0700
Subject: [PATCH 04/25] Editorial changes to Target Size Enhanced (#3901)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. Remove two `note-title-marker` elements that were adding extra “Note”
headings to notes.
2. Change `ul` element to `dl`
---
understanding/21/target-size-enhanced.html | 31 +++++++++++++---------
1 file changed, 18 insertions(+), 13 deletions(-)
diff --git a/understanding/21/target-size-enhanced.html b/understanding/21/target-size-enhanced.html
index 35887b9c4b..62f172fd39 100644
--- a/understanding/21/target-size-enhanced.html
+++ b/understanding/21/target-size-enhanced.html
@@ -3,7 +3,7 @@
Understanding Target Size (Enhanced)
-
+
Understanding SC 2.5.5 Target Size (Enhanced)
@@ -31,11 +31,9 @@
Intent
Equivalent targets: If there is more than one target on a screen that performs the same action, only one of the targets need to meet the target size of 44 by 44 CSS pixels.
Inline: Content displayed can often be reflowed based on the screen width available. This is known as responsive design and makes it easier to read since you do not need to scroll both horizontally and vertically. In reflowed content, the targets can appear anywhere on a line and can change position based on the width of the available screen. Since targets can appear anywhere on the line, the size cannot be larger than the available text and spacing between the sentences or paragraphs, otherwise the targets could overlap. It is for this reason targets which are contained within one or more sentences are excluded from the target size requirements.
-
Note
If the target is the full sentence and the sentence is not in a block of text, then the target needs to be at least 44 by 44 CSS pixels.
-
Note
A footnote or an icon within or at the end of a sentence is considered to be part of a sentence and therefore are excluded from the minimum target size.
User Agent Control: If the size of the target is not modified by the author through CSS or other size properties, then the target does not need to meet the target size of 44 by 44 CSS pixels.
@@ -55,16 +53,23 @@
Benefits
Examples
-
-
Example 1: Buttons Three buttons are on-screen and the touch target area of each button is 44 by 44 CSS pixels.
-
Example 2: Equivalent target Multiple targets are provided on the page that perform the same function. One of the targets is 44 by 44 CSS pixels. The other targets do not have a minimum touch target of 44 by 44 CSS pixels.
-
Example 3: Anchor Link The target is an in-page link and the target is less than 44 by 44 CSS pixels.
-
Example 4: Text Link in a paragraph Links within a paragraph of text have varying touch target dimensions. Links within
- paragraphs of text do no need to meet the 44 by 44 CSS pixels requirements.
-
Example 5: Text Link in a sentence A text link that is in a sentence is excluded and does not need to meet the 44 by 44 CSS pixel requirement. If the text link is the full sentence, then the text link target touch area does need to meet the 44 by 44 CSS pixels.
-
Example 6: Footnote A footnote link at the end of a sentence does not need to meet the 44 by 44 CSS pixels requirements. The footnote at the end of the sentence is considered to be part of the sentence.
-
Example 7: Help icon A help icon within or at the end of a sentence does not need to meet the 44 by 44 CSS pixels requirements. The icon at the end of the sentence is considered to be part of the sentence.
-
+
+
Example 1: Buttons
+
Three buttons are on-screen and the touch target area of each button is 44 by 44 CSS pixels.
+
Example 2: Equivalent target
+
Multiple targets are provided on the page that perform the same function. One of the targets is 44 by 44 CSS pixels. The other targets do not have a minimum touch target of 44 by 44 CSS pixels.
+
Example 3: Anchor Link
+
The target is an in-page link and the target is less than 44 by 44 CSS pixels.
+
Example 4: Text Link in a paragraph
+
Links within a paragraph of text have varying touch target dimensions. Links within
+ paragraphs of text do no need to meet the 44 by 44 CSS pixels requirements.
+
Example 5: Text Link in a sentence
+
A text link that is in a sentence is excluded and does not need to meet the 44 by 44 CSS pixel requirement. If the text link is the full sentence, then the text link target touch area does need to meet the 44 by 44 CSS pixels.
+
Example 6: Footnote
+
A footnote link at the end of a sentence does not need to meet the 44 by 44 CSS pixels requirements. The footnote at the end of the sentence is considered to be part of the sentence.
+
Example 7: Help icon
+
A help icon within or at the end of a sentence does not need to meet the 44 by 44 CSS pixels requirements. The icon at the end of the sentence is considered to be part of the sentence.
+
Resources
From 2d181a52d0dc42c254598d0171eb35d04f1cb169 Mon Sep 17 00:00:00 2001
From: Francis Storr
Date: Wed, 10 Jul 2024 15:18:08 -0700
Subject: [PATCH 05/25] Updated Page Titled for better links (#3824)
1. updated the "document collection" Understanding link to say WCAG 2.2
instead of 2.1.
2. updated the quoted page title for the Introduction to WCAG page.
3. linked to Introduction to WCAG page.
Note: I don't understand the references to the appendices on the WCAG
homepage. What do they have to do with page titles?
---------
Co-authored-by: Patrick H. Lauke
---
understanding/20/page-titled.html | 9 +++------
1 file changed, 3 insertions(+), 6 deletions(-)
diff --git a/understanding/20/page-titled.html b/understanding/20/page-titled.html
index f5ddc0c76a..a6c1f16ed8 100644
--- a/understanding/20/page-titled.html
+++ b/understanding/20/page-titled.html
@@ -98,13 +98,10 @@
Examples of Page Titled
that it will be displayed in the title bar of the user agent.
Major sections of the document collection are pages titled "Understanding Guideline X" and "Understanding Success Criterion X."
Appendix A has the title "Glossary."
Appendix B has the title "Acknowledgements."
Appendix C has the title "References."
From e293dc797c6c6235526da1ffabbcd79be55dbb3c Mon Sep 17 00:00:00 2001
From: "Patrick H. Lauke"
Date: Wed, 10 Jul 2024 23:19:01 +0100
Subject: [PATCH 06/25] Updates test steps in F94 (#3739)
- Removes 1280 pixels wide step
- Removes the unnecessary preamble before the test steps, and tweaks the
last line of the test procedure to reference the last step, rather than
giving it a number
- Additionally, cleans up formatting of the HTML
Closes https://github.com/w3c/wcag/issues/704
---------
Co-authored-by: Mike Gower
---
techniques/failures/F94.html | 14 +++++---------
1 file changed, 5 insertions(+), 9 deletions(-)
diff --git a/techniques/failures/F94.html b/techniques/failures/F94.html
index 3c47d95a74..ca87adc267 100644
--- a/techniques/failures/F94.html
+++ b/techniques/failures/F94.html
@@ -20,35 +20,31 @@
Description
The objective of this technique is to document the failure of text to re-scale when viewport units are used on text. As these units are relative to the viewport, it means they cannot be resized by zooming or adjusting text-size.
There are various methods to increase and decrease the size of text and other content, but viewport units applied to text (generally via font-size in CSS) prevent most available methods. Attempts to use browser controls to zoom or adjust text-size will not work. Only methods that completely override the CSS will work, and those could cause other issues such as layouts collapsing or text overlapping.
Some uses of viewport units may not prevent text-size adjustments, but if they are used as the primary method for defining text-size, they are likely to cause a failure of Success Criterion 1.4.4.
-
-
If media queries were used to adjust the size of text or unit of measure at different screen sizes, it may not be a failure of Resize Text. On-page controls provided by the author are also a way of passing the resize text success criteria.
+
If media queries were used to adjust the size of text or unit of measure at different screen sizes, it may not be a failure of Resize Text. On-page controls provided by the author are also a way of passing the resize text success criteria.
Examples
Failure example 1
The following CSS and HTML snippet uses VW units to size the text.
-
/* CSS */
+
/* CSS */
.callout {
font-size:1vw;
}<p class="callout">Text that scales by viewport units</p>
Use any of the following methods to resize text when available:
-
the zoom feature of the browser
+
the zoom feature of the browser,
the text-sizing feature of the browser,
on-page controls for resizing text.
@@ -59,7 +55,7 @@
Procedure
Expected Results
-
If step #5 is false, then this failure condition applies and the content fails Success Criteria 1.4.4, Resize Text.
+
If the last step is false, then this failure condition applies and the content fails Success Criterion 1.4.4 Resize Text.
From 916d649ec0a224d10a0ddbb7f3aa327e99a3c21f Mon Sep 17 00:00:00 2001
From: Scott O'Hara
Date: Wed, 10 Jul 2024 18:19:21 -0400
Subject: [PATCH 07/25] Update labels-or-instructions.html (#3888)
closes #3887
clarifies that wcag is not recommending use of placeholder text, and
instead recommends accompanying text to provide the information (which
could then be part of label or description - but those details are not
applicable to this SC so not specified in the update)
This helps better match [technique
g89](https://www.w3.org/WAI/WCAG22/Techniques/general/G89.html) which
includes the date format expectation as part of the label text
---------
Co-authored-by: Patrick H. Lauke
---
understanding/20/labels-or-instructions.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/understanding/20/labels-or-instructions.html b/understanding/20/labels-or-instructions.html
index fac5608a1f..8da1eca98a 100644
--- a/understanding/20/labels-or-instructions.html
+++ b/understanding/20/labels-or-instructions.html
@@ -92,7 +92,7 @@
Examples of Labels or Instructions
correct abbreviation.
-
A field for entering a date contains initial text which indicates the correct format
+
A field for entering a date has text instructions to indicate the correct format
for the date.
From 9a291c981790a391cc01c5930d4e8fe6b08ce2e9 Mon Sep 17 00:00:00 2001
From: Mike Gower
Date: Mon, 15 Jul 2024 08:52:08 -0700
Subject: [PATCH 08/25] Update H39.html (#2137)
Update to address https://github.com/w3c/wcag/issues/2136
Tighten the language of [H39 Using caption elements to associate data
table captions with data
tables](https://www.w3.org/WAI/WCAG22/Techniques/html/H39) to isolate it
to only passing 1.3.1 Info and Relationships:
1. if there is a visible text title/caption for the table, then
2. make sure it is referenced so that the title/caption is
programmaticalyy associated.
---------
Co-authored-by: Patrick H. Lauke
Co-authored-by: Francis Storr
---
techniques/html/H39.html | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/techniques/html/H39.html b/techniques/html/H39.html
index 19963593e2..1fc656d6df 100644
--- a/techniques/html/H39.html
+++ b/techniques/html/H39.html
@@ -40,15 +40,14 @@
Tests
Procedure
For each data table:
-
Check that the table has content that is presented as a table caption.
Check that the table includes a caption element.
-
Check that the content of the caption element identifies the table.
-
+
Check that the text that titles or describes the table is included in the caption element.
+
Expected Results
-
#1, #2, and #3 are true.
+
#1 and #2 are true.
From b2dc10aaa228c7c6e2b225f41b7788216d664970 Mon Sep 17 00:00:00 2001
From: EricDunsworth <1907279+EricDunsworth@users.noreply.github.com>
Date: Mon, 15 Jul 2024 11:52:46 -0400
Subject: [PATCH 09/25] Remove leftover references to Flash techniques (#3540)
A few references to FLASH5 and FLASH7 were missed when #1142 resolved
#1140.
The leftovers seem to have resulted in broken FLASH# links appearing in
some of WCAG 2.1 and 2.2's understanding pages:
* Understanding SC 2.4.4: Link Purpose (In Context) (Level A)
* WCAG 2.1:
https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-in-context#sufficient
* WCAG 2.2:
https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-in-context#sufficient
* Understanding SC 2.4.9: Link Purpose (Link Only) (Level AAA)
* WCAG 2.1:
https://www.w3.org/WAI/WCAG21/Understanding/link-purpose-link-only.html#sufficient
* WCAG 2.2:
https://www.w3.org/WAI/WCAG22/Understanding/link-purpose-link-only.html#sufficient
This should resolve it by removing the leftovers from the HTML and JSON
versions of affected understanding pages.
Closes https://github.com/w3c/wcag/issues/3928
---
guidelines/wcag.json | 32 -------------------
understanding/20/link-purpose-in-context.html | 12 -------
understanding/20/link-purpose-link-only.html | 12 -------
3 files changed, 56 deletions(-)
diff --git a/guidelines/wcag.json b/guidelines/wcag.json
index d396c17bed..8e1bdcf24e 100644
--- a/guidelines/wcag.json
+++ b/guidelines/wcag.json
@@ -4765,14 +4765,6 @@
"title":
"Using scripts to change the link text"
}
- ,
- {
- "id":
- "TECH:FLASH7"
- ,
- "title":
- ""
- }
]
@@ -4918,14 +4910,6 @@
,
{
"id":
- "TECH:FLASH5"
- ,
- "title":
- ""
- }
- ,
- {
- "id":
"TECH:H80"
,
"title":
@@ -5319,14 +5303,6 @@
"title":
"Using scripts to change the link text"
}
- ,
- {
- "id":
- "TECH:FLASH7"
- ,
- "title":
- ""
- }
]
@@ -5395,14 +5371,6 @@
,
{
"id":
- "TECH:FLASH5"
- ,
- "title":
- ""
- }
- ,
- {
- "id":
"TECH:H33"
,
"title":
diff --git a/understanding/20/link-purpose-in-context.html b/understanding/20/link-purpose-in-context.html
index 8d8ed0d0a8..4c7533ba7c 100644
--- a/understanding/20/link-purpose-in-context.html
+++ b/understanding/20/link-purpose-in-context.html
@@ -228,12 +228,6 @@
Sufficient Techniques for Link Purpose (In Context)
Supplementing link text with the title attribute
From 485e01b52871862bc3bc7bfd8440cb9e04ca4396 Mon Sep 17 00:00:00 2001
From: "Kenneth G. Franqueiro"
Date: Wed, 17 Jul 2024 13:23:02 -0400
Subject: [PATCH 10/25] Update deploy workflow to use Eleventy build process
(#3955)
This replaces the existing files under `.github` (both `workflows` and
`scripts`) with a new workflow using the Eleventy build process.
- Current behavior RE `conformance-challenges`, `guidelines`, and
`requirements` directories is retained
- Also ensures necessary CSS is updated under `guidelines`
- `techniques`, `understanding`, and `working-examples` are now
generated by the Eleventy build process
This has positive side effects over what's currently seen on github.io:
- Fixes broken styles
- Updates any auto-generated cross-links to guidelines to point to 2.2
editor's draft instead of 2.1
- All of the fixes mentioned in #3917
---
.github/scripts/deploy.sh | 28 ------------------
.github/workflows/11ty-publish.yaml | 44 ++++++++++++++++++++++++++++
.github/workflows/manual-publish.yml | 44 ----------------------------
11ty/README.md | 17 ++++++++---
11ty/cp-cvs.ts | 5 +++-
11ty/guidelines.ts | 2 +-
eleventy.config.ts | 15 ++++++----
7 files changed, 72 insertions(+), 83 deletions(-)
delete mode 100755 .github/scripts/deploy.sh
create mode 100644 .github/workflows/11ty-publish.yaml
delete mode 100644 .github/workflows/manual-publish.yml
diff --git a/.github/scripts/deploy.sh b/.github/scripts/deploy.sh
deleted file mode 100755
index 05e06b0bc3..0000000000
--- a/.github/scripts/deploy.sh
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/bin/bash
-set -exu
-# e: Exit immediately if a command exits with a non-zero status
-# x: Print commands and their arguments as they are executed
-# u: Treat unset variables as an error when substituting
-
-# NOTE: you probably need to add 'w3cbot' to the list of authorized users to push to your repository
-git config --global user.email 87540780+w3cgruntbot@users.noreply.github.com
-git config --global user.name w3cgruntbot
-git config --global user.password $GITHUB_TOKEN
-
-REPO_URL="https://w3cbot:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git"
-
-cd ${LOCAL_DIR}
-
-git remote set-url origin "${REPO_URL}"
-
-if [[ -z $(git status --porcelain) ]]; then
- echo "No changes to the output on this push; exiting."
- exit 0
-fi
-
-git add -A .
-git commit -m ":robot: Deploy to GitHub Pages: $GITHUB_SHA from branch $GITHUB_REF"
-
-git push $REPO_URL $BRANCH
-
-echo done
\ No newline at end of file
diff --git a/.github/workflows/11ty-publish.yaml b/.github/workflows/11ty-publish.yaml
new file mode 100644
index 0000000000..35ca84577b
--- /dev/null
+++ b/.github/workflows/11ty-publish.yaml
@@ -0,0 +1,44 @@
+name: CI
+
+# Reference documentation: https://docs.github.com/en/actions/reference
+
+# See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestbranchestags
+on:
+ push:
+ branches: [main]
+
+jobs:
+ main:
+ name: deploy (11ty)
+ runs-on: ubuntu-20.04
+ steps:
+ - name: Checkout the repository
+ uses: actions/checkout@v4
+ - name: Checkout gh-pages
+ uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+ path: _site
+ - name: Install Node.js and dependencies
+ uses: actions/setup-node@v4
+ with:
+ cache: npm
+ node-version-file: '.nvmrc'
+ - name: Build
+ env:
+ WCAG_MODE: editors
+ run: |
+ npm i
+ npm run build
+ cp guidelines/guidelines.css guidelines/relative-luminance.html _site/guidelines/22
+ curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/$GITHUB_REPOSITORY/main/guidelines/index.html -o _site/guidelines/22/index.html -f --retry 3
+ curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/$GITHUB_REPOSITORY/main/requirements/22/index.html -o _site/requirements/22/index.html -f --retry 3
+ curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/$GITHUB_REPOSITORY/main/conformance-challenges/index.html -o _site/conformance-challenges/index.html -f --retry 3
+ - name: Push
+ working-directory: _site
+ run: |
+ git config user.email 87540780+w3cgruntbot@users.noreply.github.com
+ git config user.name w3cgruntbot
+ git add -A .
+ git commit -m ":robot: Deploy to GitHub Pages: $GITHUB_SHA from branch $GITHUB_REF"
+ git push origin gh-pages
diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml
deleted file mode 100644
index f5eadf897c..0000000000
--- a/.github/workflows/manual-publish.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: CI
-
-# Reference documentation: https://docs.github.com/en/actions/reference
-
-# See https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#onpushpull_requestbranchestags
-on:
- push:
- branches: [main]
-
-jobs:
- main:
- name: deploy to
- runs-on: ubuntu-20.04
- env:
- GH_REF: github.com/w3c/wcag.git
- steps:
- - name: Checkout the repository
- uses: actions/checkout@v2
- - name: Setup Java
- # see https://github.com/actions/setup-java#supported-distributions
- # note that this also deploys ant
- uses: actions/setup-java@v2
- with:
- distribution: 'adopt'
- java-version: '11'
- - name: before_install
- run: |
- tar -xzvf lib/apache-ant-1.10.6-bin.tar.gz
- export PATH=`pwd`/apache-ant-1.10.6/bin:$PATH
- - name: script
- run: |
- mkdir output
- git clone --depth=1 --branch=gh-pages https://github.com/w3c/wcag.git output
- curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/w3c/wcag/main/guidelines/index.html -o output/guidelines/22/index.html -f --retry 3
- curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/w3c/wcag/main/requirements/22/index.html -o output/requirements/22/index.html -f --retry 3
- curl https://labs.w3.org/spec-generator/?type=respec"&"url=https://raw.githack.com/w3c/wcag/main/conformance-challenges/index.html -o output/conformance-challenges/index.html -f --retry 3
- ant deploy
- - name: deploy
- run: .github/scripts/deploy.sh
- env:
- BRANCH: gh-pages
- LOCAL_DIR: output
- GITHUB_TOKEN: ${{ secrets.W3CGRUNTBOT_TOKEN }}
-
diff --git a/11ty/README.md b/11ty/README.md
index 35b9ca85d8..048dab7cb4 100644
--- a/11ty/README.md
+++ b/11ty/README.md
@@ -40,16 +40,16 @@ Indicates top-level path of W3C CVS checkout, for WAI site updates (via `publish
### `WCAG_VERSION`
-**Usage context:** `publish-w3c` script only;
-this should currently not be changed, pending future improvements to `21` support.
+**Usage context:** currently this should not be changed, pending future improvements to `21` support
-Indicates WCAG version being built, in `XY` format (i.e. no `.`)
+Indicates WCAG version being built, in `XY` format (i.e. no `.`).
+Influences base URLs for links to guidelines, techniques, and understanding pages.
**Default:** `22`
### `WCAG_MODE`
-**Usage context:** should not need to be used manually except in specific testing scenarios
+**Usage context:** should not need to be set manually except in specific testing scenarios
Influences base URLs for links to guidelines, techniques, and understanding pages.
Typically set by specific npm scripts or CI processes.
@@ -60,6 +60,15 @@ Possible values:
- `editors` - Sets base URLs appropriate for `gh-pages` publishing; used by deploy action
- `publication` - Sets base URLs appropriate for WAI site publishing; used by `publish-w3c` script
+### `GITHUB_REPOSITORY`
+
+**Usage context:** Automatically set during GitHub workflows; should not need to be set manually
+
+Influences base URLs for links to guidelines, techniques, and understanding pages,
+when `WCAG_MODE=editors` is also set.
+
+**Default:** `w3c/wcag`
+
## Other points of interest
- The main configuration can be found in top-level `eleventy.config.ts`
diff --git a/11ty/cp-cvs.ts b/11ty/cp-cvs.ts
index 2bae08c4e7..3adf39bb7c 100644
--- a/11ty/cp-cvs.ts
+++ b/11ty/cp-cvs.ts
@@ -1,14 +1,17 @@
/** @fileoverview script to copy already-built output to CVS subfolders */
-import { copyFile, unlink } from "fs/promises";
import { glob } from "glob";
import { mkdirp } from "mkdirp";
+import { copyFile, unlink } from "fs/promises";
import { dirname, join } from "path";
+import { assertIsWcagVersion } from "./guidelines";
+
const outputBase = "_site";
const cvsBase = process.env.WCAG_CVSDIR || "../../../w3ccvs";
const wcagVersion = process.env.WCAG_VERSION || "22";
+assertIsWcagVersion(wcagVersion);
const wcagBase = `${cvsBase}/WWW/WAI/WCAG${wcagVersion}`;
// Map (git) sources to (CVS) destinations, since some don't match case-sensitively
diff --git a/11ty/guidelines.ts b/11ty/guidelines.ts
index 5b081fc6bb..7d68a3bb4a 100644
--- a/11ty/guidelines.ts
+++ b/11ty/guidelines.ts
@@ -8,7 +8,7 @@ import { flattenDomFromFile, load } from "./cheerio";
import { generateId } from "./common";
export type WcagVersion = "20" | "21" | "22";
-function assertIsWcagVersion(v: string): asserts v is WcagVersion {
+export function assertIsWcagVersion(v: string): asserts v is WcagVersion {
if (!/^2[012]$/.test(v)) throw new Error(`Unexpected version found: ${v}`);
}
diff --git a/eleventy.config.ts b/eleventy.config.ts
index 9528e68a12..205e253948 100644
--- a/eleventy.config.ts
+++ b/eleventy.config.ts
@@ -3,7 +3,7 @@ import compact from "lodash-es/compact";
import { copyFile } from "fs/promises";
import { CustomLiquid } from "11ty/CustomLiquid";
-import { actRules, getFlatGuidelines, getPrinciples } from "11ty/guidelines";
+import { actRules, assertIsWcagVersion, getFlatGuidelines, getPrinciples } from "11ty/guidelines";
import {
getFlatTechniques,
getTechniqueAssociations,
@@ -15,7 +15,8 @@ import { generateUnderstandingNavMap, getUnderstandingDocs } from "11ty/understa
import type { EleventyContext, EleventyData, EleventyEvent } from "11ty/types";
/** Version of WCAG to build */
-const version = "22";
+const version = process.env.WCAG_VERSION || "22";
+assertIsWcagVersion(version);
const principles = await getPrinciples();
const flatGuidelines = getFlatGuidelines(principles);
@@ -45,6 +46,8 @@ export type GlobalData = EleventyData &
isUnderstanding?: boolean;
};
+const [GH_ORG, GH_REPO] = (process.env.GITHUB_REPOSITORY || "w3c/wcag").split("/");
+
const baseUrls = {
guidelines: `https://www.w3.org/TR/WCAG${version}/`,
techniques: "/techniques/",
@@ -53,9 +56,11 @@ const baseUrls = {
if (process.env.WCAG_MODE === "editors") {
// For pushing to gh-pages
- baseUrls.guidelines = "https://w3c.github.io/wcag/guidelines/";
- baseUrls.techniques = "https://w3c.github.io/wcag/techniques/";
- baseUrls.understanding = "https://w3c.github.io/wcag/understanding/";
+ baseUrls.guidelines = `https://${GH_ORG}.github.io/${GH_REPO}/guidelines/${
+ version === "21" ? "" : `${version}/`
+ }`;
+ baseUrls.techniques = `https://${GH_ORG}.github.io/${GH_REPO}/techniques/`;
+ baseUrls.understanding = `https://${GH_ORG}.github.io/${GH_REPO}/understanding/`;
} else if (process.env.WCAG_MODE === "publication") {
// For pushing to W3C site
baseUrls.guidelines = `https://www.w3.org/TR/WCAG${version}/`;
From 0440c75c71059fa18eb22dc56476cd57d9d1b576 Mon Sep 17 00:00:00 2001
From: "Kenneth G. Franqueiro"
Date: Wed, 17 Jul 2024 14:43:02 -0400
Subject: [PATCH 11/25] Fix token for Eleventy deploy workflow (#3972)
---
.github/workflows/11ty-publish.yaml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/11ty-publish.yaml b/.github/workflows/11ty-publish.yaml
index 35ca84577b..adbb274be0 100644
--- a/.github/workflows/11ty-publish.yaml
+++ b/.github/workflows/11ty-publish.yaml
@@ -19,6 +19,7 @@ jobs:
with:
ref: gh-pages
path: _site
+ token: ${{ secrets.W3CGRUNTBOT_TOKEN }}
- name: Install Node.js and dependencies
uses: actions/setup-node@v4
with:
From e1f139fca722ccc60fed90494f95d3e136258d9b Mon Sep 17 00:00:00 2001
From: "Kenneth G. Franqueiro"
Date: Mon, 22 Jul 2024 11:14:34 -0400
Subject: [PATCH 12/25] Don't force term names to lowercase in definition lists
(#3978)
Fixes a couple of issues in the build system:
- A few term names (e.g. ASCII art) have uppercase letters, which the
build was forcing to lowercase (this was apparently also true of the
XSLT build)
- Any linked terms containing newlines would not be recognized
---
11ty/CustomLiquid.ts | 8 ++++++--
11ty/guidelines.ts | 8 ++++++--
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/11ty/CustomLiquid.ts b/11ty/CustomLiquid.ts
index 49e071fd7a..9f8da18338 100644
--- a/11ty/CustomLiquid.ts
+++ b/11ty/CustomLiquid.ts
@@ -393,7 +393,7 @@ export class CustomLiquid extends Liquid {
// where we have access to global data and the about box's HTML
const $termLinks = $(termLinkSelector);
const extractTermName = ($el: Cheerio) => {
- const name = $el.text().trim().toLowerCase();
+ const name = $el.text().toLowerCase().trim().replace(/\s*\n+\s*/, " ");
const term = termsMap[name];
if (!term) {
console.warn(`${scope.page.inputPath}: Term not found: ${name}`);
@@ -433,7 +433,11 @@ export class CustomLiquid extends Liquid {
}
// Iterate over sorted names to populate alphabetized Key Terms definition list
- termNames.sort();
+ termNames.sort((a, b) => {
+ if (a.toLowerCase() < b.toLowerCase()) return -1;
+ if (a.toLowerCase() > b.toLowerCase()) return 1;
+ return 0;
+ });
for (const name of termNames) {
const term = termsMap[name]; // Already verified existence in the earlier loop
$termsList.append(
diff --git a/11ty/guidelines.ts b/11ty/guidelines.ts
index 7d68a3bb4a..def71cc02f 100644
--- a/11ty/guidelines.ts
+++ b/11ty/guidelines.ts
@@ -208,11 +208,15 @@ export async function getTermsMap() {
// but the XSLT process generates id from the element's text which is not always the same
id: `dfn-${generateId($el.text())}`,
definition: getContentHtml($el.parent().next()),
- name: $el.text().toLowerCase(),
+ name: $el.text(),
trId: el.attribs.id,
};
- const names = [term.name].concat((el.attribs["data-lt"] || "").toLowerCase().split("|"));
+ // Include both original and all-lowercase version to simplify lookups
+ // (since most synonyms are lowercase) while preserving case in name
+ const names = [term.name, term.name.toLowerCase()].concat(
+ (el.attribs["data-lt"] || "").toLowerCase().split("|")
+ );
for (const name of names) terms[name] = term;
});
From 588d0c007babd19f7aa80611a0dd14cb798c9687 Mon Sep 17 00:00:00 2001
From: "Patrick H. Lauke"
Date: Thu, 25 Jul 2024 01:27:58 +0100
Subject: [PATCH 13/25] Fix broken description paragraph for SM6 (#3932)
Closes https://github.com/w3c/wcag/issues/3919
---
techniques/smil/SM6.html | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/techniques/smil/SM6.html b/techniques/smil/SM6.html
index ade2f38f7a..51c9d0ecc0 100644
--- a/techniques/smil/SM6.html
+++ b/techniques/smil/SM6.html
@@ -18,10 +18,10 @@
When to Use
Description
The objective of this technique is to provide a way for people who are blind
- able to access the material. With this technique a description of the video
or otherwise have trouble seeing the video in audio-visual material to be
- in the audio-visual material.
+ able to access the material. With this technique a description of the video
is provided via audio description that will fit into the gaps in the dialogue
+ in the audio-visual material.
From 47a54da7e0bb2a575c6798e40904fa5caedf3f54 Mon Sep 17 00:00:00 2001
From: Momdo Nakamura
Date: Thu, 25 Jul 2024 09:28:39 +0900
Subject: [PATCH 15/25] Fix link in Timing Adjustable (#3944)
Fixes a broken link where the `href` had `http://https://` instead of
`https://`
---
understanding/20/timing-adjustable.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/understanding/20/timing-adjustable.html b/understanding/20/timing-adjustable.html
index 8889700473..088983cb0a 100644
--- a/understanding/20/timing-adjustable.html
+++ b/understanding/20/timing-adjustable.html
@@ -45,7 +45,7 @@
Intent of Timing Adjustable
It also includes content that is advancing or updating at a rate beyond the user's ability to read and/or understand it. In other words, animated, moving or scrolling content introduces a time limit on a users ability to read content.
-
This success criterion is generally not applicable when the content repeats or is synchronized with other content, so long as the information and data is adjustable or otherwise under the control of the end user. Examples of time limits for which this success criterion is not applicable include scrolling text that repeats, captioning, and carousels. These are situations which do include time limits, but the content is still available to the user because it has controls for accessing it, as specified in 2.2.2 Pause, Stop, Hide.
+
This success criterion is generally not applicable when the content repeats or is synchronized with other content, so long as the information and data is adjustable or otherwise under the control of the end user. Examples of time limits for which this success criterion is not applicable include scrolling text that repeats, captioning, and carousels. These are situations which do include time limits, but the content is still available to the user because it has controls for accessing it, as specified in 2.2.2 Pause, Stop, Hide.
In some cases, however, it is not possible to change the time limit (for example, for an auction or other real-time event) and exceptions are therefore provided for those cases.
From 2f24a2f2f229e3b0cb8900e1bdddf6da1f80e3a3 Mon Sep 17 00:00:00 2001
From: Giacomo Petri
Date: Thu, 25 Jul 2024 02:29:32 +0200
Subject: [PATCH 16/25] Technique F110 - Added examples for sticky header and
sticky footer that covers completely elements receiving focus (#3775)
Closes: #3764
Description:
- Added examples showcasing the failure of Technique F110.
- Updated F110 by incorporating links to the failing examples.
Note: The examples contain extensive content (utilizing "Lorem Ipsum"
text) to ensure that regardless of the viewport, zoom level, or user
preferences, at least one element receiving focus is consistently
obscured by the sticky element.
Edit:
Demos available here:
- Sticky header: https://codepen.io/Giacomo-Petri/full/OJGQOBy
- Sticky footer: https://codepen.io/Giacomo-Petri/full/KKYQyrq
---
techniques/failures/F110.html | 8 +-
.../sticky-footer.html | 160 +++++++++++++++++
.../sticky-header.html | 162 ++++++++++++++++++
3 files changed, 329 insertions(+), 1 deletion(-)
create mode 100644 working-examples/sticky-elements-hiding-focused-elements/sticky-footer.html
create mode 100644 working-examples/sticky-elements-hiding-focused-elements/sticky-header.html
diff --git a/techniques/failures/F110.html b/techniques/failures/F110.html
index 8ed761cb64..6327f0b4ae 100644
--- a/techniques/failures/F110.html
+++ b/techniques/failures/F110.html
@@ -25,11 +25,17 @@
Examples
Sticky footer
A Web page has a sticky footer, an element that stays visible at the bottom of the viewport as the user scrolls the page. The footer is tall enough to completely cover the element in focus as a user tabs down the page.
A Web page has a sticky header, an element that stays visible at the top of the viewport as the user scrolls the page. The header is tall enough to completely cover the element in focus as a user tabs up the page.
diff --git a/working-examples/sticky-elements-hiding-focused-elements/sticky-footer.html b/working-examples/sticky-elements-hiding-focused-elements/sticky-footer.html
new file mode 100644
index 0000000000..5a29bf2824
--- /dev/null
+++ b/working-examples/sticky-elements-hiding-focused-elements/sticky-footer.html
@@ -0,0 +1,160 @@
+
+
+
+ Sticky footer is hiding focused elements
+
+
+
+
+
Sticky footer is hiding focused elements
+
+
This demo page is crafted to illustrate Example 1: Sticky footer of the failing technique F110: Failure of Success Criterion 2.4.11 due to a sticky footers or headers hiding focused elements.
+
All links and buttons on this page are non-functional; links merely serve as focusable elements and anchor directing to the top of the page; buttons are simply not functional.
+
To identify the problem, please navigate through the page using the TAB key until one of the elements receiving focus becomes obscured by the sticky element.
+
+
+
You will rejoice to hear that no disaster has accompanied the commencement of an enterprise which you have regarded with such evil forebodings. I arrived here yesterday, and my first task is to assure my dear sister of my welfare and increasing confidence in the success of my undertaking.
+
I am already far north of London, and as I walk in the streets of Petersburgh, I feel a cold northern breeze play upon my cheeks, which braces my nerves and fills me with delight. Do you understand this feeling? This breeze, which has travelled from the regions towards which I am advancing, gives me a foretaste of those icy climes. Inspirited by this wind of promise, my daydreams become more fervent and vivid. I try in vain to be persuaded that the pole is the seat of frost and desolation; it ever presents itself to my imagination as the region of beauty and delight. There, Margaret, the sun is for ever visible, its broad disk just skirting the horizon and diffusing a perpetual splendour. There—for with your leave, my sister, I will put some trust in preceding navigators—there snow and frost are banished; and, sailing over a calm sea, we may be wafted to a land surpassing in wonders and in beauty every region hitherto discovered on the habitable globe. Its productions and features may be without example, as the phenomena of the heavenly bodies undoubtedly are in those undiscovered solitudes. What may not be expected in a country of eternal light? I may there discover the wondrous power which attracts the needle and may regulate a thousand celestial observations that require only this voyage to render their seeming eccentricities consistent for ever. I shall satiate my ardent curiosity with the sight of a part of the world never before visited, and may tread a land never before imprinted by the foot of man. These are my enticements, and they are sufficient to conquer all fear of danger or death and to induce me to commence this laborious voyage with the joy a child feels when he embarks in a little boat, with his holiday mates, on an expedition of discovery up his native river. But supposing all these conjectures to be false, you cannot contest the inestimable benefit which I shall confer on all mankind, to the last generation, by discovering a passage near the pole to those countries, to reach which at present so many months are requisite; or by ascertaining the secret of the magnet, which, if at all possible, can only be effected by an undertaking such as mine.
+
These reflections have dispelled the agitation with which I began my letter, and I feel my heart glow with an enthusiasm which elevates me to heaven, for nothing contributes so much to tranquillise the mind as a steady purpose—a point on which the soul may fix its intellectual eye. This expedition has been the favourite dream of my early years. I have read with ardour the accounts of the various voyages which have been made in the prospect of arriving at the North Pacific Ocean through the seas which surround the pole. You may remember that a history of all the voyages made for purposes of discovery composed the whole of our good Uncle Thomas’ library. My education was neglected, yet I was passionately fond of reading. These volumes were my study day and night, and my familiarity with them increased that regret which I had felt, as a child, on learning that my father’s dying injunction had forbidden my uncle to allow me to embark in a seafaring life.
+
These visions faded when I perused, for the first time, those poets whose effusions entranced my soul and lifted it to heaven. I also became a poet and for one year lived in a paradise of my own creation; I imagined that I also might obtain a niche in the temple where the names of Homer and Shakespeare are consecrated. You are well acquainted with my failure and how heavily I bore the disappointment. But just at that time I inherited the fortune of my cousin, and my thoughts were turned into the channel of their earlier bent.
+
Six years have passed since I resolved on my present undertaking. I can, even now, remember the hour from which I dedicated myself to this great enterprise. I commenced by inuring my body to hardship. I accompanied the whale-fishers on several expeditions to the North Sea; I voluntarily endured cold, famine, thirst, and want of sleep; I often worked harder than the common sailors during the day and devoted my nights to the study of mathematics, the theory of medicine, and those branches of physical science from which a naval adventurer might derive the greatest practical advantage. Twice I actually hired myself as an under-mate in a Greenland whaler, and acquitted myself to admiration. I must own I felt a little proud when my captain offered me the second dignity in the vessel and entreated me to remain with the greatest earnestness, so valuable did he consider my services.
+
And now, dear Margaret, do I not deserve to accomplish some great purpose? My life might have been passed in ease and luxury, but I preferred glory to every enticement that wealth placed in my path. Oh, that some encouraging voice would answer in the affirmative! My courage and my resolution is firm; but my hopes fluctuate, and my spirits are often depressed. I am about to proceed on a long and difficult voyage, the emergencies of which will demand all my fortitude: I am required not only to raise the spirits of others, but sometimes to sustain my own, when theirs are failing.
+
This is the most favourable period for travelling in Russia. They fly quickly over the snow in their sledges; the motion is pleasant, and, in my opinion, far more agreeable than that of an English stagecoach. The cold is not excessive, if you are wrapped in furs—a dress which I have already adopted, for there is a great difference between walking the deck and remaining seated motionless for hours, when no exercise prevents the blood from actually freezing in your veins. I have no ambition to lose my life on the post-road between St. Petersburgh and Archangel.
+
I shall depart for the latter town in a fortnight or three weeks; and my intention is to hire a ship there, which can easily be done by paying the insurance for the owner, and to engage as many sailors as I think necessary among those who are accustomed to the whale-fishing. I do not intend to sail until the month of June; and when shall I return? Ah, dear sister, how can I answer this question? If I succeed, many, many months, perhaps years, will pass before you and I may meet. If I fail, you will see me again soon, or never.
+
Farewell, my dear, excellent Margaret. Heaven shower down blessings on you, and save me, that I may again and again testify my gratitude for all your love and kindness.
This demo page is crafted to illustrate Example 1: Sticky header of the failing technique F110: Failure of Success Criterion 2.4.11 due to a sticky footers or headers hiding focused elements.
+
All links and buttons on this page are non-functional; links merely serve as focusable elements and anchor directing to the top of the page; buttons are simply not functional.
+
To identify the problem, please navigate through the page using the TAB key until you reach the bottom, then return to the top using SHIFT + TAB.
+
+
+
You will rejoice to hear that no disaster has accompanied the commencement of an enterprise which you have regarded with such evil forebodings. I arrived here yesterday, and my first task is to assure my dear sister of my welfare and increasing confidence in the success of my undertaking.
+
I am already far north of London, and as I walk in the streets of Petersburgh, I feel a cold northern breeze play upon my cheeks, which braces my nerves and fills me with delight. Do you understand this feeling? This breeze, which has travelled from the regions towards which I am advancing, gives me a foretaste of those icy climes. Inspirited by this wind of promise, my daydreams become more fervent and vivid. I try in vain to be persuaded that the pole is the seat of frost and desolation; it ever presents itself to my imagination as the region of beauty and delight. There, Margaret, the sun is for ever visible, its broad disk just skirting the horizon and diffusing a perpetual splendour. There—for with your leave, my sister, I will put some trust in preceding navigators—there snow and frost are banished; and, sailing over a calm sea, we may be wafted to a land surpassing in wonders and in beauty every region hitherto discovered on the habitable globe. Its productions and features may be without example, as the phenomena of the heavenly bodies undoubtedly are in those undiscovered solitudes. What may not be expected in a country of eternal light? I may there discover the wondrous power which attracts the needle and may regulate a thousand celestial observations that require only this voyage to render their seeming eccentricities consistent for ever. I shall satiate my ardent curiosity with the sight of a part of the world never before visited, and may tread a land never before imprinted by the foot of man. These are my enticements, and they are sufficient to conquer all fear of danger or death and to induce me to commence this laborious voyage with the joy a child feels when he embarks in a little boat, with his holiday mates, on an expedition of discovery up his native river. But supposing all these conjectures to be false, you cannot contest the inestimable benefit which I shall confer on all mankind, to the last generation, by discovering a passage near the pole to those countries, to reach which at present so many months are requisite; or by ascertaining the secret of the magnet, which, if at all possible, can only be effected by an undertaking such as mine.
+
These reflections have dispelled the agitation with which I began my letter, and I feel my heart glow with an enthusiasm which elevates me to heaven, for nothing contributes so much to tranquillise the mind as a steady purpose—a point on which the soul may fix its intellectual eye. This expedition has been the favourite dream of my early years. I have read with ardour the accounts of the various voyages which have been made in the prospect of arriving at the North Pacific Ocean through the seas which surround the pole. You may remember that a history of all the voyages made for purposes of discovery composed the whole of our good Uncle Thomas’ library. My education was neglected, yet I was passionately fond of reading. These volumes were my study day and night, and my familiarity with them increased that regret which I had felt, as a child, on learning that my father’s dying injunction had forbidden my uncle to allow me to embark in a seafaring life.
+
These visions faded when I perused, for the first time, those poets whose effusions entranced my soul and lifted it to heaven. I also became a poet and for one year lived in a paradise of my own creation; I imagined that I also might obtain a niche in the temple where the names of Homer and Shakespeare are consecrated. You are well acquainted with my failure and how heavily I bore the disappointment. But just at that time I inherited the fortune of my cousin, and my thoughts were turned into the channel of their earlier bent.
+
Six years have passed since I resolved on my present undertaking. I can, even now, remember the hour from which I dedicated myself to this great enterprise. I commenced by inuring my body to hardship. I accompanied the whale-fishers on several expeditions to the North Sea; I voluntarily endured cold, famine, thirst, and want of sleep; I often worked harder than the common sailors during the day and devoted my nights to the study of mathematics, the theory of medicine, and those branches of physical science from which a naval adventurer might derive the greatest practical advantage. Twice I actually hired myself as an under-mate in a Greenland whaler, and acquitted myself to admiration. I must own I felt a little proud when my captain offered me the second dignity in the vessel and entreated me to remain with the greatest earnestness, so valuable did he consider my services.
+
And now, dear Margaret, do I not deserve to accomplish some great purpose? My life might have been passed in ease and luxury, but I preferred glory to every enticement that wealth placed in my path. Oh, that some encouraging voice would answer in the affirmative! My courage and my resolution is firm; but my hopes fluctuate, and my spirits are often depressed. I am about to proceed on a long and difficult voyage, the emergencies of which will demand all my fortitude: I am required not only to raise the spirits of others, but sometimes to sustain my own, when theirs are failing.
+
This is the most favourable period for travelling in Russia. They fly quickly over the snow in their sledges; the motion is pleasant, and, in my opinion, far more agreeable than that of an English stagecoach. The cold is not excessive, if you are wrapped in furs—a dress which I have already adopted, for there is a great difference between walking the deck and remaining seated motionless for hours, when no exercise prevents the blood from actually freezing in your veins. I have no ambition to lose my life on the post-road between St. Petersburgh and Archangel.
+
I shall depart for the latter town in a fortnight or three weeks; and my intention is to hire a ship there, which can easily be done by paying the insurance for the owner, and to engage as many sailors as I think necessary among those who are accustomed to the whale-fishing. I do not intend to sail until the month of June; and when shall I return? Ah, dear sister, how can I answer this question? If I succeed, many, many months, perhaps years, will pass before you and I may meet. If I fail, you will see me again soon, or never.
+
Farewell, my dear, excellent Margaret. Heaven shower down blessings on you, and save me, that I may again and again testify my gratitude for all your love and kindness.
- This technique uses the status role from the ARIA specification to notify Assistive Technologies (AT) when content has been updated with information about the user's or application's status. This is done by adding role="status" to the element that contains the status message. The aria live region role of status has an implicit aria-live value of polite, which allows a user to be notified via AT (such as a screen reader) when status messages are added. The role of status also has a default aria-atomic value of true, so that updates to the container marked with a role of status will result in the AT presenting the entire contents of the container to the user, including any author-defined labels (or additional nested elements). Such additional context can be critical where the status message text alone will not provide an equivalent to the visual experience. The content of the aria-live container is automatically read by the AT, without the AT having to focus on the place where the text is displayed. See WAI-ARIA status (role) for more details.
+ This technique uses the status role from the ARIA specification to notify Assistive Technologies (AT) when content has been updated with information about the user's or application's status. This is done by adding role="status" to the element that contains the status message. The aria live region role of status has an implicit aria-live value of polite, which allows a user to be notified via AT (such as a screen reader) when status messages are added. The role of status also has a default aria-atomic value of true, so that updates to the container marked with a role of status will result in the AT presenting the entire contents of the container to the user, including any author-defined labels (or additional nested elements). Such additional context can be critical where the status message text alone will not provide an equivalent to the visual experience. The content of the aria-live container is automatically read by the AT, without the AT having to focus on the place where the text is displayed. See WAI-ARIA status (role) for more details.
@@ -45,7 +45,7 @@
the new content does not take focus (does not change context);
the new content provides information to the user on the outcome of an action, the state of an application, the progress of a process, or the existence of errors.
- Where updated content does not conform to the definition of status message, a failure of 4.1.3 has not taken place.
+ Where updated content does not conform to the definition of status messages, a failure of 4.1.3 has not taken place.
The second step in this failure technique involves examining code. Where dynamic content meets the definition of a status message, its container can be examined for an appropriate WAI-ARIA role or property which allows it to be programmatically determinable as a status message. Currently there are only a small number of techniques available to indicate status messages to assistive technologies. They are:
Requiring users to authenticate by entering a password or code in a different format from which it was originally created is a failure to meet Success Criteria 3.3.8 and 3.3.9 (unless alternative authentication methods are available). The string to be entered could include a password, verification code, or any string of characters the user has to remember or record to authenticate.
-
If a user is required to enter individual characters across multiple fields in a way that prevents pasting the password in a single action, it prevents use of a password manager or pasting from local copy of the password. This means users cannot avoid transcription, resulting in a cognitive function test. This applies irrespective of whether users are required to enter all characters in the string, or just a subset.
+
If a user is required to enter individual characters across multiple fields in a way that prevents pasting the password in a single action, it prevents use of a password manager or pasting from local copy of the password. This means users cannot avoid transcription, resulting in a cognitive function test. This applies irrespective of whether users are required to enter all characters in the string, or just a subset.
diff --git a/techniques/failures/F61.html b/techniques/failures/F61.html
index 9de1b0738b..334cd528c5 100644
--- a/techniques/failures/F61.html
+++ b/techniques/failures/F61.html
@@ -3,7 +3,7 @@
automatic update that the user cannot disable from within the content
ID: F61
Technology: failures
Type: Failure
When to Use
General
Description
-
This document describes a failure that occurs when the content in the main viewport viewport is automatically updated, and there is no option for a user to disable this behavior.
+
This document describes a failure that occurs when the content in the main viewport is automatically updated, and there is no option for a user to disable this behavior.
Two procedures are presented below to test for the existence of a failure against Success Criterion 3.2.5. Procedure 1 is the preferred procedure and assumes that content authors have access to the code that generates the viewport content.
However there may be instances where this may not be possible (eg: in certain content management systems, application environments such as django or ruby-on-rails, or content generated through scripting languages such as AJAX or PHP that are generated by third parties.) To that end, the second procedure is supplied to allow testing in these instances. Note that timeframes are indicative only, and that any change after any amount of time should be treated as a failure if the test otherwise does not pass the other step evaluations.
In native buttons in iOS and Android onclick events are triggered on the up-event by default.
-
The WCAG standard itself applies to web pages at a URL, and therefore this example is provided as helpful supplementary advice for those looking to implement the WCAG2ICT for native applications.
+
The WCAG standard itself applies to web pages, and therefore this example is provided as helpful supplementary advice for those looking to implement the WCAG2ICT for native applications.
@@ -50,7 +50,7 @@
Procedure
Activate the down-event then move the pointer outside the target before triggering the up-event, and then release the pointer to trigger the up-event.
Check that the action was not triggered when the pointer is released outside of the hit area for the target.
-
If the action is triggered, check that the action is reversible.
+
If the action is triggered, check that the action is reversible.
The objective of this technique is to ensure that users can obtain an accessible version of content where both non-conforming and conforming versions are provided.
-
Conformance Requirement 1 allows non-conforming pages to be included within the scope of conformance as long as they have a "conforming alternate version". It is not always possible for authors to include accessibility supported links to conforming content from within non-conforming content. Therefore, authors may need to rely on the use of Server Side Scripting technologies (ex. PHP, ASP, JSP) to ensure that the non-conforming version can only be reached from a conforming page.
+
Conformance Requirement 1 allows non-conforming pages to be included within the scope of conformance as long as they have a conforming alternate version. It is not always possible for authors to include accessibility supported links to conforming content from within non-conforming content. Therefore, authors may need to rely on the use of Server Side Scripting technologies (ex. PHP, ASP, JSP) to ensure that the non-conforming version can only be reached from a conforming page.
This technique describes how to use information provided by the HTTP referer to ensure that non-conforming content can only be reached from a conforming page. The HTTP referer header is set by the user agent and contains the URI of the page (if any) which referred the user agent to the non-conforming page.
To implement this technique, an author identifies the URI for the conforming version of the content, for each non-conforming page. When a request for the non-conforming version of a page is received, the server compares the value of the HTTP referer header against the URI of the conforming version to determine whether the link to the non-conforming version came from the conforming version. The non-conforming version is only served if the HTTP referer matches the URI of the non-conforming version. Otherwise, the user is redirected to the conforming version of the content. Note that when comparing the URI in the HTTP referer header, non-relevant variations in the URI, such as in the query and target, should be taken into account.
on what the control represents. Specifics about such information are defined by other
specifications, such as WAI-ARIA, or the
relevant platform standards. Another factor to consider is whether there is sufficient
- accessibility support
- with assistive technologies to convey the information as specified.
+ accessibility support with assistive technologies to convey the information as specified.
A particularly important state of a user interface control is whether or not it has
diff --git a/understanding/20/three-flashes-or-below-threshold.html b/understanding/20/three-flashes-or-below-threshold.html
index e0de2ab4f2..9fa345e15f 100644
--- a/understanding/20/three-flashes-or-below-threshold.html
+++ b/understanding/20/three-flashes-or-below-threshold.html
@@ -53,7 +53,7 @@
Intent of Three Flashes or Below Threshold
represents the central vision portion of the eye, where people are most susceptible
to photo stimuli.)
-
With the proliferation of devices of varying screen sizes (from small hand-helds to large living room displays), as well as the adoption of CSS pixels as a density-independent unit of measurement, the prior assessment criteria may seem outdated. However, an image of a consistent size uses up relatively the same percentage of a user's visual field on any device. On a large screen, the image takes up less size, but the large screen takes up a larger part of the visual field. On a mobile screen, the image may take up most or all of the screen; however, the mobile screen itself takes up a smaller portion of the user's visual field. So the same dimension of the flashing content, represented in CSS pixels can still provide a consistent means of assessment. Substituting CSS pixels for the original pixel block means that the combined area of flashing becomes 341 x 256 CSS pixels, or a flashing area of 87,296 CSS pixels.
+
With the proliferation of devices of varying screen sizes (from small hand-helds to large living room displays), as well as the adoption of CSS pixels as a density-independent unit of measurement, the prior assessment criteria may seem outdated. However, an image of a consistent size uses up relatively the same percentage of a user's visual field on any device. On a large screen, the image takes up less size, but the large screen takes up a larger part of the visual field. On a mobile screen, the image may take up most or all of the screen; however, the mobile screen itself takes up a smaller portion of the user's visual field. So the same dimension of the flashing content, represented in CSS pixels can still provide a consistent means of assessment. Substituting CSS pixels for the original pixel block means that the combined area of flashing becomes 341 x 256 CSS pixels, or a flashing area of 87,296 CSS pixels.
Content should be analyzed at the largest scale at which a user may view the content, and at the standard zoom level of the user agent. For example, with a video that may play in an area of a web page and also at full screen, the video should be analyzed for risks at full screen.
adequate contrast against the background in each of its states, Focus Appearance requires sufficient
contrast for the focus indicator itself.
-
For sighted people with mobility impairments who use a keyboard or a device that utilizes the keyboard interface (such as a switch or
- voice input), knowing the current point of focus is very important. Visible focus must also meet the needs
+
For sighted people with mobility impairments who use a keyboard or a device that utilizes the keyboard interface
+ (such as a switch or voice input), knowing the current point of focus is very important. Visible focus must also meet the needs
of users with low vision, who may also rely on the keyboard.
A keyboard focus indicator can take different forms. This Success Criterion encourages the use of a solid
@@ -594,9 +593,8 @@
Focus indicator around only the subcomponent
Where something with focus is not a user interface component
Some pages contain very large editing regions, such as web implementations of word processors and code
editors. Unlike a textarea element, which is a user interface component, these large
- editing regions do not typically meet the definition of user interface
- components; they are not "perceived by users as a single control for a distinct function."
+ editing regions do not typically meet the definition of user interface components;
+ they are not "perceived by users as a single control for a distinct function."
Providing focus indicators around such editing regions may still be beneficial to some; however, where
the region is not perceived as a single control, it is not covered by this Success Criterion. The web
page will still need to provide a insertion point (caret indicator) in such editing regions in order to
@@ -675,8 +673,7 @@
Modifying the focus indicator background
Altering the body element's background-color attribute
is one way of altering the pixels directly adjacent to the indicator in most
implementations. However, specifying a value of white (#FFFFFF) does not nullify this
- exception since, as established in the third note of the contrast ratio definition, the
+ exception since, as established in the third note of the contrast ratio definition, the
default ("unspecified") color is assumed to be white.
Under each guideline, there are success criteria that describe specifically what must be achieved in order to conform to this standard. They are similar to the "checkpoints" in WCAG 1.0. Each success criterion is written as a statement that will be either true or false when specific Web content is tested against it. The success criteria are written to be technology neutral.
+
Under each guideline, there are success criteria that describe specifically what must be achieved in order to conform to this standard. They are similar to the "checkpoints" in WCAG 1.0. Each success criterion is written as a statement that will be either true or false when specific Web content is tested against it. The success criteria are written to be technology neutral.
All WCAG 2 success criteria are written as testable criteria for objectively determining if content satisfies the success criteria. While some of the testing can be automated using software evaluation programs, others require human testers for part or all of the test.
Although content may satisfy the success criteria, the content may not always be usable by people with a wide variety of disabilities. Professional reviews utilizing recognized qualitative heuristics are important in achieving accessibility for some audiences. In addition, usability testing is recommended. Usability testing aims to determine how well people can use the content for its intended purpose.
The content should be tested by those who understand how people with different types of disabilities use the Web. It is recommended that users with disabilities be included in test groups when performing human testing.
From deb91ea8f8a9cf9fb86bbd0f537d76a0a35fa590 Mon Sep 17 00:00:00 2001
From: Hidde de Vries <160571138+hidde-logius@users.noreply.github.com>
Date: Thu, 25 Jul 2024 02:30:27 +0200
Subject: [PATCH 18/25] Update broken link, remove double link in F47 (#3701)
MDN no longer has a page on the blink element, replacing it with a link
to Wikipedia for historical reference.
The second link went to the same place, also removed that as it was
redundant.
---
techniques/failures/F47.html | 7 ++-----
1 file changed, 2 insertions(+), 5 deletions(-)
diff --git a/techniques/failures/F47.html b/techniques/failures/F47.html
index e6f50b0be3..f65fd7e5a6 100644
--- a/techniques/failures/F47.html
+++ b/techniques/failures/F47.html
@@ -25,12 +25,9 @@