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
+
+```
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 = [
+ '
',
+ "`
`", // 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": {