Skip to content

Commit d417bf9

Browse files
davidxborsrazvand
authored andcommitted
Add markdown linting capabilities to Super-linter
This commit adds markdown linting capabilites to Super-linter. It uses an additional set of custom rules (.github/workflows/rules) and a custom configuration file (.github/workflows/config).
1 parent 6a5cffd commit d417bf9

File tree

9 files changed

+295
-8
lines changed

9 files changed

+295
-8
lines changed
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"default": true,
3+
"MD048": { "style": "backtick" },
4+
"MD046": { "style": "fenced" },
5+
"MD029": { "style": "one" },
6+
"line-length": false,
7+
"no-hard-tabs": false
8+
}

.github/workflows/linter.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
name: Linter
22

3-
on:
4-
push:
5-
branches-ignore: [master, main]
6-
pull_request:
7-
branches: [master]
3+
on: [push, pull_request]
84

95
jobs:
10-
build:
11-
name: Linter
6+
superlinter:
7+
name: Super Linter
128
runs-on: ubuntu-latest
139

1410
steps:
@@ -23,5 +19,9 @@ jobs:
2319
env:
2420
# Don't check already existent files
2521
VALIDATE_ALL_CODEBASE: false
26-
DEFAULT_BRANCH: master
22+
VALIDATE_GITHUB_ACTIONS: false
23+
LINTER_RULES_PATH: /.github/workflows/
24+
MARKDOWN_CONFIG_FILE: config/config.json
25+
MARKDOWN_CUSTOM_RULE_GLOBS: rules/rules.js
26+
DEFAULT_BRANCH: main
2727
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class InlineTokenChildren {
2+
constructor(token) {
3+
if (token.type === "inline") {
4+
this.root = token;
5+
this.column = -1;
6+
this.lineNumber = token.map[0];
7+
} else {
8+
throw new TypeError("wrong argument token type");
9+
}
10+
}
11+
12+
*[Symbol.iterator]() {
13+
for (let token of this.root.children) {
14+
let { line, lineNumber } = token;
15+
if (this.lineNumber !== lineNumber) {
16+
this.column = -1;
17+
this.lineNumber = lineNumber;
18+
}
19+
this.column = line.indexOf(token.content, this.column + 1);
20+
yield { token, column: this.column + 1, lineNumber };
21+
}
22+
}
23+
}
24+
25+
module.exports = { InlineTokenChildren };
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
class WordPattern {
2+
constructor(pattern, parameters) {
3+
const escapedDots = pattern.replace(/\\?\./g, "\\.");
4+
this.pattern = parameters && parameters.hasOwnProperty('noWordBoundary') ? escapedDots : "\\b" + escapedDots + "\\b";
5+
const modifiers = parameters && parameters.hasOwnProperty('caseSensitive') && parameters.caseSensitive ? "" : "i";
6+
this.regex = new RegExp(this.pattern, modifiers);
7+
this.suggestion = parameters && parameters.hasOwnProperty('suggestion') ? parameters.suggestion : pattern;
8+
this.stringRegex = new RegExp("^" + escapedDots + "$", modifiers); // To match "Category" column words in changelogs, see case-sensitive.js
9+
this.skipForUseCases = !!(parameters && parameters.hasOwnProperty('skipForUseCases'));
10+
}
11+
12+
test(line) {
13+
return new Match(line.match(this.regex));
14+
}
15+
}
16+
17+
class Match {
18+
constructor(match) {
19+
this.match = match;
20+
}
21+
22+
range() {
23+
if (this.match) {
24+
let column = this.match.index + 1;
25+
let length = this.match[0].length;
26+
if (this.match[2]) {
27+
column += this.match[1].length;
28+
length -= this.match[1].length;
29+
}
30+
return [column, length];
31+
}
32+
return null;
33+
}
34+
35+
toString() {
36+
return this.match ? this.match.toString() : "null";
37+
}
38+
}
39+
40+
module.exports = { WordPattern };

