Skip to content

Commit

Permalink
Merge branch 'main' into patrickhlauke-issue3949
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickhlauke authored Dec 13, 2024
2 parents abe30f6 + b9cd498 commit eec0e03
Show file tree
Hide file tree
Showing 310 changed files with 1,192 additions and 3,594 deletions.
19 changes: 11 additions & 8 deletions .github/workflows/11ty-publish.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: CI
name: Push to gh-pages branch

# Reference documentation: https://docs.github.com/en/actions/reference

Expand Down 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 [email protected]
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: [email protected]
commit_author: "w3cgruntbot <[email protected]>"
commit_message: ":robot: Deploy to GitHub Pages: ${{ github.sha }} from branch ${{ github.ref }}"
skip_fetch: true
skip_checkout: true
4 changes: 0 additions & 4 deletions .pr-preview.json

This file was deleted.

82 changes: 61 additions & 21 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 } 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,7 +21,7 @@ const indexPattern = /(techniques|understanding)\/(index|about)\.html$/;
const techniquesPattern = /\btechniques\//;
const understandingPattern = /\bunderstanding\//;

const termsMap = await getTermsMap();
const biblio = await getBiblio();
const termLinkSelector = "a:not([href])";

/** Generates {% include "foo.html" %} directives from 1 or more basenames */
Expand Down Expand Up @@ -61,7 +61,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 +71,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 +87,18 @@ 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;
}
public parse(html: string, filepath?: string) {
// Filter out Liquid calls for computed data and includes themselves
if (filepath && !filepath.includes("_includes/") && 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 @@ -299,13 +308,21 @@ 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;

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 +416,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 +436,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 +450,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,7 +467,7 @@ export class CustomLiquid extends Liquid {
return 0;
});
for (const name of termNames) {
const term = termsMap[name]; // Already verified existence in the earlier loop
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>`
Expand All @@ -460,7 +477,7 @@ export class CustomLiquid extends Liquid {
// 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 +511,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 +539,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 +565,21 @@ export class CustomLiquid extends Liquid {
});
}

// Link biblio references
if (scope.isUnderstanding) {
$("p").each((_, el) => {
const $el = $(el);
const html = $el.html();
if (html && biblioPattern.test(html)) {
$el.html(
html.replace(biblioPattern, (substring, code) =>
biblio[code]?.href ? `[<a href="${biblio[code].href}">${code}</a>]` : substring
)
);
}
});
}

// 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
39 changes: 39 additions & 0 deletions 11ty/biblio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import axios from "axios";
import { readFile } from "fs/promises";
import { glob } from "glob";
import uniq from "lodash-es/uniq";

import { wrapAxiosRequest } from "./common";

export const biblioPattern = /\[\[\??([\w-]+)\]\]/g;

/** Compiles URLs from local biblio + specref for linking in Understanding documents. */
export async function getBiblio() {
const localBiblio = eval(
(await readFile("biblio.js", "utf8"))
.replace(/^respecConfig\.localBiblio\s*=\s*/, "(")
.replace("};", "})")
);

const refs: string[] = [];
for (const path of await glob(["guidelines/**/*.html", "understanding/*/*.html"])) {
const content = await readFile(path, "utf8");
let match;
while ((match = biblioPattern.exec(content))) if (!localBiblio[match[1]]) refs.push(match[1]);
}
const uniqueRefs = uniq(refs);

const response = await wrapAxiosRequest(
axios.get(`https://api.specref.org/bibrefs?refs=${uniqueRefs.join(",")}`)
);
const fullBiblio = {
...response.data,
...localBiblio,
};

const resolvedRefs = Object.keys(fullBiblio);
const unresolvedRefs = uniqueRefs.filter((ref) => !resolvedRefs.includes(ref));
if (unresolvedRefs.length) console.warn(`Unresolved biblio refs: ${unresolvedRefs.join(", ")}`);

return fullBiblio;
}
3 changes: 3 additions & 0 deletions 11ty/cheerio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import { dirname, resolve } from "path";

export { load } from "cheerio";

/** Superset of the type returned by any Cheerio $() call. */
export type CheerioAnyNode = ReturnType<ReturnType<typeof load>>;

/** Convenience function that combines readFile and load. */
export const loadFromFile = async (
inputPath: string,
Expand Down
22 changes: 22 additions & 0 deletions 11ty/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
/** @fileoverview Common functions used by multiple parts of the build process */

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

import type { Guideline, Principle, SuccessCriterion } from "./guidelines";

/** Generates an ID for heading permalinks. Equivalent to wcag:generate-id in base.xslt. */
Expand Down Expand Up @@ -28,3 +30,23 @@ export function wcagSort(
}
return 0;
}

/**
* Handles HTTP error responses from Axios requests in local dev;
* re-throws error during builds to fail loudly.
* This should only be used for non-critical requests that can tolerate null data
* without major side effects.
*/
export const wrapAxiosRequest = <T, D>(promise: Promise<AxiosResponse<T, D>>) =>
promise.catch((error) => {
if (!(error instanceof AxiosError) || !error.response || !error.request) throw error;
const { response, request } = error;
console.warn(
`AxiosError: status ${response.status} received from ${
request.protocol + "//" + request.host
}${request.path || ""}`
);

if (process.env.ELEVENTY_RUN_MODE === "build") throw error;
else return { data: null };
});
Loading

0 comments on commit eec0e03

Please sign in to comment.