Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add errata to w3c/wcag repo and cross-reference from informative docs #4170

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eleventyignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
*.md
**/README.md
11ty/
acknowledgements.html
acknowledgements/
Expand Down
43 changes: 38 additions & 5 deletions 11ty/CustomLiquid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,36 @@ export class CustomLiquid extends Liquid {
super(options);
this.termsMap = options.termsMap;
}

private renderErrata(html: string) {
const $ = load(html);

const $tocList = $("#contents .toc");
let $childList: CheerioAnyNode | null = null;
$("main section[id]:has(h2:first-child, h3:first-child)").each((_, el) => {
const $el = $(el);
// Only one of the following queries will match for each section
$el.find("> h2:first-child").each((_, h2El) => {
$childList = null;
$tocList.append(`<li><a href="#${el.attribs.id}">${$(h2El).text()}</a></li>`);
});
$el.find("> h3:first-child").each((_, h3El) => {
if (!$childList) $childList = $(`<ol class="toc"></ol>`).appendTo($tocList);
$childList.append(`<li><a href="#${el.attribs.id}">${$(h3El).text()}</a></li>`);
});
});

return $.html();
}

public parse(html: string, filepath?: string) {
// Filter out Liquid calls for computed data and includes themselves
if (filepath && !filepath.includes("_includes/") && isHtmlFileContent(html)) {
if (
filepath &&
!filepath.includes("_includes/") &&
!filepath.includes("errata/") &&
isHtmlFileContent(html)
) {
const isIndex = indexPattern.test(filepath);
const isTechniques = techniquesPattern.test(filepath);
const isUnderstanding = understandingPattern.test(filepath);
Expand Down Expand Up @@ -309,6 +336,7 @@ export class CustomLiquid extends Liquid {
// html contains markup after Liquid tags/includes have been processed
const html = (await super.render(templates, scope, options)).toString();
if (!isHtmlFileContent(html) || !scope || scope.page.url === false) return html;
if (scope.page.inputPath.includes("errata/")) return this.renderErrata(html);

const $ = load(html);

Expand Down Expand Up @@ -468,10 +496,15 @@ export class CustomLiquid extends Liquid {
});
for (const name of termNames) {
const term = this.termsMap[name]; // Already verified existence in the earlier loop
$termsList.append(
`<dt id="${term.id}">${term.name}</dt>` +
`<dd><definition>${term.definition}</definition></dd>`
);
let termBody = term.definition;
if (scope.errata[term.id]) {
termBody += `
<p><strong>Errata:</strong></p>
<ul>${scope.errata[term.id].map((erratum) => `<li>${erratum}</li>`)}</ul>
<p><a href="https://www.w3.org/WAI/WCAG${scope.version}/errata/">View all errata</a></p>
`;
}
$termsList.append(`<dt id="${term.id}">${term.name}</dt><dd>${termBody}</dd>`);
}

// Iterate over non-href links once more in now-expanded document to add hrefs
Expand Down
6 changes: 6 additions & 0 deletions 11ty/cp-cvs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,9 @@ for (const [srcDir, destDir] of Object.entries(dirs)) {
await copyFile(srcPath, destPath);
}
}

await mkdirp(join(wcagBase, "errata"));
await copyFile(
join(outputBase, "errata", `${wcagVersion}.html`),
join(wcagBase, "errata", "Overview.html")
);
106 changes: 75 additions & 31 deletions 11ty/guidelines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import type { CheerioAPI } from "cheerio";
import { glob } from "glob";

import { readFile } from "fs/promises";
import { basename } from "path";
import { basename, join } from "path";

import { flattenDomFromFile, load, type CheerioAnyNode } from "./cheerio";
import { flattenDomFromFile, load, loadFromFile, type CheerioAnyNode } from "./cheerio";
import { generateId } from "./common";

export type WcagVersion = "20" | "21" | "22";
Expand Down Expand Up @@ -233,41 +233,43 @@ export async function getTermsMap(version?: WcagVersion) {

// Version-specific APIs

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

/** Loads guidelines from TR space for specific version, caching for future calls. */
const loadRemoteGuidelines = async (version: WcagVersion) => {
if (!remoteGuidelines$[version]) {
const $ = load(
(await axios.get(`https://www.w3.org/TR/WCAG${version}/`, { responseType: "text" })).data
);

// Re-collapse definition links and notes, to be processed by this build system
$("a.internalDFN").removeAttr("class data-link-type id href title");
$("[role='note'] .marker").remove();
$("[role='note']").find("> div, > p").addClass("note").unwrap();

// Un-process bibliography references, to be processed by CustomLiquid
$("cite:has(a.bibref:only-child)").each((_, el) => {
const $el = $(el);
$el.replaceWith(`[${$el.find("a.bibref").html()}]`);
});
const loadRemoteGuidelines = async (version: WcagVersion, stripRespec = true) => {
const html =
guidelinesCache[version] ||
(guidelinesCache[version] = (
await axios.get(`https://www.w3.org/TR/WCAG${version}/`, { responseType: "text" })
).data);

const $ = load(html);
if (!stripRespec) return $;

// Re-collapse definition links and notes, to be processed by this build system
$("a.internalDFN").removeAttr("class data-link-type id href title");
$("[role='note'] .marker").remove();
$("[role='note']").find("> div, > p").addClass("note").unwrap();

// Un-process bibliography references, to be processed by CustomLiquid
$("cite:has(a.bibref:only-child)").each((_, el) => {
const $el = $(el);
$el.replaceWith(`[${$el.find("a.bibref").html()}]`);
});

// Remove generated IDs and markers from examples
$(".example[id]").removeAttr("id");
$(".example .marker:has(.self-link)").remove();
// Remove generated IDs and markers from examples
$(".example[id]").removeAttr("id");
$(".example .marker:has(.self-link)").remove();

// Remove extra markup from headings so they can be parsed for names
$("bdi").remove();
// Remove extra markup from headings so they can be parsed for names
$("bdi").remove();

// Remove abbr elements which exist only in TR, not in informative docs
$("#acknowledgements li abbr, #glossary abbr").each((_, abbrEl) => {
$(abbrEl).replaceWith($(abbrEl).text());
});
// Remove abbr elements which exist only in TR, not in informative docs
$("#acknowledgements li abbr, #glossary abbr").each((_, abbrEl) => {
$(abbrEl).replaceWith($(abbrEl).text());
});

remoteGuidelines$[version] = $;
}
return remoteGuidelines$[version]!;
return $;
};

/**
Expand All @@ -290,3 +292,45 @@ export const getAcknowledgementsForVersion = async (version: WcagVersion) => {
*/
export const getPrinciplesForVersion = async (version: WcagVersion) =>
processPrinciples(await loadRemoteGuidelines(version));

/** Parses errata items from the errata document for the specified WCAG version. */
export const getErrataForVersion = async (version: WcagVersion) => {
const $ = await loadFromFile(join("errata", `${version}.html`));
const $guidelines = await loadRemoteGuidelines(version, false);
const aSelector = `a[href*='}}#']:first-of-type`;
const errata: Record<string, string[]> = {};

$("main > section[id]")
.first()
.find(`li:has(${aSelector})`)
.each((_, el) => {
const $el = $(el);
const erratumHtml = $el
.html()!
// Remove everything before and including the final TR link
.replace(/^[\s\S]*href="\{\{\s*\w+\s*\}\}#[\s\S]*?<\/a>,?\s*/, "")
// Remove parenthetical github references (still in Liquid syntax)
.replace(/\(\{%.*%\}\)\s*$/, "")
.replace(/^(\w)/, (_, p1) => p1.toUpperCase());

$el.find(aSelector).each((_, aEl) => {
const $aEl = $(aEl);
let hash: string | undefined = $aEl.attr("href")!.replace(/^.*#/, "");

// Check whether hash pertains to a guideline/SC section or term definition;
// if it doesn't, attempt to resolve it to one
const $hashEl = $guidelines(`#${hash}`);
if (!$hashEl.is("section.guideline, #terms dfn")) {
const $closest = $hashEl.closest("#terms dd, section.guideline");
if ($closest.is("#terms dd")) hash = $closest.prev().find("dfn[id]").attr("id");
else hash = $closest.attr("id");
}
if (!hash) return;

if (hash in errata) errata[hash].push(erratumHtml);
else errata[hash] = [erratumHtml];
});
});

return errata;
};
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,14 @@ To create a working example:
* 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.
* When the example is complete and functional, submit a pull request into the main branch.

## Errata

The errata documents for WCAG 2.1 and 2.2 are now maintained in this repository.
See the [Errata README](errata/README.md) for authoring details.

**Note:** The errata for both versions are maintained on the `main` branch for use in builds.
Direct edits to the guidelines for WCAG 2.1 must be performed under `guidelines/` on the `WCAG-2.1` branch.

## Translations

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/).
27 changes: 18 additions & 9 deletions _includes/understanding/about.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
{%- if guideline.type == "SC" -%}
{% sectionbox "success-criterion" "Success Criterion (SC)" -%}
{{ guideline.content }}
{%- endsectionbox %}
{%- elsif guideline.type == "Guideline" -%}
{% sectionbox "guideline" "Guideline" -%}
{{ guideline.content }}
{%- endsectionbox %}
{%- endif -%}
{%- capture section_id -%}
{%- if guideline.type == "SC" -%}success-criterion{%- else -%}guideline{%- endif -%}
{%- endcapture -%}
{%- capture section_title -%}
{%- if guideline.type == "SC" -%}Success Criterion (SC){%- else -%}Guideline{%- endif -%}
{%- endcapture -%}
{% sectionbox section_id section_title -%}
{{ guideline.content }}
{%- if errata[guideline.id] %}
<h3>Errata</h3>
<ul>
{%- for erratum in errata[guideline.id] %}
<li>{{ erratum }}</li>
{%- endfor -%}
</ul>
<p><a href="https://www.w3.org/WAI/WCAG{{ version }}/errata/">View all errata</a></p>
{% endif -%}
{%- endsectionbox %}
16 changes: 16 additions & 0 deletions eleventy.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { resolveDecimalVersion } from "11ty/common";
import {
actRules,
assertIsWcagVersion,
getErrataForVersion,
getFlatGuidelines,
getPrinciples,
getPrinciplesForVersion,
Expand Down Expand Up @@ -110,6 +111,7 @@ const termsMap = process.env.WCAG_VERSION ? await getTermsMap(version) : await g
const globalData = {
version,
versionDecimal: resolveDecimalVersion(version),
errata: process.env.WCAG_VERSION ? await getErrataForVersion(version) : {},
techniques, // Used for techniques/index.html
technologies, // Used for techniques/index.html
technologyTitles, // Used for techniques/index.html
Expand Down Expand Up @@ -277,6 +279,7 @@ export default function (eleventyConfig: any) {
root: ["_includes", "."],
jsTruthy: true,
strictFilters: true,
timezoneOffset: 0, // Avoid off-by-one YYYY-MM-DD date stamp conversions
termsMap,
})
);
Expand Down Expand Up @@ -382,6 +385,19 @@ export default function (eleventyConfig: any) {
}
);

// Renders a link to a GitHub commit or pull request, in parentheses
eleventyConfig.addShortcode("gh", (id: string) => {
if (/^#\d+$/.test(id)) {
const num = id.slice(1);
return `<a href="https://github.com/${GH_ORG}/${GH_REPO}/pull/${num}" aria-label="pull request ${num}">${id}</a>`
}
else if (/^[0-9a-f]{7,}$/.test(id)) {
const sha = id.slice(0, 7); // Truncate in case full SHA was passed
return `<a href="https://github.com/${GH_ORG}/${GH_REPO}/commit/${sha}" aria-label="commit ${sha}">${sha}</a>`
}
else throw new Error(`Invalid SHA or PR ID passed to gh tag: ${id}`);
});

// Renders a section box (used for About this Technique and Guideline / SC)
eleventyConfig.addPairedShortcode(
"sectionbox",
Expand Down
Loading