Skip to content

Commit 05fd019

Browse files
authored
Add errata to w3c/wcag repo and cross-reference from informative docs (#4170)
This adds the following: - Imports the 2.1 and 2.2 errata pages into this repo - Reorganizes existing 2.1 errata in reverse-chronological order - Adds date stamps for all existing errata I could trace back to a commit - Makes use of variables and Liquid expressions to minimize potential for copy-paste errors between sections - Auto-generates table of contents within `CustomLiquid.ts` to avoid desync when adding versions - Documents patterns/formats for authoring errata in `errata/README.md` (with mention in top-level README) - Adds logic to parse information from errata pages and include relevant errata within Guideline/SC boxes and term definitions within informative docs pages - Errata are only included when building for a specific version (i.e. not the editor's draft, in which case corrections are expected to already be inlined) - Only errata against the latest published version are included in informative docs - Updates `publish-w3c` script to also copy relevant errata file for version being published - Adds links to errata pages in top-level index used for dev server and PR builds
1 parent b034ff5 commit 05fd019

File tree

11 files changed

+555
-53
lines changed

11 files changed

+555
-53
lines changed

.eleventyignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*.md
2+
**/README.md
23
11ty/
34
acknowledgements.html
45
acknowledgements/

11ty/CustomLiquid.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,9 +92,36 @@ export class CustomLiquid extends Liquid {
9292
super(options);
9393
this.termsMap = options.termsMap;
9494
}
95+
96+
private renderErrata(html: string) {
97+
const $ = load(html);
98+
99+
const $tocList = $("#contents .toc");
100+
let $childList: CheerioAnyNode | null = null;
101+
$("main section[id]:has(h2:first-child, h3:first-child)").each((_, el) => {
102+
const $el = $(el);
103+
// Only one of the following queries will match for each section
104+
$el.find("> h2:first-child").each((_, h2El) => {
105+
$childList = null;
106+
$tocList.append(`<li><a href="#${el.attribs.id}">${$(h2El).text()}</a></li>`);
107+
});
108+
$el.find("> h3:first-child").each((_, h3El) => {
109+
if (!$childList) $childList = $(`<ol class="toc"></ol>`).appendTo($tocList);
110+
$childList.append(`<li><a href="#${el.attribs.id}">${$(h3El).text()}</a></li>`);
111+
});
112+
});
113+
114+
return $.html();
115+
}
116+
95117
public parse(html: string, filepath?: string) {
96118
// Filter out Liquid calls for computed data and includes themselves
97-
if (filepath && !filepath.includes("_includes/") && isHtmlFileContent(html)) {
119+
if (
120+
filepath &&
121+
!filepath.includes("_includes/") &&
122+
!filepath.includes("errata/") &&
123+
isHtmlFileContent(html)
124+
) {
98125
const isIndex = indexPattern.test(filepath);
99126
const isTechniques = techniquesPattern.test(filepath);
100127
const isUnderstanding = understandingPattern.test(filepath);
@@ -309,6 +336,7 @@ export class CustomLiquid extends Liquid {
309336
// html contains markup after Liquid tags/includes have been processed
310337
const html = (await super.render(templates, scope, options)).toString();
311338
if (!isHtmlFileContent(html) || !scope || scope.page.url === false) return html;
339+
if (scope.page.inputPath.includes("errata/")) return this.renderErrata(html);
312340

313341
const $ = load(html);
314342

@@ -468,10 +496,15 @@ export class CustomLiquid extends Liquid {
468496
});
469497
for (const name of termNames) {
470498
const term = this.termsMap[name]; // Already verified existence in the earlier loop
471-
$termsList.append(
472-
`<dt id="${term.id}">${term.name}</dt>` +
473-
`<dd><definition>${term.definition}</definition></dd>`
474-
);
499+
let termBody = term.definition;
500+
if (scope.errata[term.id]) {
501+
termBody += `
502+
<p><strong>Errata:</strong></p>
503+
<ul>${scope.errata[term.id].map((erratum) => `<li>${erratum}</li>`)}</ul>
504+
<p><a href="https://www.w3.org/WAI/WCAG${scope.version}/errata/">View all errata</a></p>
505+
`;
506+
}
507+
$termsList.append(`<dt id="${term.id}">${term.name}</dt><dd>${termBody}</dd>`);
475508
}
476509

477510
// Iterate over non-href links once more in now-expanded document to add hrefs

11ty/cp-cvs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,9 @@ for (const [srcDir, destDir] of Object.entries(dirs)) {
5151
await copyFile(srcPath, destPath);
5252
}
5353
}
54+
55+
await mkdirp(join(wcagBase, "errata"));
56+
await copyFile(
57+
join(outputBase, "errata", `${wcagVersion}.html`),
58+
join(wcagBase, "errata", "Overview.html")
59+
);

11ty/guidelines.ts

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { CheerioAPI } from "cheerio";
33
import { glob } from "glob";
44

55
import { readFile } from "fs/promises";
6-
import { basename } from "path";
6+
import { basename, join } from "path";
77

8-
import { flattenDomFromFile, load, type CheerioAnyNode } from "./cheerio";
8+
import { flattenDomFromFile, load, loadFromFile, type CheerioAnyNode } from "./cheerio";
99
import { generateId } from "./common";
1010

1111
export type WcagVersion = "20" | "21" | "22";
@@ -233,50 +233,52 @@ export async function getTermsMap(version?: WcagVersion) {
233233

234234
// Version-specific APIs
235235

236-
const remoteGuidelines$: Partial<Record<WcagVersion, CheerioAPI>> = {};
236+
const guidelinesCache: Partial<Record<WcagVersion, string>> = {};
237237

238238
/** Loads guidelines from TR space for specific version, caching for future calls. */
239-
const loadRemoteGuidelines = async (version: WcagVersion) => {
240-
if (!remoteGuidelines$[version]) {
241-
const $ = load(
242-
(await axios.get(`https://www.w3.org/TR/WCAG${version}/`, { responseType: "text" })).data
243-
);
244-
245-
// Re-collapse definition links and notes, to be processed by this build system
246-
$("a.internalDFN").removeAttr("class data-link-type id href title");
247-
$("[role='note'] .marker").remove();
248-
$("[role='note']").find("> div, > p").addClass("note").unwrap();
249-
250-
// Convert data-plurals (present in publications) to data-lt
251-
$("dfn[data-plurals]").each((_, el) => {
252-
el.attribs["data-lt"] = (el.attribs["data-lt"] || "")
253-
.split("|")
254-
.concat(el.attribs["data-plurals"].split("|"))
255-
.join("|");
256-
delete el.attribs["data-plurals"];
257-
});
239+
const loadRemoteGuidelines = async (version: WcagVersion, stripRespec = true) => {
240+
const html =
241+
guidelinesCache[version] ||
242+
(guidelinesCache[version] = (
243+
await axios.get(`https://www.w3.org/TR/WCAG${version}/`, { responseType: "text" })
244+
).data);
245+
246+
const $ = load(html);
247+
if (!stripRespec) return $;
248+
249+
// Re-collapse definition links and notes, to be processed by this build system
250+
$("a.internalDFN").removeAttr("class data-link-type id href title");
251+
$("[role='note'] .marker").remove();
252+
$("[role='note']").find("> div, > p").addClass("note").unwrap();
253+
254+
// Convert data-plurals (present in publications) to data-lt
255+
$("dfn[data-plurals]").each((_, el) => {
256+
el.attribs["data-lt"] = (el.attribs["data-lt"] || "")
257+
.split("|")
258+
.concat(el.attribs["data-plurals"].split("|"))
259+
.join("|");
260+
delete el.attribs["data-plurals"];
261+
});
258262

259-
// Un-process bibliography references, to be processed by CustomLiquid
260-
$("cite:has(a.bibref:only-child)").each((_, el) => {
261-
const $el = $(el);
262-
$el.replaceWith(`[${$el.find("a.bibref").html()}]`);
263-
});
263+
// Un-process bibliography references, to be processed by CustomLiquid
264+
$("cite:has(a.bibref:only-child)").each((_, el) => {
265+
const $el = $(el);
266+
$el.replaceWith(`[${$el.find("a.bibref").html()}]`);
267+
});
264268

265-
// Remove generated IDs and markers from examples
266-
$(".example[id]").removeAttr("id");
267-
$(".example > .marker").remove();
269+
// Remove generated IDs and markers from examples
270+
$(".example[id]").removeAttr("id");
271+
$(".example > .marker").remove();
268272

269-
// Remove extra markup from headings so they can be parsed for names
270-
$("bdi").remove();
273+
// Remove extra markup from headings so they can be parsed for names
274+
$("bdi").remove();
271275

272-
// Remove abbr elements which exist only in TR, not in informative docs
273-
$("#acknowledgements li abbr, #glossary abbr").each((_, abbrEl) => {
274-
$(abbrEl).replaceWith($(abbrEl).text());
275-
});
276+
// Remove abbr elements which exist only in TR, not in informative docs
277+
$("#acknowledgements li abbr, #glossary abbr").each((_, abbrEl) => {
278+
$(abbrEl).replaceWith($(abbrEl).text());
279+
});
276280

277-
remoteGuidelines$[version] = $;
278-
}
279-
return remoteGuidelines$[version]!;
281+
return $;
280282
};
281283

282284
/**
@@ -299,3 +301,45 @@ export const getAcknowledgementsForVersion = async (version: WcagVersion) => {
299301
*/
300302
export const getPrinciplesForVersion = async (version: WcagVersion) =>
301303
processPrinciples(await loadRemoteGuidelines(version));
304+
305+
/** Parses errata items from the errata document for the specified WCAG version. */
306+
export const getErrataForVersion = async (version: WcagVersion) => {
307+
const $ = await loadFromFile(join("errata", `${version}.html`));
308+
const $guidelines = await loadRemoteGuidelines(version, false);
309+
const aSelector = `a[href*='}}#']:first-of-type`;
310+
const errata: Record<string, string[]> = {};
311+
312+
$("main > section[id]")
313+
.first()
314+
.find(`li:has(${aSelector})`)
315+
.each((_, el) => {
316+
const $el = $(el);
317+
const erratumHtml = $el
318+
.html()!
319+
// Remove everything before and including the final TR link
320+
.replace(/^[\s\S]*href="\{\{\s*\w+\s*\}\}#[\s\S]*?<\/a>,?\s*/, "")
321+
// Remove parenthetical github references (still in Liquid syntax)
322+
.replace(/\(\{%.*%\}\)\s*$/, "")
323+
.replace(/^(\w)/, (_, p1) => p1.toUpperCase());
324+
325+
$el.find(aSelector).each((_, aEl) => {
326+
const $aEl = $(aEl);
327+
let hash: string | undefined = $aEl.attr("href")!.replace(/^.*#/, "");
328+
329+
// Check whether hash pertains to a guideline/SC section or term definition;
330+
// if it doesn't, attempt to resolve it to one
331+
const $hashEl = $guidelines(`#${hash}`);
332+
if (!$hashEl.is("section.guideline, #terms dfn")) {
333+
const $closest = $hashEl.closest("#terms dd, section.guideline");
334+
if ($closest.is("#terms dd")) hash = $closest.prev().find("dfn[id]").attr("id");
335+
else hash = $closest.attr("id");
336+
}
337+
if (!hash) return;
338+
339+
if (hash in errata) errata[hash].push(erratumHtml);
340+
else errata[hash] = [erratumHtml];
341+
});
342+
});
343+
344+
return errata;
345+
};

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,14 @@ To create a working example:
240240
* Reference working examples from techniques using the rawgit URI to the example in its development branch, e.g., `https://rawgit.com/w3c/wcag/main/working-examples/alt-attribute/`. Editors will update links when examples are approved.
241241
* When the example is complete and functional, submit a pull request into the main branch.
242242

243+
## Errata
244+
245+
The errata documents for WCAG 2.1 and 2.2 are now maintained in this repository.
246+
See the [Errata README](errata/README.md) for authoring details.
247+
248+
**Note:** The errata for both versions are maintained on the `main` branch for use in builds.
249+
Direct edits to the guidelines for WCAG 2.1 must be performed under `guidelines/` on the `WCAG-2.1` branch.
250+
243251
## Translations
244252

245253
WCAG 2.2 is ready for translation. To translate WCAG 2.2, follow instructions at [How to Translate WCAG 2](https://www.w3.org/WAI/about/translating/wcag/).

_includes/understanding/about.html

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
1-
{%- if guideline.type == "SC" -%}
2-
{% sectionbox "success-criterion" "Success Criterion (SC)" -%}
3-
{{ guideline.content }}
4-
{%- endsectionbox %}
5-
{%- elsif guideline.type == "Guideline" -%}
6-
{% sectionbox "guideline" "Guideline" -%}
7-
{{ guideline.content }}
8-
{%- endsectionbox %}
9-
{%- endif -%}
1+
{%- capture section_id -%}
2+
{%- if guideline.type == "SC" -%}success-criterion{%- else -%}guideline{%- endif -%}
3+
{%- endcapture -%}
4+
{%- capture section_title -%}
5+
{%- if guideline.type == "SC" -%}Success Criterion (SC){%- else -%}Guideline{%- endif -%}
6+
{%- endcapture -%}
7+
{% sectionbox section_id section_title -%}
8+
{{ guideline.content }}
9+
{%- if errata[guideline.id] %}
10+
<h3>Errata</h3>
11+
<ul>
12+
{%- for erratum in errata[guideline.id] %}
13+
<li>{{ erratum }}</li>
14+
{%- endfor -%}
15+
</ul>
16+
<p><a href="https://www.w3.org/WAI/WCAG{{ version }}/errata/">View all errata</a></p>
17+
{% endif -%}
18+
{%- endsectionbox %}

eleventy.config.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { resolveDecimalVersion } from "11ty/common";
1111
import {
1212
actRules,
1313
assertIsWcagVersion,
14+
getErrataForVersion,
1415
getFlatGuidelines,
1516
getPrinciples,
1617
getPrinciplesForVersion,
@@ -110,6 +111,7 @@ const termsMap = process.env.WCAG_VERSION ? await getTermsMap(version) : await g
110111
const globalData = {
111112
version,
112113
versionDecimal: resolveDecimalVersion(version),
114+
errata: process.env.WCAG_VERSION ? await getErrataForVersion(version) : {},
113115
techniques, // Used for techniques/index.html
114116
technologies, // Used for techniques/index.html
115117
technologyTitles, // Used for techniques/index.html
@@ -277,6 +279,7 @@ export default function (eleventyConfig: any) {
277279
root: ["_includes", "."],
278280
jsTruthy: true,
279281
strictFilters: true,
282+
timezoneOffset: 0, // Avoid off-by-one YYYY-MM-DD date stamp conversions
280283
termsMap,
281284
})
282285
);
@@ -382,6 +385,19 @@ export default function (eleventyConfig: any) {
382385
}
383386
);
384387

388+
// Renders a link to a GitHub commit or pull request
389+
eleventyConfig.addShortcode("gh", (id: string) => {
390+
if (/^#\d+$/.test(id)) {
391+
const num = id.slice(1);
392+
return `<a href="https://github.com/${GH_ORG}/${GH_REPO}/pull/${num}" aria-label="pull request ${num}">${id}</a>`
393+
}
394+
else if (/^[0-9a-f]{7,}$/.test(id)) {
395+
const sha = id.slice(0, 7); // Truncate in case full SHA was passed
396+
return `<a href="https://github.com/${GH_ORG}/${GH_REPO}/commit/${sha}" aria-label="commit ${sha}">${sha}</a>`
397+
}
398+
else throw new Error(`Invalid SHA or PR ID passed to gh tag: ${id}`);
399+
});
400+
385401
// Renders a section box (used for About this Technique and Guideline / SC)
386402
eleventyConfig.addPairedShortcode(
387403
"sectionbox",

0 commit comments

Comments
 (0)