.github/workflows/rules/md101.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
const { InlineTokenChildren } = require("./common/inlineTokenChildren");
2+
const { WordPattern } = require("./common/wordPattern");
3+
4+
const keywords = [
5+
new WordPattern("iExtractor-manager"),
6+
new WordPattern("device-info"),
7+
new WordPattern("device-name"),
8+
new WordPattern("list_apps"),
9+
new WordPattern("decrypt_kcache"),
10+
new WordPattern("decrypt_fs"),
11+
new WordPattern("curl"),
12+
new WordPattern("wget"),
13+
new WordPattern("crontab"),
14+
new WordPattern("cron"),
15+
new WordPattern("netcat"),
16+
new WordPattern("ping"),
17+
new WordPattern("traceroute"),
18+
new WordPattern("sudo"),
19+
new WordPattern("(?<!(system |ISRG ))root(?! ca)", { suggestion: "root" }),// match "root", but not "root CA", "MacOS System Root" and "ISRG Root X1"
20+
new WordPattern("true"),
21+
new WordPattern("false"),
22+
new WordPattern("jps"),
23+
new WordPattern("name=value"),
24+
new WordPattern("key=value"),
25+
new WordPattern("time:value"),
26+
new WordPattern("atsd.log"),
27+
new WordPattern("start.log"),
28+
new WordPattern("logback.xml"),
29+
new WordPattern("graphite.conf"),
30+
new WordPattern("command_malformed.log"),
31+
new WordPattern("stdout"),
32+
new WordPattern("stderr"),
33+
new WordPattern("SIGTERM"),
34+
new WordPattern("NaN"),
35+
new WordPattern(".png", { noWordBoundary: true }),
36+
new WordPattern(".xml", { noWordBoundary: true }),
37+
new WordPattern(".jar", { noWordBoundary: true }),
38+
new WordPattern(".gz", { noWordBoundary: true }),
39+
new WordPattern(".tar.gz", { noWordBoundary: true }),
40+
new WordPattern(".zip", { noWordBoundary: true }),
41+
new WordPattern(".txt", { noWordBoundary: true }),
42+
new WordPattern(".csv", { noWordBoundary: true }),
43+
new WordPattern(".json", { noWordBoundary: true }),
44+
new WordPattern(".pdf", { noWordBoundary: true }),
45+
new WordPattern(".html", { noWordBoundary: true })
46+
47+
];
48+
49+
module.exports = {
50+
names: ["MD101", "backtick-keywords"],
51+
description: "Keywords must be fenced and must be in appropriate case.",
52+
tags: ["backtick", "code", "bash"],
53+
"function": (params, onError) => {
54+
var inHeading = false;
55+
var inLink = false;
56+
for (let token of params.tokens) {
57+
switch (token.type) {
58+
case "heading_open":
59+
inHeading = true; break;
60+
case "heading_close":
61+
inHeading = false; break;
62+
case "inline":
63+
let children = new InlineTokenChildren(token);
64+
for (let { token: child, column, lineNumber } of children) {
65+
let isText = child.type === "text";
66+
switch (child.type) {
67+
case "link_open":
68+
inLink = true; break;
69+
case "link_close":
70+
inLink = false; break;
71+
}
72+
for (let k of keywords) {
73+
let anyCaseMatch = child.content.match(k.regex);
74+
if (anyCaseMatch != null) {
75+
let match = anyCaseMatch[0];
76+
let correct = k.suggestion;
77+
if ((!inHeading && !inLink && isText) || // Bad not fenced
78+
(match !== correct)) { // Right fencing, wrong case
79+
onError({
80+
lineNumber,
81+
detail: `Expected \`${correct}\`. Actual ${match}.`,
82+
range: [column + anyCaseMatch.index, match.length]
83+
})
84+
}
85+
}
86+
}
87+
}
88+
}
89+
}
90+
}
91+
};

