Skip to content

Commit

Permalink
Merge branch 'main' into 4109-1.3.1FailureRemoval
Browse files Browse the repository at this point in the history
  • Loading branch information
mbgower authored Jan 22, 2025
2 parents a9db929 + 2afa6a8 commit fe5256f
Show file tree
Hide file tree
Showing 127 changed files with 1,915 additions and 953 deletions.
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
17 changes: 10 additions & 7 deletions .github/workflows/11ty-publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ jobs:
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
uses: stefanzweifel/git-auto-commit-action@v5
with:
repository: _site
branch: gh-pages
commit_user_name: w3cgruntbot
commit_user_email: 87540780+w3cgruntbot@users.noreply.github.com
commit_author: "w3cgruntbot <87540780+w3cgruntbot@users.noreply.github.com>"
commit_message: ":robot: Deploy to GitHub Pages: ${{ github.sha }} from branch ${{ github.ref }}"
skip_fetch: true
skip_checkout: true
156 changes: 128 additions & 28 deletions 11ty/CustomLiquid.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import type { Cheerio, Element } from "cheerio";
import { Liquid, type Template } from "liquidjs";
import type { RenderOptions } from "liquidjs/dist/liquid-options";
import type { LiquidOptions, 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 { biblioPattern, getBiblio, getXmlBiblio } from "./biblio";
import { flattenDom, load, type CheerioAnyNode } from "./cheerio";
import { generateId } from "./common";
import { getTermsMap } from "./guidelines";
import { getAcknowledgementsForVersion, type TermsMap } from "./guidelines";
import { resolveTechniqueIdFromHref, understandingToTechniqueLinkSelector } from "./techniques";
import { techniqueToUnderstandingLinkSelector } from "./understanding";

Expand All @@ -21,13 +21,23 @@ const indexPattern = /(techniques|understanding)\/(index|about)\.html$/;
const techniquesPattern = /\btechniques\//;
const understandingPattern = /\bunderstanding\//;

const termsMap = await getTermsMap();
const biblio = await getBiblio();
const xmlBiblio = await getXmlBiblio();
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`;

/** Version of generateIncludes for a single include with parameters */
const generateIncludeWithParams = (basename: string, params: Record<string, string>) => {
const strParams = Object.entries(params).reduce(
(str, [key, value]) => `${str}, ${key}: ${JSON.stringify(value)}`,
""
);
return `\n{% include "${basename}.html"${strParams} %}\n`;
};

/**
* Determines whether a given string is actually HTML,
* not e.g. a data value Eleventy sent to the templating engine.
Expand Down Expand Up @@ -61,7 +71,7 @@ const normalizeTocLabel = (label: string) =>
* expand to a link with the full technique ID and title.
* @param $el a $()-wrapped link element
*/
function expandTechniqueLink($el: Cheerio<Element>) {
function expandTechniqueLink($el: CheerioAnyNode) {
const href = $el.attr("href");
if (!href) throw new Error("expandTechniqueLink: non-link element encountered");
const id = resolveTechniqueIdFromHref(href);
Expand All @@ -71,6 +81,10 @@ function expandTechniqueLink($el: Cheerio<Element>) {

const stripHtmlComments = (html: string) => html.replace(/<!--[\s\S]*?-->/g, "");

interface CustomLiquidOptions extends LiquidOptions {
termsMap: TermsMap;
}

// 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
Expand All @@ -83,13 +97,45 @@ const stripHtmlComments = (html: string) => html.replace(/<!--[\s\S]*?-->/g, "")
* - generating/expanding sections with auto-generated content
*/
export class CustomLiquid extends Liquid {
termsMap: TermsMap;
constructor(options: CustomLiquidOptions) {
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);

if (!isTechniques && !isUnderstanding) return super.parse(html);

const $ = flattenDom(html, filepath);
Expand Down Expand Up @@ -269,8 +315,9 @@ export class CustomLiquid extends Liquid {
// 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"));
// Add key terms and references by default, to be removed in #parse if not needed
$("body").append(generateIncludeWithParams("dl-section", { title: "Key Terms" }));
$("body").append(generateIncludeWithParams("dl-section", { title: "References" }));
}

// Remove h2-level sections with no content other than heading
Expand Down Expand Up @@ -299,13 +346,22 @@ export class CustomLiquid extends Liquid {
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;
if (!isHtmlFileContent(html) || !scope || scope.page.url === false) return html;
if (scope.page.inputPath.includes("errata/")) return this.renderErrata(html);

const $ = load(html);

if (indexPattern.test(scope.page.inputPath)) {
// Remove empty list items due to obsolete technique link removal
if (scope.isTechniques) $("ul.toc-wcag-docs li:empty").remove();

// Replace acknowledgements with pinned content for older versions
if (process.env.WCAG_VERSION && $("section#acknowledgements").length) {
const pinnedAcknowledgements = await getAcknowledgementsForVersion(scope.version);
for (const [id, content] of Object.entries(pinnedAcknowledgements)) {
$(`#${id} h3 +`).html(content);
}
}
} else {
const $title = $("title");

Expand Down Expand Up @@ -399,13 +455,13 @@ export class CustomLiquid extends Liquid {
// 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<Element>) => {
const extractTermName = ($el: CheerioAnyNode) => {
const name = $el
.text()
.toLowerCase()
.trim()
.replace(/\s*\n+\s*/, " ");
const term = termsMap[name];
const term = this.termsMap[name];
if (!term) {
console.warn(`${scope.page.inputPath}: Term not found: ${name}`);
return;
Expand All @@ -419,12 +475,12 @@ export class CustomLiquid extends Liquid {
const $el = $(el);
const termName = extractTermName($el);
$el
.attr("href", `${scope.guidelinesUrl}#${termName ? termsMap[termName].trId : ""}`)
.attr("href", `${scope.guidelinesUrl}#${termName ? this.termsMap[termName].trId : ""}`)
.attr("target", "terms");
});
} else if (scope.isUnderstanding) {
const $termsList = $("section#key-terms dl");
const extractTermNames = ($links: Cheerio<Element>) =>
const extractTermNames = ($links: CheerioAnyNode) =>
compact(uniq($links.toArray().map((el) => extractTermName($(el)))));

if ($termLinks.length) {
Expand All @@ -433,7 +489,7 @@ export class CustomLiquid extends Liquid {
// 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]];
const term = this.termsMap[termNames[i]];
if (!term) continue; // This will already warn via extractTermNames

const $definition = load(term.definition);
Expand All @@ -450,17 +506,22 @@ export class CustomLiquid extends Liquid {
return 0;
});
for (const name of termNames) {
const term = termsMap[name]; // Already verified existence in the earlier loop
$termsList.append(
`<dt id="${term.id}">${term.name}</dt>` +
`<dd><definition>${term.definition}</definition></dd>`
);
const term = this.termsMap[name]; // Already verified existence in the earlier loop
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(`\n <dt id="${term.id}">${term.name}</dt><dd>${termBody}</dd>`);
}

// 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 : ""}`;
el.attribs.href = `#${name ? this.termsMap[name].id : ""}`;
});
} else {
// No terms: remove skeleton that was placed in #parse
Expand Down Expand Up @@ -494,20 +555,22 @@ export class CustomLiquid extends Liquid {
// (This is also needed for techniques/about)
$("div.note").each((_, el) => {
const $el = $(el);
$el.replaceWith(`<div class="note">
const classes = el.attribs.class;
$el.replaceWith(`<div class="${classes}">
<p class="note-title marker">Note</p>
<div>${$el.html()}</div>
</div>`);
});
// Handle p variant after div (the reverse would double-process)
$("p.note").each((_, el) => {
const $el = $(el);
$el.replaceWith(`<div class="note">
const classes = el.attribs.class;
$el.replaceWith(`<div class="${classes}">
<p class="note-title marker">Note</p>
<p>${$el.html()}</p>
</div>`);
});

// Add header to example sections in Key Terms (aside) and Conformance (div)
$("aside.example, div.example").each((_, el) => {
const $el = $(el);
Expand All @@ -520,13 +583,19 @@ export class CustomLiquid extends Liquid {
// 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;
const classMatch = el.attribs.class.match(/\bwcag(\d\d)\b/);
if (!classMatch) throw new Error(`Invalid wcagXY class found: ${el.attribs.class}`);
const classVersion = +classMatch[1];
if (isNaN(classVersion)) throw new Error(`Invalid wcagXY class found: ${el.attribs.class}`);
const buildVersion = +scope.version;

if (classVersion > buildVersion) {
$(el).remove();
} else if (classVersion === buildVersion) {
$(el).prepend(`<span class="new-version">New in WCAG ${scope.versionDecimal}: </span>`);
if (/\bnote\b/.test(el.attribs.class))
$(el).find(".marker").append(` (new in WCAG ${scope.versionDecimal})`);
else
$(el).prepend(`<span class="new-version">New in WCAG ${scope.versionDecimal}: </span>`);
}
// Output as-is if content pertains to a version older than what's being built
});
Expand All @@ -540,6 +609,37 @@ export class CustomLiquid extends Liquid {
});
}

// Link biblio references
if (scope.isUnderstanding) {
const xmlBiblioReferences: string[] = [];
$("p").each((_, el) => {
const $el = $(el);
const html = $el.html();
if (html && biblioPattern.test(html)) {
$el.html(
html.replace(biblioPattern, (substring, code) => {
if (biblio[code]?.href) return `[<a href="${biblio[code].href}">${code}</a>]`;
if (code in xmlBiblio) {
xmlBiblioReferences.push(code);
return `[<a href="#${code}">${code}</a>]`;
}
console.warn(`${scope.page.inputPath}: Unresolved biblio ref: ${code}`);
return substring;
})
);
}
});

// Populate references section, or remove if unused
if (xmlBiblioReferences.length) {
for (const ref of uniq(xmlBiblioReferences).sort()) {
$("section#references dl").append(
`\n <dt id="${ref}">${ref}</dt><dd>${xmlBiblio[ref]}</dd>`
);
}
} else $("section#references").remove();
}

// 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]:not(.obsolete)";
Expand Down
13 changes: 9 additions & 4 deletions 11ty/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,17 @@ but may be useful if you're not seeing what you expect in the output files.

### `WCAG_VERSION`

**Usage context:** currently this should not be changed, pending future improvements to `21` support
**Usage context:** for building older versions of techniques and understanding docs

Indicates WCAG version being built, in `XY` format (i.e. no `.`).
Influences base URLs for links to guidelines, techniques, and understanding pages.

**Default:** `22`
Influences which pages get included, guideline/SC content,
and a few details within pages (e.g. titles/URLs, "New in ..." content).
Also influences base URLs for links to guidelines, techniques, and understanding pages.
Explicitly setting this causes the build to reference guidelines and acknowledgements
published under `w3.org/TR/WCAG{version}`, rather than using the local checkout
(which is effectively the 2.2 Editors' Draft).

Possible values: `22`, `21`

### `WCAG_MODE`

Expand Down
Loading

0 comments on commit fe5256f

Please sign in to comment.