From ccc7d238a2ceb0e92ec91062f1c402af768865e7 Mon Sep 17 00:00:00 2001 From: Kate Higa <16447748+khiga8@users.noreply.github.com> Date: Mon, 9 Oct 2023 08:04:52 -0700 Subject: [PATCH] Introduce a `no-empty-alt-text` rule (#85) * Make sure that HTML that is inlined is supported * Add detail * Update the helpers to support multiple errors in one line * add test support * Add test cases * Update configg * Update export * Add new rule * add test cases and update docs * Update test matchers * Update src/rules/no-empty-string-alt.js Co-authored-by: Ian Sanders * Update src/rules/no-empty-string-alt.js Co-authored-by: Ian Sanders * Revert "Update src/rules/no-empty-string-alt.js" This reverts commit 5b17abfbc297834a94c3c91a5f18a523b7a3a8aa. Reverting because we are using this index. * Fix Regex syntax * Remove markdown syntax support * Update doc to remove markdown syntax * Add test case for multiple images in one line Related:https://github.com/github/markdownlint-github/pull/85#discussion_r1348919044 * Rename rule to no-empty-alt-text * Add missing bracket * Update README with the rule * Update docs/rules/GH003-no-empty-alt-text.md Co-authored-by: Ian Sanders * Update src/rules/no-empty-alt-text.js Co-authored-by: Ian Sanders --------- Co-authored-by: Ian Sanders --- README.md | 1 + docs/rules/GH003-no-empty-alt-text.md | 27 ++++++++++++ index.js | 5 ++- src/rules/index.js | 6 ++- src/rules/no-empty-alt-text.js | 40 ++++++++++++++++++ test/no-empty-alt-text.test.js | 60 +++++++++++++++++++++++++++ test/usage.test.js | 6 ++- 7 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 docs/rules/GH003-no-empty-alt-text.md create mode 100644 src/rules/no-empty-alt-text.js create mode 100644 test/no-empty-alt-text.test.js diff --git a/README.md b/README.md index 073b602..3bc08be 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The following are custom rules defined in this plugin. * [**GH001** _no-default-alt-text_](./docs/rules/GH001-no-default-alt-text.md) * [**GH002** _no-generic-link-text_](./docs/rules/GH002-no-generic-link-text.md) +* [**GH002** _no-empty-alt-text_](./docs/rules/GH002-no-empty-alt-text.md) See [`markdownlint` rules](https://github.com/DavidAnson/markdownlint#rules--aliases) for documentation on rules pulled in from `markdownlint`. diff --git a/docs/rules/GH003-no-empty-alt-text.md b/docs/rules/GH003-no-empty-alt-text.md new file mode 100644 index 0000000..dc73ef6 --- /dev/null +++ b/docs/rules/GH003-no-empty-alt-text.md @@ -0,0 +1,27 @@ +# GH003 No Empty Alt Text + +## Rule details + +⚠️ This rule is _off_ by default and is only applicable for GitHub rendered markdown. + +Currently, all images on github.com are automatically wrapped in an anchor tag. + +As a result, images that are intentionally marked as decorative (via `alt=""`) end up rendering as a link without an accessible name. This is confusing and inaccessible for assistive technology users. + +This rule can be enabled to enforce that the alt attribute is always set to descriptive text. + +This rule should be removed once this behavior is updated on GitHub's UI. + +## Examples + +### Incorrect 👎 + +```html + +``` + +### Correct 👍 + +```html +Mona Lisa, the Octocat +``` diff --git a/index.js b/index.js index c16a4ff..06ba60c 100644 --- a/index.js +++ b/index.js @@ -6,8 +6,11 @@ const gitHubCustomRules = require("./src/rules/index").rules; module.exports = [...gitHubCustomRules]; +const offByDefault = ["no-empty-alt-text"]; + for (const rule of gitHubCustomRules) { - base[rule.names[1]] = true; + const ruleName = rule.names[1]; + base[ruleName] = offByDefault.includes(ruleName) ? false : true; } module.exports.init = function init(consumerConfig) { diff --git a/src/rules/index.js b/src/rules/index.js index c6c9664..08fd9e6 100644 --- a/src/rules/index.js +++ b/src/rules/index.js @@ -1,3 +1,7 @@ module.exports = { - rules: [require("./no-default-alt-text"), require("./no-generic-link-text")], + rules: [ + require("./no-default-alt-text"), + require("./no-generic-link-text"), + require("./no-empty-alt-text"), + ], }; diff --git a/src/rules/no-empty-alt-text.js b/src/rules/no-empty-alt-text.js new file mode 100644 index 0000000..02cab26 --- /dev/null +++ b/src/rules/no-empty-alt-text.js @@ -0,0 +1,40 @@ +module.exports = { + names: ["GH003", "no-empty-alt-text"], + description: "Please provide an alternative text for the image.", + information: new URL( + "https://github.com/github/markdownlint-github/blob/main/docs/rules/GH003-no-empty-alt-text.md", + ), + tags: ["accessibility", "images"], + function: function GH003(params, onError) { + const htmlTagsWithImages = params.parsers.markdownit.tokens.filter( + (token) => { + return ( + (token.type === "html_block" && token.content.includes(" child.type === "html_inline")) + ); + }, + ); + + const htmlAltRegex = new RegExp(/alt=['"]['"]/, "gid"); + + for (const token of htmlTagsWithImages) { + const lineRange = token.map; + const lineNumber = token.lineNumber; + const lines = params.lines.slice(lineRange[0], lineRange[1]); + + for (const [i, line] of lines.entries()) { + const matches = line.matchAll(htmlAltRegex); + for (const match of matches) { + const matchingContent = match[0]; + const startIndex = match.indices[0][0]; + onError({ + lineNumber: lineNumber + i, + range: [startIndex + 1, matchingContent.length], + }); + } + } + } + }, +}; diff --git a/test/no-empty-alt-text.test.js b/test/no-empty-alt-text.test.js new file mode 100644 index 0000000..0b6fd43 --- /dev/null +++ b/test/no-empty-alt-text.test.js @@ -0,0 +1,60 @@ +const noEmptyStringAltRule = require("../src/rules/no-empty-alt-text"); +const runTest = require("./utils/run-test").runTest; + +describe("GH003: No Empty Alt Text", () => { + describe("successes", () => { + test("html image", async () => { + const strings = [ + 'A helpful description', + "``", // code block + ]; + + const results = await runTest(strings, noEmptyStringAltRule); + expect(results).toHaveLength(0); + }); + }); + describe("failures", () => { + test("HTML example", async () => { + const strings = [ + '', + "", + ' ', + ]; + + const results = await runTest(strings, noEmptyStringAltRule); + + const failedRules = results + .map((result) => result.ruleNames) + .flat() + .filter((name) => !name.includes("GH")); + + expect(failedRules).toHaveLength(4); + for (const rule of failedRules) { + expect(rule).toBe("no-empty-alt-text"); + } + }); + + test("error message", async () => { + const strings = [ + '', + ' ', + ]; + + const results = await runTest(strings, noEmptyStringAltRule); + + expect(results[0].ruleDescription).toMatch( + "Please provide an alternative text for the image.", + ); + expect(results[0].errorRange).toEqual([6, 6]); + + expect(results[1].ruleDescription).toMatch( + "Please provide an alternative text for the image.", + ); + expect(results[1].errorRange).toEqual([20, 6]); + expect(results[2].ruleDescription).toMatch( + "Please provide an alternative text for the image.", + ); + expect(results[2].errorRange).toEqual([49, 6]); + }); + }); +}); diff --git a/test/usage.test.js b/test/usage.test.js index 24cd235..a87d10b 100644 --- a/test/usage.test.js +++ b/test/usage.test.js @@ -4,8 +4,11 @@ describe("usage", () => { describe("default export", () => { test("custom rules on default export", () => { const rules = githubMarkdownLint; - expect(rules).toHaveLength(2); + expect(rules).toHaveLength(3); + expect(rules[0].names).toEqual(["GH001", "no-default-alt-text"]); + expect(rules[1].names).toEqual(["GH002", "no-generic-link-text"]); + expect(rules[2].names).toEqual(["GH003", "no-empty-alt-text"]); }); }); describe("init method", () => { @@ -17,6 +20,7 @@ describe("usage", () => { "no-space-in-links": false, "single-h1": true, "no-emphasis-as-header": true, + "no-empty-alt-text": false, "heading-increment": true, "no-generic-link-text": true, "ul-style": {