.github/workflows/rules/md102.js

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
const http_keywords = [
2+
"GET",
3+
"POST",
4+
"PUT",
5+
"PATCH",
6+
"DELETE",
7+
"Content-Type",
8+
"Content-Encoding",
9+
"User-Agent",
10+
"200 OK",
11+
"401 Unauthorized",
12+
"403 Forbidden",
13+
"API_DATA_READ",
14+
"API_DATA_WRITE",
15+
"API_META_READ",
16+
"API_META_WRITE",
17+
"USER",
18+
"EDITOR",
19+
"ENTITY_GROUP_ADMIN",
20+
"ADMIN"
21+
];
22+
const keywordsRegex = new RegExp(http_keywords.map(word => "\\b" + word + "\\b").join("|"));
23+
24+
const { InlineTokenChildren } = require("./common/inlineTokenChildren");
25+
26+
module.exports = {
27+
names: ["MD102", "backtick-http"],
28+
description: "HTTP keywords must be fenced.",
29+
tags: ["backtick", "HTTP", "HTTPS"],
30+
"function": (params, onError) => {
31+
var inHeading = false;
32+
for (let token of params.tokens) {
33+
switch (token.type) {
34+
case "heading_open":
35+
inHeading = true; break;
36+
case "heading_close":
37+
inHeading = false; break;
38+
case "inline":
39+
if (!inHeading) {
40+
let children = new InlineTokenChildren(token);
41+
for (let { token: child, column, lineNumber } of children) {
42+
if (child.type === "text") {
43+
let exactCaseMatch = child.content.match(keywordsRegex);
44+
if (exactCaseMatch != null) {
45+
let match = exactCaseMatch[0];
46+
onError({
47+
lineNumber,
48+
detail: `Expected \`${match}\`. Actual ${match}.`,
49+
range: [column + exactCaseMatch.index, match.length]
50+
})
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}
57+
}
58+
};

.github/workflows/rules/md103.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"use strict";
2+
3+
module.exports = {
4+
"names": [ "MD103", "inline triple backticks" ],
5+
"description": "inline triple backticks",
6+
"tags": [ "backticks" ],
7+
"function": function rule(params, onError) {
8+
for (const inline of params.tokens.filter(function filterToken(token) {
9+
return token.type === "inline";
10+
})) {
11+
const index = inline.content.toLowerCase().indexOf("```");
12+
if (index !== -1) {
13+
onError({
14+
"lineNumber": inline.lineNumber,
15+
"context": inline.content.substr(index - 1, 4),
16+
"detail": "Expected `. Actual ```"
17+
});
18+
}
19+
}
20+
}
21+
};

.github/workflows/rules/md104.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"use strict";
2+
3+
module.exports = {
4+
names: ["MD104", "one line per sentence"],
5+
description: "one line (and only one line) per sentence",
6+
tags: ["sentences"],
7+
function: function rule(params, onError) {
8+
for (const inline of params.tokens.filter(function filterToken(token) {
9+
return token.type === "inline";
10+
})) {
11+
var actual_lines = inline.content.split("\n");
12+
actual_lines.forEach((line, index, arr) => {
13+
let outside = true;
14+
let count = 0;
15+
Array.from(line).forEach((char) => {
16+
if ((char == "." || char == "?" || char == "!" || char == ";" || char == ":") && outside) {
17+
count++;
18+
}
19+
if (char == "`") outside = !outside;
20+
if (char == "[") outside = false;
21+
if (char == "(") outside = false;
22+
if (char == "]") outside = true;
23+
if (char == ")") outside = true;
24+
});
25+
if (count > 1) {
26+
onError({
27+
lineNumber: inline.lineNumber + index,
28+
detail:
29+
"Expected one sentence per line. Multiple end of sentence punctuation signs found on one line!",
30+
});
31+
}
32+
});
33+
}
34+
},
35+
};

.github/workflows/rules/rules.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
"use strict";
2+
3+
const rules = [
4+
require("./md101.js"),
5+
require("./md102.js"),
6+
require("./md103.js"),
7+
require("./md104.js"),
8+
];
9+
module.exports = rules;

0 commit comments

Comments
 (0)