-
Notifications
You must be signed in to change notification settings - Fork 1
Added the glossary page #53
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
base: main
Are you sure you want to change the base?
Changes from 9 commits
69f4051
d8787e6
31cfb5a
1f3b836
d6387cc
91f0816
82f624f
61dea9c
037816c
6aa3ade
0b17c67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,180 @@ | ||
| #!/usr/bin/env ts-node | ||
| import fs from "fs"; | ||
| import path from "path"; | ||
| import yaml from "js-yaml"; | ||
| import { Command } from "commander"; | ||
| import { DefinitionPage } from "../types"; | ||
| import { getRulePages, getDefinitionPages } from "../utils/get-page-data"; | ||
| import { | ||
| getGlossaryBody, | ||
| getGlossaryHeading, | ||
| normalizeHeadingLevels as normalizeGlossaryHeadingLevels, | ||
| } from "../utils/glossary"; | ||
| 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 normalizeGlossaryHeadingLevels(body); | ||
| } | ||
|
|
||
| function generateGlossaryContent( | ||
| glossaryDefinitions: DefinitionPage[], | ||
| 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.md"); | ||
| lines.push("---"); | ||
| lines.push(""); | ||
|
|
||
| glossaryDefinitions.forEach((def) => { | ||
| const key = def.frontmatter.key; | ||
| const title = def.frontmatter.title; | ||
| const body = getGlossaryBody(def, { | ||
| mode: "full", | ||
| normalizeHeadings: true, | ||
| }); | ||
|
|
||
| lines.push(getGlossaryHeading({ title, key }, 2)); | ||
| 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(""); | ||
| }); | ||
|
|
||
|
|
||
| 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"); | ||
| const outputFile = path.join(outputDir, "terms.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); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,36 +1,26 @@ | ||
| import { DefinitionPage } from "../../types"; | ||
| import { getGlossaryBody, getGlossaryHeading } from "../../utils/glossary"; | ||
| import { joinStrings } from "../../utils/join-strings"; | ||
|
|
||
| export const getGlossary = (_: unknown, glossary: DefinitionPage[]): string => { | ||
| const glossaryTexts = glossary.map(getGlossaryMarkdown); | ||
| return joinStrings(`## Glossary`, ...glossaryTexts); | ||
| }; | ||
|
|
||
| export const getFullGlossary = (glossary: DefinitionPage[]): string => { | ||
| const glossaryTexts = glossary.map(getFullGlossaryMarkdown); | ||
| return joinStrings(`## Glossary`, ...glossaryTexts); | ||
| }; | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch, I'll remove it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| function getGlossaryMarkdown(definition: DefinitionPage): string { | ||
| const { title, key } = definition.frontmatter; | ||
| const heading = `### ${title} {#${key}}`; | ||
| const body = getDefinitionBody(definition); | ||
| const heading = getGlossaryHeading(definition.frontmatter, 3); | ||
| const body = getGlossaryBody(definition, { mode: "rule" }); | ||
| 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 | ||
| const lines = definition.body.split("\n"); | ||
| const headingLineNum = lines.findIndex((line) => line.match(/^##/)); | ||
| if (headingLineNum === -1) { | ||
| return stripDefinitions(definition); | ||
| } | ||
|
|
||
| lines.splice(headingLineNum); | ||
| return lines; | ||
| } | ||
|
|
||
| function stripDefinitions({ body, markdownAST }: DefinitionPage): string { | ||
| const firstRefLink = markdownAST.children.find( | ||
| ({ type }) => type === "definition" | ||
| ); | ||
| const refLinkOffset = firstRefLink?.position?.start?.offset; | ||
|
|
||
| return !refLinkOffset ? body : body.substr(0, refLinkOffset); | ||
| function getFullGlossaryMarkdown(definition: DefinitionPage): string { | ||
| const heading = getGlossaryHeading(definition.frontmatter, 3); | ||
| const body = getGlossaryBody(definition, { mode: "full" }); | ||
| // Keep full source definition body (including all sections after first ##) | ||
| return joinStrings(heading, body); | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind adding some tests for this?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tests for |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| type GlossaryFrontmatter = { | ||
| title: string; | ||
| key: string; | ||
| }; | ||
|
|
||
| type GlossaryPage = { | ||
| body: string; | ||
| markdownAST: { | ||
| children: Array<{ | ||
| type?: string; | ||
| position?: { start?: { offset?: number } }; | ||
| }>; | ||
| }; | ||
| }; | ||
|
|
||
| type GlossaryBodyMode = "rule" | "full"; | ||
|
|
||
| type GlossaryBodyOptions = { | ||
| mode: GlossaryBodyMode; | ||
| normalizeHeadings?: boolean; | ||
| }; | ||
|
|
||
| export function getGlossaryHeading( | ||
| frontmatter: GlossaryFrontmatter, | ||
| level: number, | ||
| ): string { | ||
| return `${"#".repeat(level)} ${frontmatter.title} {#${frontmatter.key}}`; | ||
| } | ||
|
|
||
| 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}`; | ||
| }); | ||
| } | ||
|
|
||
| export function getGlossaryBody( | ||
| definition: GlossaryPage, | ||
| options: GlossaryBodyOptions, | ||
| ): string { | ||
| const body = | ||
| options.mode === "full" | ||
| ? definition.body.trim() | ||
| : getDefinitionBodyForRule(definition); | ||
|
|
||
| return options.normalizeHeadings ? normalizeHeadingLevels(body) : body; | ||
| } | ||
|
|
||
| function getDefinitionBodyForRule(definition: GlossaryPage): string { | ||
| // Delete all lines after the first heading. | ||
| // References are mixed into the bottom of the rule page later. | ||
| const lines = definition.body.split("\n"); | ||
| const headingLineNum = lines.findIndex((line) => line.match(/^##/)); | ||
| if (headingLineNum === -1) { | ||
| return stripDefinitions(definition); | ||
| } | ||
|
|
||
| lines.splice(headingLineNum); | ||
| return lines.join("\n").trim(); | ||
| } | ||
|
|
||
| function stripDefinitions({ body, markdownAST }: GlossaryPage): string { | ||
| const firstRefLink = markdownAST.children.find( | ||
| ({ type }) => type === "definition", | ||
| ); | ||
| const refLinkOffset = firstRefLink?.position?.start?.offset; | ||
|
|
||
| return !refLinkOffset ? body.trim() : body.substring(0, refLinkOffset).trim(); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "Used in Rules" list can become quite big and require a lot of scrolling from users. For example, the list for the "Outcome" term consists of 94 items.
Before considering possible solutions, could you clarify who this section is intended for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, this adds a large amount of visual space. I think embedding this content in summary and details elements is probably the best solution but I that would be assuming use cases myself. This came from the issue requirements:
I think it may also be a good option to move forward with the page as it is and adjust this later if it holds us up.
@WilcoFiers , do you have any context or suggestions to add here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hi.
I am not even sure if we should have this "used in rules" at all.
I think the primary use case for a dedicated (self-contained) terms page is for rule authors to know whether or not something is or is not currently defined by ACT.
If you want to know which rules use which terms, you could get to the specific rule pages and see the terms at the bottom. I see this as a secondary need that is not worth the extra clutter.
If folks think strongly about keeping this, then I would support the details/summary alternative, but that should be done in alignment with how the expand/collapse single and expand/collapse multiple currently work on the wai website theme
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should keep this section. Yeah it's more for rule authors than other people, but this page is mostly for rule authors anyway. I'm okay with using a details / summary thing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second (or third?) keeping the "used in rules" section. It is a common use case for me to search all rules using a given definition, typically to assess which rules will be impacted by changes in the definition. I'm currently doing that somewhat manually through
grep, which is not perfect due to transitive definition inclusion, … (and going through all rules pages is not a real possibility).A details/summary is perfect for that, indeed.
(for a recent case: searching all rules that use "marked as decorative" to evaluate impact of "whitespace alt is decorative")