Skip to content
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"lint": "eslint src/**/*",
"transform-rule": "ts-node src/cli/rule-transform.ts",
"build-examples": "ts-node src/cli/build-examples.ts",
"generate-glossary": "ts-node src/cli/generate-glossary.ts",
"map-implementation": "ts-node src/cli/map-implementation.ts",
"implementations-update": "ts-node src/cli/implementations-update.ts",
"test": "jest",
Expand Down
12 changes: 11 additions & 1 deletion src/build-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,17 @@ export async function buildExamples({
// Copy test assets
if (testAssetsDir) {
const targetDir = path.resolve(assetsPath, "test-assets");
fs.copyFileSync(testAssetsDir, targetDir);

if (
fs.existsSync(testAssetsDir) &&
fs.lstatSync(testAssetsDir).isDirectory()
) {
fs.mkdirSync(targetDir, { recursive: true });
fs.cpSync(testAssetsDir, targetDir, { recursive: true });
} else {
fs.copyFileSync(testAssetsDir, targetDir);
}

console.log(`Copied test assets to ${targetDir}`);
}
}
17 changes: 17 additions & 0 deletions src/cli/__tests__/generate-glossary.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { normalizeHeadingLevels } from "../generate-glossary";

describe("normalizeHeadingLevels", () => {
it("decrements all heading hashes by one level, preserves #", () => {
const input = `#### Item\n##### Subitem\n###### Deep`;
const output = normalizeHeadingLevels(input);

expect(output).toBe(`### Item\n#### Subitem\n##### Deep`);
});

it("preserves leading spaces before headings", () => {
const input = ` #### indented`;
const output = normalizeHeadingLevels(input);

expect(output).toBe(" ### indented");
});
});
186 changes: 186 additions & 0 deletions src/cli/generate-glossary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env ts-node
import fs from "fs";
import path from "path";
import yaml from "js-yaml";
import { Command } from "commander";
import { getRulePages, getDefinitionPages } from "../utils/get-page-data";
import { getRuleDefinitions } from "../act/get-rule-definitions";

interface GlossaryOptions {
rulesDir: string;
glossaryDir: string;
testAssetsDir?: string;
outDir: string;
wcagActRulesDir?: string;
}

const program = new Command();
program
.requiredOption("-r, --rulesDir <dirname>", "Path to _rules directory")
.requiredOption("-g, --glossaryDir <dirname>", "Path to glossary directory")
.option("-t, --testAssetsDir <dirname>", "Path to test-assets directory", "")
.requiredOption("-o, --outDir <dirname>", "Path to output directory")
.option(
"--wcagActRulesDir <dirname>",
"Path to wcag-act-rules checkout directory for config nav injection",
);

function buildUsedInRulesMap(
rulesDir: string,
glossaryDir: string,
testAssetsDir: string,
) {
const rules = getRulePages(rulesDir, testAssetsDir || ".");
const glossary = getDefinitionPages(glossaryDir);

const usedInRules = new Map<string, Set<{ id: string; name: string }>>();
glossary.forEach((definition) => {
usedInRules.set(definition.frontmatter.key, new Set());
});

rules.forEach((rule) => {
const ruleDefinitions = getRuleDefinitions(rule, glossary);
const ruleDefKeys = new Set(
ruleDefinitions.map((def) => def.frontmatter.key),
);

ruleDefKeys.forEach((key) => {
if (!usedInRules.has(key)) return;
usedInRules
.get(key)
?.add({ id: rule.frontmatter.id, name: rule.frontmatter.name });
});
});

return { glossary, usedInRules };
}

export function normalizeHeadingLevels(body: string): string {
return body.replace(/^(\s*)(#{1,6})(\s+)/gm, (_, leading, hashes, space) => {
if (hashes.length <= 1) {
return `${leading}${hashes}${space}`;
}
return `${leading}${"#".repeat(hashes.length - 1)}${space}`;
});
}

function generateGlossaryContent(
glossaryDefinitions: Array<{
frontmatter: { key: string; title: string };
body: string;
}>,
usedInRules: Map<string, Set<{ id: string; name: string }>>,
): string {
const lines: string[] = [];

lines.push("---");
lines.push("layout: standalone_resource");
lines.push('title: "ACT Rules Glossary"');
lines.push("permalink: /standards-guidelines/act/rules/terms/");
lines.push("ref: /standards-guidelines/act/rules/terms/");
lines.push("lang: en");
lines.push('type_of_guidance: ""');
lines.push("feedbackmail: public-wcag-act@w3.org");
lines.push('footer: ""');
lines.push("github:");
lines.push(" repository: w3c/wcag-act-rules");
lines.push(" path: content/terms/glossary.md");
lines.push("---");
lines.push("");
lines.push("{::nomarkdown}");
lines.push('{% include box.html type="start" title="Glossary" class="" %}');
lines.push("{:/}");
lines.push("");

glossaryDefinitions.forEach((def) => {
const key = def.frontmatter.key;
const title = def.frontmatter.title;
const body = normalizeHeadingLevels(def.body.trim());

lines.push(`## ${title} {#${key}}`);
lines.push("");
lines.push(body);
lines.push("");
lines.push("### Used in rules");

const rules = [...(usedInRules.get(key) || new Set())].sort((a, b) =>
a.id.localeCompare(b.id),
);
if (rules.length === 0) {
lines.push("- None");
} else {
rules.forEach((rule) => {
lines.push(
`- [${rule.name}](/standards-guidelines/act/rules/${rule.id}/proposed/)`,
);
});
}

lines.push("");
});

lines.push("{::nomarkdown}");
lines.push('{% include box.html type="end" %}');
lines.push("{:/}");

return lines.join("\n");
}

async function generateFile(options: GlossaryOptions): Promise<void> {
const { glossary, usedInRules } = buildUsedInRulesMap(
options.rulesDir,
options.glossaryDir,
options.testAssetsDir || "",
);

const content = generateGlossaryContent(glossary, usedInRules);
const outputDir = path.join(options.outDir, "content/terms");
const outputFile = path.join(outputDir, "glossary.md");

await fs.promises.mkdir(outputDir, { recursive: true });
await fs.promises.writeFile(outputFile, content, "utf8");
console.log(`Created glossary at ${outputFile}`);

await updateWcagConfigNav(options.outDir);
}

async function updateWcagConfigNav(outputDir: string) {
const configPath = path.join(outputDir, "_config.yml");
const configContent = await fs.promises.readFile(configPath, "utf8");
const configData: any = yaml.load(configContent);

if (!configData?.defaults) return;

const defaultValues = configData.defaults.find(
(item: any) => item?.values?.standalone_resource_nav_links,
);
if (!defaultValues) return;

const navLinks = defaultValues.values.standalone_resource_nav_links;
const hasGlossary = navLinks.some(
(link: any) => link.ref === "/standards-guidelines/act/rules/terms/",
);

if (!hasGlossary) {
navLinks.push({
name: "Glossary",
ref: "/standards-guidelines/act/rules/terms/",
});
await fs.promises.writeFile(configPath, yaml.dump(configData), "utf8");
console.log(
"Updated wcag-act-rules _config.yml to include glossary nav link.",
);
}
}

if (require.main === module) {
program.parse(process.argv);
const options = program.opts<GlossaryOptions>();

generateFile(options)
.then(() => process.exit(0))
.catch((err) => {
console.error(err);
process.exit(1);
});
}
21 changes: 20 additions & 1 deletion src/rule-transform/rule-content/__tests__/get-glossary.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import outdent from "outdent";
import { parsePage } from "../../../utils/parse-page";
import { getGlossary } from "../get-glossary";
import { getGlossary, getFullGlossary } from "../get-glossary";
import { createGlossary } from "../../__test-utils";

describe("rule-content", () => {
Expand Down Expand Up @@ -62,5 +62,24 @@ describe("rule-content", () => {
You can see [it][].
`);
});

it("keeps full source body for the glossary page", () => {
const glossary = createGlossary({ visible });
const fullGlossary = getFullGlossary(glossary);

expect(fullGlossary).toBe(outdent`
## Glossary

### Visible {#visible}

You can see [it][].

### Really

Ignore me

[it]: https://w3.org/
`);
});
});
});
15 changes: 14 additions & 1 deletion src/rule-transform/rule-content/get-glossary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@ export const getGlossary = (_: unknown, glossary: DefinitionPage[]): string => {
return joinStrings(`## Glossary`, ...glossaryTexts);
};

export const getFullGlossary = (glossary: DefinitionPage[]): string => {
const glossaryTexts = glossary.map(getFullGlossaryMarkdown);
return joinStrings(`## Glossary`, ...glossaryTexts);
};

function getGlossaryMarkdown(definition: DefinitionPage): string {
const { title, key } = definition.frontmatter;
const heading = `### ${title} {#${key}}`;
const body = getDefinitionBody(definition);
return joinStrings(heading, body);
}

function getFullGlossaryMarkdown(definition: DefinitionPage): string {
const { title, key } = definition.frontmatter;
const heading = `### ${title} {#${key}}`;
const body = definition.body.trim();
// Keep full source definition body (including all sections after first ##)
return joinStrings(heading, body);
}

function getDefinitionBody(definition: DefinitionPage): string | string[] {
// Delete all lines after the first heading
// References are mixed into the bottom of the rule page later
Expand All @@ -28,7 +41,7 @@ function getDefinitionBody(definition: DefinitionPage): string | string[] {

function stripDefinitions({ body, markdownAST }: DefinitionPage): string {
const firstRefLink = markdownAST.children.find(
({ type }) => type === "definition"
({ type }) => type === "definition",
);
const refLinkOffset = firstRefLink?.position?.start?.offset;

Expand Down
Loading
Loading