Skip to content

Commit 43b7ca3

Browse files
authored
Re-implement wcag.json output (#4301)
- Re-implements `wcag.json` generation using TypeScript (reusing code already involved in the Eleventy build process where possible) - Always generates based on published guidelines (not ED); also relies on built Understanding docs (i.e. it needs to run after Eleventy) - Can generate for both 2.1 and 2.2 (since multiple consumers rely on parts of both) - Adds a `WCAG_JSON` environment variable recognized by the Eleventy build process to also build `wcag.json` at the end - Updates the `w3c-publish` scripts to include the new environment variable - Outputs `wcag.json` under `_site` (the build output folder), since it is an output of the build process; there may be some discussion as to the precise behavior of this - Updates behavior of some fields in the JSON to be easier to work with by their respective consumers, e.g.: - Any fields containing HTML have links modified to point to absolute URLs - `techniques` is replaced by `techniquesHtml`, as Quickref's version of the former had been manually modified to the point where it would be unrealistic to automate, while the latter can be output as-is - Removes old XML build targets and XSLT code that this supersedes - Removes wcag.json from the guidelines folder, as it is now part of build output intended to be published
1 parent 9fbbdb3 commit 43b7ca3

13 files changed

+472
-9226
lines changed

Diff for: 11ty/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ Possible values:
9292
- `editors` - Sets base URLs appropriate for `gh-pages` publishing; used by deploy action
9393
- `publication` - Sets base URLs appropriate for WAI site publishing; used by `publish-w3c` script
9494

95+
### `WCAG_JSON`
96+
97+
Generates `_site/wcag.json`. (This is not done by default, as it adds to build time.)
98+
99+
**Default:** Unset by default; `publish-w3c` scripts set this to a non-empty value.
100+
95101
### `GITHUB_REPOSITORY`
96102

97103
**Usage context:** Automatically set during GitHub workflows; should not need to be set manually

Diff for: 11ty/cheerio.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
import { load, type CheerioOptions } from "cheerio";
1+
import { load, type CheerioAPI, type CheerioOptions } from "cheerio";
22
import { readFileSync } from "fs";
33
import { readFile } from "fs/promises";
44
import { dirname, resolve } from "path";
55

66
export { load } from "cheerio";
77

8-
/** Superset of the type returned by any Cheerio $() call. */
9-
export type CheerioAnyNode = ReturnType<ReturnType<typeof load>>;
8+
/** Superset of the type returned by any Cheerio $() call */
9+
export type CheerioAnyNode = ReturnType<CheerioAPI>;
10+
/** Type returned by e.g. $(...).find() */
11+
export type CheerioElement = ReturnType<CheerioAnyNode["find"]>;
1012

1113
/** Convenience function that combines readFile and load. */
1214
export const loadFromFile = async (

Diff for: 11ty/common.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import { AxiosError, type AxiosResponse } from "axios";
44

5-
import type { Guideline, Principle, SuccessCriterion } from "./guidelines";
5+
import type { WcagItem } from "./guidelines";
66

77
/** Generates an ID for heading permalinks. Equivalent to wcag:generate-id in base.xslt. */
88
export function generateId(title: string) {
@@ -18,8 +18,8 @@ export const resolveDecimalVersion = (version: `${number}`) => version.split("")
1818

1919
/** Sort function for ordering WCAG principle/guideline/SC numbers ascending */
2020
export function wcagSort(
21-
a: Principle | Guideline | SuccessCriterion,
22-
b: Principle | Guideline | SuccessCriterion
21+
a: WcagItem,
22+
b: WcagItem
2323
) {
2424
const aParts = a.num.split(".").map((n) => +n);
2525
const bParts = b.num.split(".").map((n) => +n);

Diff for: 11ty/cp-cvs.ts

+4
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ for (const [srcDir, destDir] of Object.entries(dirs)) {
5252
}
5353
}
5454

55+
try {
56+
await copyFile(join(outputBase, "wcag.json"), join(wcagBase, "wcag.json"));
57+
} catch (error) {}
58+
5559
await mkdirp(join(wcagBase, "errata"));
5660
await copyFile(
5761
join(outputBase, "errata", `${wcagVersion}.html`),

Diff for: 11ty/guidelines.ts

+55-25
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from "axios";
22
import type { CheerioAPI } from "cheerio";
33
import { glob } from "glob";
4+
import pick from "lodash-es/pick";
45

56
import { readFile } from "fs/promises";
67
import { basename, join } from "path";
@@ -34,22 +35,28 @@ export const actRules = (
3435
JSON.parse(await readFile("guidelines/act-mapping.json", "utf8")) as ActMapping
3536
)["act-rules"];
3637

38+
/** Version-dependent overrides of SC shortcodes for older versions */
39+
export const scSlugOverrides: Record<string, (version: WcagVersion) => string> = {
40+
"target-size-enhanced": (version) => (version < "22" ? "target-size" : "target-size-enhanced"),
41+
};
42+
3743
/**
3844
* Flattened object hash, mapping each WCAG 2 SC slug to the earliest WCAG version it applies to.
3945
* (Functionally equivalent to "guidelines-versions" target in build.xml; structurally inverted)
4046
*/
41-
const scVersions = await (async function () {
47+
async function resolveScVersions(version: WcagVersion) {
4248
const paths = await glob("*/*.html", { cwd: "understanding" });
4349
const map: Record<string, WcagVersion> = {};
4450

4551
for (const path of paths) {
4652
const [fileVersion, filename] = path.split("/");
4753
assertIsWcagVersion(fileVersion);
48-
map[basename(filename, ".html")] = fileVersion;
54+
const slug = basename(filename, ".html");
55+
map[slug in scSlugOverrides ? scSlugOverrides[slug](version) : slug] = fileVersion;
4956
}
5057

5158
return map;
52-
})();
59+
}
5360

5461
export interface DocNode {
5562
id: string;
@@ -83,15 +90,12 @@ export interface SuccessCriterion extends DocNode {
8390
type: "SC";
8491
}
8592

93+
export type WcagItem = Principle | Guideline | SuccessCriterion;
94+
8695
export function isSuccessCriterion(criterion: any): criterion is SuccessCriterion {
8796
return !!(criterion?.type === "SC" && "level" in criterion);
8897
}
8998

90-
/** Version-dependent overrides of SC shortcodes for older versions */
91-
export const scSlugOverrides: Record<string, (version: WcagVersion) => string> = {
92-
"target-size-enhanced": (version) => (version < "22" ? "target-size" : "target-size-enhanced"),
93-
};
94-
9599
/** Selectors ignored when capturing content of each Principle / Guideline / SC */
96100
const contentIgnores = [
97101
"h1, h2, h3, h4, h5, h6",
@@ -115,7 +119,12 @@ const getContentHtml = ($el: CheerioAnyNode) => {
115119
};
116120

117121
/** Performs processing common across WCAG versions */
118-
function processPrinciples($: CheerioAPI) {
122+
async function processPrinciples($: CheerioAPI) {
123+
// Auto-detect version from end of title
124+
const version = $("title").text().trim().split(" ").pop()!.replace(".", "");
125+
assertIsWcagVersion(version);
126+
const scVersions = await resolveScVersions(version);
127+
119128
const principles: Principle[] = [];
120129
$(".principle").each((i, el) => {
121130
const guidelines: Guideline[] = [];
@@ -175,7 +184,7 @@ export const getPrinciples = async (path = "guidelines/index.html") =>
175184
* Returns a flattened object hash, mapping shortcodes to each principle/guideline/SC.
176185
*/
177186
export function getFlatGuidelines(principles: Principle[]) {
178-
const map: Record<string, Principle | Guideline | SuccessCriterion> = {};
187+
const map: Record<string, WcagItem> = {};
179188
for (const principle of principles) {
180189
map[principle.id] = principle;
181190
for (const guideline of principle.guidelines) {
@@ -199,7 +208,7 @@ interface Term {
199208
}
200209
export type TermsMap = Record<string, Term>;
201210

202-
function processTermsMap($: CheerioAPI) {
211+
function processTermsMap($: CheerioAPI, includeSynonyms = true) {
203212
const terms: TermsMap = {};
204213

205214
$("dfn").each((_, el) => {
@@ -213,12 +222,16 @@ function processTermsMap($: CheerioAPI) {
213222
trId: el.attribs.id,
214223
};
215224

216-
// Include both original and all-lowercase version to simplify lookups
217-
// (since most synonyms are lowercase) while preserving case in name
218-
const names = [term.name, term.name.toLowerCase()].concat(
219-
(el.attribs["data-lt"] || "").toLowerCase().split("|")
220-
);
221-
for (const name of names) terms[name] = term;
225+
if (includeSynonyms) {
226+
// Include both original and all-lowercase version to simplify lookups
227+
// (since most synonyms are lowercase) while preserving case in name
228+
const names = [term.name, term.name.toLowerCase()].concat(
229+
(el.attribs["data-lt"] || "").toLowerCase().split("|")
230+
);
231+
for (const name of names) terms[name] = term;
232+
} else {
233+
terms[term.name] = term;
234+
}
222235
});
223236

224237
return terms;
@@ -230,7 +243,7 @@ function processTermsMap($: CheerioAPI) {
230243
* comparable to the term elements in wcag.xml from the guidelines-xml Ant task.
231244
*/
232245
export const getTermsMap = async (path = "guidelines/index.html") =>
233-
processTermsMap(await flattenDomFromFile(path));
246+
processTermsMap(await flattenDomFromFile(path), true);
234247

235248
// Version-specific APIs
236249

@@ -245,6 +258,11 @@ const loadRemoteGuidelines = async (version: WcagVersion, stripRespec = true) =>
245258
).data);
246259

247260
const $ = load(html);
261+
262+
// Remove extra markup from headings, regardless of stripRespec setting,
263+
// so that names parse consistently
264+
$("bdi").remove();
265+
248266
if (!stripRespec) return $;
249267

250268
// Re-collapse definition links and notes, to be processed by this build system
@@ -271,9 +289,6 @@ const loadRemoteGuidelines = async (version: WcagVersion, stripRespec = true) =>
271289
$(".example[id]").removeAttr("id");
272290
$(".example > .marker").remove();
273291

274-
// Remove extra markup from headings so they can be parsed for names
275-
$("bdi").remove();
276-
277292
// Remove abbr elements which exist only in TR, not in informative docs
278293
$("#acknowledgements li abbr, #glossary abbr").each((_, abbrEl) => {
279294
$(abbrEl).replaceWith($(abbrEl).text());
@@ -300,15 +315,30 @@ export const getAcknowledgementsForVersion = async (version: WcagVersion) => {
300315
/**
301316
* Retrieves and processes a pinned WCAG version using published guidelines.
302317
*/
303-
export const getPrinciplesForVersion = async (version: WcagVersion) =>
304-
processPrinciples(await loadRemoteGuidelines(version));
318+
export const getPrinciplesForVersion = async (version: WcagVersion, stripRespec?: boolean) =>
319+
processPrinciples(await loadRemoteGuidelines(version, stripRespec));
320+
321+
interface GetTermsMapForVersionOptions {
322+
/**
323+
* Whether to populate additional map keys based on synonyms defined in data-lt;
324+
* this should typically be true for informative docs, but may be false for other purposes
325+
*/
326+
includeSynonyms?: boolean;
327+
/**
328+
* Whether to strip respec-generated content and attributes;
329+
* this should typically be true for informative docs, but may be false for other purposes
330+
*/
331+
stripRespec?: boolean;
332+
}
305333

306334
/**
307335
* Resolves term definitions from a WCAG 2.x publication,
308336
* organized for lookup by name.
309337
*/
310-
export const getTermsMapForVersion = async (version: WcagVersion) =>
311-
processTermsMap(await loadRemoteGuidelines(version));
338+
export const getTermsMapForVersion = async (
339+
version: WcagVersion,
340+
{ includeSynonyms, stripRespec }: GetTermsMapForVersionOptions = {}
341+
) => processTermsMap(await loadRemoteGuidelines(version, stripRespec), includeSynonyms);
312342

313343
/** Parses errata items from the errata document for the specified WCAG version. */
314344
export const getErrataForVersion = async (version: WcagVersion) => {

0 commit comments

Comments
 (0)