diff --git a/plugin/scripts/lib/md-checks.mjs b/plugin/scripts/lib/md-checks.mjs index 5cf5759..3502066 100644 --- a/plugin/scripts/lib/md-checks.mjs +++ b/plugin/scripts/lib/md-checks.mjs @@ -27,6 +27,9 @@ // ref-undefined full/collapsed reference [text][label] with no // definition (renders as literal brackets on GitHub) // def-orphan a [label]: definition no reference ever uses +// image-alt-missing image with empty or absent alt text — screen readers +// announce "image" with no description, search engines +// cannot index content (MD045 class, WCAG 1.1.1) // bare-url a raw http(s)/www URL in prose text (GFM auto-links // it, CommonMark does not; MD034 class — style signal) // doc-too-large pre-parse short-circuit: input over MAX_DOC_BYTES is @@ -246,6 +249,19 @@ export function checkDocument(src, opts = {}) { if (node.type === 'link' || node.type === 'image' || node.type === 'definition') checkTarget(node, node.url); }); + // ---- images: alt text ------------------------------------------------------- + // MD045 / WCAG 1.1.1: every image needs a non-empty alt. Without it a screen + // reader says "image" with no description, and search engines cannot index the + // content. Both inline images (![alt](url)) and reference images (![alt][ref]) + // are checked — the AST carries the alt field on both node types. + walk(root, (node) => { + if (node.type !== 'image' && node.type !== 'imageReference') return; + const alt = (node.alt || '').trim(); + if (!alt) { + add('image-alt-missing', node, `image has no alt text — screen readers and search engines cannot describe it (MD045)`); + } + }); + // ---- tables ---------------------------------------------------------------- walk(root, (node) => { if (node.type !== 'table' || !node.children.length) return; diff --git a/plugin/skills/doc-structure/SKILL.md b/plugin/skills/doc-structure/SKILL.md index e27a13c..8cc75ad 100644 --- a/plugin/skills/doc-structure/SKILL.md +++ b/plugin/skills/doc-structure/SKILL.md @@ -36,6 +36,7 @@ Scan markdown docs for structural breakage. Report CONFIRMED findings. Fix on re | table-ragged | row with MORE cells than the header (GitHub silently drops them) | | ref-undefined | \[text]\[label] with no definition (renders as literal brackets) | | def-orphan | definition never referenced | +| image-alt-missing | image with empty or absent alt text — inaccessible to screen readers, invisible to search engines (MD045 / WCAG 1.1.1) | | bare-url | raw URL in prose (MD034 class) | | doc-unreadable | binary/corrupted input (NUL byte sniffed) — refused before parsing, never a false "0 findings" clean bill | diff --git a/plugin/skills/doc-structure/lib/md-checks.mjs b/plugin/skills/doc-structure/lib/md-checks.mjs index 5cf5759..3502066 100644 --- a/plugin/skills/doc-structure/lib/md-checks.mjs +++ b/plugin/skills/doc-structure/lib/md-checks.mjs @@ -27,6 +27,9 @@ // ref-undefined full/collapsed reference [text][label] with no // definition (renders as literal brackets on GitHub) // def-orphan a [label]: definition no reference ever uses +// image-alt-missing image with empty or absent alt text — screen readers +// announce "image" with no description, search engines +// cannot index content (MD045 class, WCAG 1.1.1) // bare-url a raw http(s)/www URL in prose text (GFM auto-links // it, CommonMark does not; MD034 class — style signal) // doc-too-large pre-parse short-circuit: input over MAX_DOC_BYTES is @@ -246,6 +249,19 @@ export function checkDocument(src, opts = {}) { if (node.type === 'link' || node.type === 'image' || node.type === 'definition') checkTarget(node, node.url); }); + // ---- images: alt text ------------------------------------------------------- + // MD045 / WCAG 1.1.1: every image needs a non-empty alt. Without it a screen + // reader says "image" with no description, and search engines cannot index the + // content. Both inline images (![alt](url)) and reference images (![alt][ref]) + // are checked — the AST carries the alt field on both node types. + walk(root, (node) => { + if (node.type !== 'image' && node.type !== 'imageReference') return; + const alt = (node.alt || '').trim(); + if (!alt) { + add('image-alt-missing', node, `image has no alt text — screen readers and search engines cannot describe it (MD045)`); + } + }); + // ---- tables ---------------------------------------------------------------- walk(root, (node) => { if (node.type !== 'table' || !node.children.length) return; diff --git a/scripts/fixtures/defects-structure.md b/scripts/fixtures/defects-structure.md index 0bd49d5..d510736 100644 --- a/scripts/fixtures/defects-structure.md +++ b/scripts/fixtures/defects-structure.md @@ -31,3 +31,5 @@ Bare URL: https://example.com/dangling in prose. Some instructions. ## Setup + +![](./img/no-alt.png) diff --git a/scripts/lib/md-checks.mjs b/scripts/lib/md-checks.mjs index 5cf5759..3502066 100644 --- a/scripts/lib/md-checks.mjs +++ b/scripts/lib/md-checks.mjs @@ -27,6 +27,9 @@ // ref-undefined full/collapsed reference [text][label] with no // definition (renders as literal brackets on GitHub) // def-orphan a [label]: definition no reference ever uses +// image-alt-missing image with empty or absent alt text — screen readers +// announce "image" with no description, search engines +// cannot index content (MD045 class, WCAG 1.1.1) // bare-url a raw http(s)/www URL in prose text (GFM auto-links // it, CommonMark does not; MD034 class — style signal) // doc-too-large pre-parse short-circuit: input over MAX_DOC_BYTES is @@ -246,6 +249,19 @@ export function checkDocument(src, opts = {}) { if (node.type === 'link' || node.type === 'image' || node.type === 'definition') checkTarget(node, node.url); }); + // ---- images: alt text ------------------------------------------------------- + // MD045 / WCAG 1.1.1: every image needs a non-empty alt. Without it a screen + // reader says "image" with no description, and search engines cannot index the + // content. Both inline images (![alt](url)) and reference images (![alt][ref]) + // are checked — the AST carries the alt field on both node types. + walk(root, (node) => { + if (node.type !== 'image' && node.type !== 'imageReference') return; + const alt = (node.alt || '').trim(); + if (!alt) { + add('image-alt-missing', node, `image has no alt text — screen readers and search engines cannot describe it (MD045)`); + } + }); + // ---- tables ---------------------------------------------------------------- walk(root, (node) => { if (node.type !== 'table' || !node.children.length) return; diff --git a/scripts/lib/md-checks.test.mjs b/scripts/lib/md-checks.test.mjs index 1215541..86dd825 100644 --- a/scripts/lib/md-checks.test.mjs +++ b/scripts/lib/md-checks.test.mjs @@ -31,7 +31,9 @@ test('defects-structure.md: every planted defect found — exact check ids and l 'def-orphan@27', 'file-missing@11', // ./no-such-file.md 'file-missing@13', // dead image + 'file-missing@35', // no-alt image target also missing 'heading-duplicate@33', // second "## Setup" — anchor silently points to first + 'image-alt-missing@35', // ![](./img/no-alt.png) — no alt text 'heading-multiple-h1@5', 'heading-skip@3', 'ref-undefined@23', diff --git a/skills/doc-structure/SKILL.md b/skills/doc-structure/SKILL.md index e27a13c..8cc75ad 100644 --- a/skills/doc-structure/SKILL.md +++ b/skills/doc-structure/SKILL.md @@ -36,6 +36,7 @@ Scan markdown docs for structural breakage. Report CONFIRMED findings. Fix on re | table-ragged | row with MORE cells than the header (GitHub silently drops them) | | ref-undefined | \[text]\[label] with no definition (renders as literal brackets) | | def-orphan | definition never referenced | +| image-alt-missing | image with empty or absent alt text — inaccessible to screen readers, invisible to search engines (MD045 / WCAG 1.1.1) | | bare-url | raw URL in prose (MD034 class) | | doc-unreadable | binary/corrupted input (NUL byte sniffed) — refused before parsing, never a false "0 findings" clean bill |