Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add no-generic-link-text rule #10

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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 17 commits into from
Dec 21, 2022
Merged
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
10 changes: 10 additions & 0 deletions 10 helpers/strip-and-downcase-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* Downcase and strip extra whitespaces and punctuation */
function stripAndDowncaseText(text) {
return text
.toLowerCase()
.replace(/[.,/#!$%^&*;:{}=\-_`~()]/g, "")
.replace(/\s+/g, " ")
.trim();
}

module.exports = { stripAndDowncaseText };
3 changes: 2 additions & 1 deletion 3 index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ const _ = require("lodash");
const accessibilityRules = require("./style/accessibility.json");
const base = require("./style/base.json");
const noDefaultAltText = require("./no-default-alt-text");
const noGenericLinkText = require("./no-generic-link-text");

const customRules = [noDefaultAltText];
const customRules = [noDefaultAltText, noGenericLinkText];

module.exports = [...customRules];

Expand Down
50 changes: 50 additions & 0 deletions 50 no-generic-link-text.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
const { stripAndDowncaseText } = require("./helpers/strip-and-downcase-text");

const bannedLinkText = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible or worth making this work for various languages?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plugin doesn't currently have i18n support, but I added support so people can additionally configure text so they can through that!

Related: #10 (comment)

Copy link
Contributor Author

@khiga8 khiga8 Dec 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we will want to address i18n in a separate issue! I'll make an issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"read more",
"learn more",
"more",
"here",
"click here",
"link",
];

module.exports = {
names: ["GH002", "no-generic-link-text"],
description:
"Avoid using generic link text like `Learn more` or `Click here`",
information: new URL(
"https://primer.style/design/accessibility/links#writing-link-text"
),
tags: ["accessibility", "links"],
function: function GH002(params, onError) {
// markdown syntax
const allBannedLinkTexts = bannedLinkText.concat(
params.config.additional_banned_texts || []
Copy link
Contributor Author

@khiga8 khiga8 Dec 21, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm using snake case to follow the convention set in markdownlint for how rule configs are set.

);
const inlineTokens = params.tokens.filter((t) => t.type === "inline");
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found markdown-it demo and using the debug functionality helpful for understanding how markdown is parsed.

Additionally, I referenced: md042.

for (const token of inlineTokens) {
const { children } = token;
let inLink = false;
let linkText = "";

for (const child of children) {
const { content, type } = child;
if (type === "link_open") {
inLink = true;
linkText = "";
} else if (type === "link_close") {
inLink = false;
if (allBannedLinkTexts.includes(stripAndDowncaseText(linkText))) {
onError({
lineNumber: child.lineNumber,
detail: `For link: ${linkText}`,
});
}
} else if (inLink) {
linkText += content;
}
}
}
},
};
1 change: 1 addition & 0 deletions 1 style/accessibility.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"no-default-alt-text": true,
"no-duplicate-header": true,
"no-emphasis-as-header": true,
"no-generic-link-text": true,
"no-space-in-links": false,
"ol-prefix": "ordered",
"single-h1": true,
Expand Down
20 changes: 20 additions & 0 deletions 20 test/helpers/strip-and-downcase-text.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const {
stripAndDowncaseText,
} = require("../../helpers/strip-and-downcase-text");

describe("stripAndDowncaseText", () => {
test("strips extra whitespace", () => {
expect(stripAndDowncaseText(" read more ")).toBe("read more");
expect(stripAndDowncaseText(" learn ")).toBe("learn");
});

test("strips punctuation", () => {
expect(stripAndDowncaseText("learn more!!!!")).toBe("learn more");
expect(stripAndDowncaseText("I like dogs...")).toBe("i like dogs");
});

test("downcases text", () => {
expect(stripAndDowncaseText("HeRe")).toBe("here");
expect(stripAndDowncaseText("CLICK HERE")).toBe("click here");
});
});
44 changes: 8 additions & 36 deletions 44 test/no-default-alt-text.test.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,5 @@
const markdownlint = require("markdownlint");
const altTextRule = require("../no-default-alt-text");

const thisRuleName = altTextRule.names[1];

const config = {
config: {
default: false,
[thisRuleName]: true,
},
customRules: [altTextRule],
};

async function runTest(strings) {
return await Promise.all(
strings.map((variation) => {
const thisTestConfig = {
...config,
strings: [variation],
};

return new Promise((resolve, reject) => {
markdownlint(thisTestConfig, (err, result) => {
if (err) reject(err);
resolve(result[0][0]);
});
});
})
);
}
const runTest = require("./utils/run-test").runTest;

describe("GH001: No Default Alt Text", () => {
describe("successes", () => {
Expand All @@ -36,7 +8,7 @@ describe("GH001: No Default Alt Text", () => {
"![Chart with a single root node reading 'Example'](https://user-images.githubusercontent.com/abcdef.png)",
];

const results = await runTest(strings);
const results = await runTest(strings, altTextRule);

for (const result of results) {
expect(result).not.toBeDefined();
Expand All @@ -47,7 +19,7 @@ describe("GH001: No Default Alt Text", () => {
'<img alt="A helpful description" src="https://user-images.githubusercontent.com/abcdef.png">',
];

const results = await runTest(strings);
const results = await runTest(strings, altTextRule);

for (const result of results) {
expect(result).not.toBeDefined();
Expand All @@ -63,7 +35,7 @@ describe("GH001: No Default Alt Text", () => {
"![Screenshot 2022-06-26 at 7 41 30 PM](https://user-images.githubusercontent.com/abcdef.png)",
];

const results = await runTest(strings);
const results = await runTest(strings, altTextRule);

const failedRules = results
.map((result) => result.ruleNames)
Expand All @@ -72,7 +44,7 @@ describe("GH001: No Default Alt Text", () => {

expect(failedRules).toHaveLength(4);
for (const rule of failedRules) {
expect(rule).toBe(thisRuleName);
expect(rule).toBe("no-default-alt-text");
}
});

Expand All @@ -84,7 +56,7 @@ describe("GH001: No Default Alt Text", () => {
'<img alt="Screenshot 2022-06-26 at 7 41 30 PM" src="https://user-images.githubusercontent.com/abcdef.png">',
];

const results = await runTest(strings);
const results = await runTest(strings, altTextRule);

const failedRules = results
.map((result) => result.ruleNames)
Expand All @@ -93,7 +65,7 @@ describe("GH001: No Default Alt Text", () => {

expect(failedRules).toHaveLength(4);
for (const rule of failedRules) {
expect(rule).toBe(thisRuleName);
expect(rule).toBe("no-default-alt-text");
}
});

Expand All @@ -103,7 +75,7 @@ describe("GH001: No Default Alt Text", () => {
'<img alt="Screen Shot 2022-06-26 at 7 41 30 PM" src="https://user-images.githubusercontent.com/abcdef.png">',
];

const results = await runTest(strings);
const results = await runTest(strings, altTextRule);

expect(results[0].ruleDescription).toMatch(
/Images should not use the MacOS default screenshot filename as alternate text/
Expand Down
79 changes: 79 additions & 0 deletions 79 test/no-generic-link-text.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const noGenericLinkTextRule = require("../no-generic-link-text");
const runTest = require("./utils/run-test").runTest;

describe("GH002: No Generic Link Text", () => {
describe("successes", () => {
test("inline", async () => {
const strings = [
"[GitHub](https://www.github.com)",
"[Read more about GitHub](https://www.github.com/about)",
"[](www.github.com)",
"![Image](www.github.com)",
`
## Hello
I am not a link, and unrelated.
![GitHub](some_image.png)
`,
];

const results = await runTest(strings, noGenericLinkTextRule);

for (const result of results) {
expect(result).not.toBeDefined();
}
});
});
describe("failures", () => {
test("inline", async () => {
const strings = [
"[Click here](www.github.com)",
"[here](www.github.com)",
"Please [read more](www.github.com)",
"[more](www.github.com)",
"[link](www.github.com)",
"You may [learn more](www.github.com) at GitHub",
"[learn more.](www.github.com)",
"[click here!](www.github.com)",
];

const results = await runTest(strings, noGenericLinkTextRule);

const failedRules = results
.map((result) => result.ruleNames)
.flat()
.filter((name) => !name.includes("GH"));

expect(failedRules).toHaveLength(8);
for (const rule of failedRules) {
expect(rule).toBe("no-generic-link-text");
}
});

test("error message", async () => {
const strings = ["[Click here](www.github.com)"];

const results = await runTest(strings, noGenericLinkTextRule);

expect(results[0].ruleDescription).toMatch(
/Avoid using generic link text like `Learn more` or `Click here`/
);
expect(results[0].errorDetail).toBe("For link: Click here");
});

test("additional words can be configured", async () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

馃挅

const results = await runTest(
["[something](www.github.com)"],
noGenericLinkTextRule,
// eslint-disable-next-line camelcase
{ additional_banned_texts: ["something"] }
);

const failedRules = results
.map((result) => result.ruleNames)
.flat()
.filter((name) => !name.includes("GH"));

expect(failedRules).toHaveLength(1);
});
});
});
3 changes: 2 additions & 1 deletion 3 test/usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ describe("usage", () => {
describe("default export", () => {
test("custom rules on default export", () => {
const rules = githubMarkdownLint;
expect(rules).toHaveLength(1);
expect(rules).toHaveLength(2);
expect(rules[0].names).toEqual(["GH001", "no-default-alt-text"]);
});
});
Expand All @@ -17,6 +17,7 @@ describe("usage", () => {
"no-space-in-links": false,
"single-h1": true,
"no-emphasis-as-header": true,
"no-generic-link-text": true,
"ul-style": true,
default: true,
"no-inline-html": false,
Expand Down
31 changes: 31 additions & 0 deletions 31 test/utils/run-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const markdownlint = require("markdownlint");
khiga8 marked this conversation as resolved.
Show resolved Hide resolved

async function runTest(strings, rule, ruleConfig) {
const thisRuleName = rule.names[1];

const config = {
config: {
default: false,
[thisRuleName]: ruleConfig || true,
},
customRules: [rule],
};

return await Promise.all(
strings.map((variation) => {
const thisTestConfig = {
...config,
strings: [variation],
};

return new Promise((resolve, reject) => {
markdownlint(thisTestConfig, (err, result) => {
if (err) reject(err);
resolve(result[0][0]);
});
});
})
);
}

exports.runTest = runTest;
Morty Proxy This is a proxified and sanitized view of the page, visit original site.