@khanacademy/perseus-linter 0.0.0-PR973-20240207204548 → 0.0.0-PR973-20240207204934
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -4
- package/.eslintrc.js +0 -12
- package/CHANGELOG.md +0 -168
- package/src/README.md +0 -41
- package/src/__tests__/matcher.test.ts +0 -498
- package/src/__tests__/rule.test.ts +0 -110
- package/src/__tests__/rules.test.ts +0 -548
- package/src/__tests__/selector-parser.test.ts +0 -51
- package/src/__tests__/tree-transformer.test.ts +0 -444
- package/src/index.ts +0 -281
- package/src/proptypes.ts +0 -19
- package/src/rule.ts +0 -419
- package/src/rules/absolute-url.ts +0 -23
- package/src/rules/all-rules.ts +0 -71
- package/src/rules/blockquoted-math.ts +0 -9
- package/src/rules/blockquoted-widget.ts +0 -9
- package/src/rules/double-spacing-after-terminal.ts +0 -11
- package/src/rules/extra-content-spacing.ts +0 -11
- package/src/rules/heading-level-1.ts +0 -13
- package/src/rules/heading-level-skip.ts +0 -19
- package/src/rules/heading-sentence-case.ts +0 -10
- package/src/rules/heading-title-case.ts +0 -68
- package/src/rules/image-alt-text.ts +0 -20
- package/src/rules/image-in-table.ts +0 -9
- package/src/rules/image-spaces-around-urls.ts +0 -34
- package/src/rules/image-widget.ts +0 -49
- package/src/rules/link-click-here.ts +0 -10
- package/src/rules/lint-utils.ts +0 -47
- package/src/rules/long-paragraph.ts +0 -13
- package/src/rules/math-adjacent.ts +0 -9
- package/src/rules/math-align-extra-break.ts +0 -10
- package/src/rules/math-align-linebreaks.ts +0 -42
- package/src/rules/math-empty.ts +0 -9
- package/src/rules/math-font-size.ts +0 -11
- package/src/rules/math-frac.ts +0 -9
- package/src/rules/math-nested.ts +0 -10
- package/src/rules/math-starts-with-space.ts +0 -11
- package/src/rules/math-text-empty.ts +0 -9
- package/src/rules/math-without-dollars.ts +0 -13
- package/src/rules/nested-lists.ts +0 -10
- package/src/rules/profanity.ts +0 -9
- package/src/rules/table-missing-cells.ts +0 -19
- package/src/rules/unbalanced-code-delimiters.ts +0 -13
- package/src/rules/unescaped-dollar.ts +0 -9
- package/src/rules/widget-in-table.ts +0 -9
- package/src/selector.ts +0 -504
- package/src/tree-transformer.ts +0 -583
- package/src/types.ts +0 -7
- package/src/version.ts +0 -10
- package/tsconfig-build.json +0 -12
- package/tsconfig-build.tsbuildinfo +0 -1
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "image-alt-text",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "image",
|
|
7
|
-
lint: function (state, content, nodes, match) {
|
|
8
|
-
const image = nodes[0];
|
|
9
|
-
if (!image.alt || !image.alt.trim()) {
|
|
10
|
-
return `Images should have alt text:
|
|
11
|
-
for accessibility, all images should have alt text.
|
|
12
|
-
Specify alt text inside square brackets after the !.`;
|
|
13
|
-
}
|
|
14
|
-
if (image.alt.length < 8) {
|
|
15
|
-
return `Images should have alt text:
|
|
16
|
-
for accessibility, all images should have descriptive alt text.
|
|
17
|
-
This image's alt text is only ${image.alt.length} characters long.`;
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
}) as Rule;
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "image-spaces-around-urls",
|
|
5
|
-
severity: Rule.Severity.ERROR,
|
|
6
|
-
selector: "image",
|
|
7
|
-
lint: function (state, content, nodes, match, context) {
|
|
8
|
-
const image = nodes[0];
|
|
9
|
-
const url = image.target;
|
|
10
|
-
|
|
11
|
-
// The markdown parser strips leading and trailing spaces for us,
|
|
12
|
-
// but they're still a problem for our translation process, so
|
|
13
|
-
// we need to go check for them in the unparsed source string
|
|
14
|
-
// if we have it.
|
|
15
|
-
if (context && context.content) {
|
|
16
|
-
// Find the url in the original content and make sure that the
|
|
17
|
-
// character before is '(' and the character after is ')'
|
|
18
|
-
const index = context.content.indexOf(url);
|
|
19
|
-
if (index === -1) {
|
|
20
|
-
// It is not an error if we didn't find it.
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (
|
|
25
|
-
context.content[index - 1] !== "(" ||
|
|
26
|
-
context.content[index + url.length] !== ")"
|
|
27
|
-
) {
|
|
28
|
-
return `Whitespace before or after image url:
|
|
29
|
-
For images, don't include any space or newlines after '(' or before ')'.
|
|
30
|
-
Whitespace in image URLs causes translation difficulties.`;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
}) as Rule;
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
// Normally we have one rule per file. But since our selector class
|
|
4
|
-
// can't match specific widget types directly, this rule implements
|
|
5
|
-
// a number of image widget related rules in one place. This should
|
|
6
|
-
// slightly increase efficiency, but it means that if there is more
|
|
7
|
-
// than one problem with an image widget, the user will only see one
|
|
8
|
-
// problem at a time.
|
|
9
|
-
export default Rule.makeRule({
|
|
10
|
-
name: "image-widget",
|
|
11
|
-
severity: Rule.Severity.WARNING,
|
|
12
|
-
selector: "widget",
|
|
13
|
-
lint: function (state, content, nodes, match, context) {
|
|
14
|
-
// This rule only looks at image widgets
|
|
15
|
-
if (state.currentNode().widgetType !== "image") {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// If it can't find a definition for the widget it does nothing
|
|
20
|
-
const widget =
|
|
21
|
-
context &&
|
|
22
|
-
context.widgets &&
|
|
23
|
-
context.widgets[state.currentNode().id];
|
|
24
|
-
if (!widget) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Make sure there is alt text
|
|
29
|
-
const alt = widget.options.alt;
|
|
30
|
-
if (!alt) {
|
|
31
|
-
return `Images should have alt text:
|
|
32
|
-
for accessibility, all images should have a text description.
|
|
33
|
-
Add a description in the "Alt Text" box of the image widget.`;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Make sure the alt text it is not trivial
|
|
37
|
-
if (alt.trim().length < 8) {
|
|
38
|
-
return `Images should have alt text:
|
|
39
|
-
for accessibility, all images should have descriptive alt text.
|
|
40
|
-
This image's alt text is only ${alt.trim().length} characters long.`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// Make sure there is no math in the caption
|
|
44
|
-
if (widget.options.caption && widget.options.caption.match(/[^\\]\$/)) {
|
|
45
|
-
return `No math in image captions:
|
|
46
|
-
Don't include math expressions in image captions.`;
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
}) as Rule;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "link-click-here",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "link",
|
|
7
|
-
pattern: /click here/i,
|
|
8
|
-
message: `Inappropriate link text:
|
|
9
|
-
Do not use the words "click here" in links.`,
|
|
10
|
-
}) as Rule;
|
package/src/rules/lint-utils.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/* eslint-disable no-useless-escape */
|
|
2
|
-
// Return the portion of a URL between // and /. This is the authority
|
|
3
|
-
// portion which is usually just the hostname, but may also include
|
|
4
|
-
// a username, password or port. We don't strip those things out because
|
|
5
|
-
// we typically want to reject any URL that includes them
|
|
6
|
-
const HOSTNAME = /\/\/([^\/]+)/;
|
|
7
|
-
|
|
8
|
-
// Return the hostname of the URL, with any "www." prefix removed.
|
|
9
|
-
// If this is a relative URL with no hostname, return an empty string.
|
|
10
|
-
export function getHostname(url: string): string {
|
|
11
|
-
if (!url) {
|
|
12
|
-
return "";
|
|
13
|
-
}
|
|
14
|
-
const match = url.match(HOSTNAME);
|
|
15
|
-
return match ? match[1] : "";
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// This list of domains that count as internal domains is from
|
|
19
|
-
// webapp/content/models.py and webapp/url_util.py
|
|
20
|
-
const internalDomains = {
|
|
21
|
-
"khanacademy.org": true,
|
|
22
|
-
"www.khanacademy.org": true,
|
|
23
|
-
"kasandbox.org": true,
|
|
24
|
-
"fastly.kastatic.org": true,
|
|
25
|
-
"cdn.kastatic.org": true, // This isn't a link to cdn.kastatic.org
|
|
26
|
-
"ka-youtube-converted.storage.googleapis.com": true,
|
|
27
|
-
"KA-share.s3.amazonaws.com": true,
|
|
28
|
-
"ka-article-iframes.s3.amazonaws.com": true,
|
|
29
|
-
"ka-cs-algorithms.s3.amazonaws.com": true,
|
|
30
|
-
"ka-cs-challenge-images.s3.amazonaws.com": true,
|
|
31
|
-
"ka-cs-scratchpad-audio.s3.amazonaws.com": true,
|
|
32
|
-
"ka-exercise-screenshots.s3.amazonaws.com": true,
|
|
33
|
-
"ka-exercise-screenshots-2.s3.amazonaws.com": true,
|
|
34
|
-
"ka-exercise-screenshots-3.s3.amazonaws.com": true,
|
|
35
|
-
"ka-learnstorm.s3.amazonaws.com": true,
|
|
36
|
-
"ka-mobile.s3.amazonaws.com": true,
|
|
37
|
-
"ka-perseus-graphie.s3.amazonaws.com": true,
|
|
38
|
-
"ka-perseus-images.s3.amazonaws.com": true,
|
|
39
|
-
} as const;
|
|
40
|
-
|
|
41
|
-
// Returns true if this URL is relative, or if it is an absolute
|
|
42
|
-
// URL with one of the domains listed above as its hostname.
|
|
43
|
-
export function isInternalURL(url: string): boolean {
|
|
44
|
-
const hostname = getHostname(url);
|
|
45
|
-
// eslint-disable-next-line no-prototype-builtins
|
|
46
|
-
return hostname === "" || internalDomains.hasOwnProperty(hostname);
|
|
47
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "long-paragraph",
|
|
5
|
-
severity: Rule.Severity.GUIDELINE,
|
|
6
|
-
selector: "paragraph",
|
|
7
|
-
pattern: /^.{501,}/,
|
|
8
|
-
lint: function (state, content, nodes, match) {
|
|
9
|
-
return `Paragraph too long:
|
|
10
|
-
This paragraph is ${content.length} characters long.
|
|
11
|
-
Shorten it to 500 characters or fewer.`;
|
|
12
|
-
},
|
|
13
|
-
}) as Rule;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-adjacent",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "blockMath+blockMath",
|
|
7
|
-
message: `Adjacent math blocks:
|
|
8
|
-
combine the blocks between \\begin{align} and \\end{align}`,
|
|
9
|
-
}) as Rule;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-align-extra-break",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "blockMath",
|
|
7
|
-
pattern: /(\\{2,})\s*\\end{align}/,
|
|
8
|
-
message: `Extra space at end of block:
|
|
9
|
-
Don't end an align block with backslashes`,
|
|
10
|
-
}) as Rule;
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-align-linebreaks",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "blockMath",
|
|
7
|
-
// Match any align block with double backslashes in it
|
|
8
|
-
// Use [\s\S]* instead of .* so we match newlines as well.
|
|
9
|
-
pattern: /\\begin{align}[\s\S]*\\\\[\s\S]+\\end{align}/,
|
|
10
|
-
// Look for double backslashes and ensure that they are
|
|
11
|
-
// followed by optional space and another pair of backslashes.
|
|
12
|
-
// Note that this rule can't know where line breaks belong so
|
|
13
|
-
// it can't tell whether backslashes are completely missing. It just
|
|
14
|
-
// enforces that you don't have the wrong number of pairs of backslashes.
|
|
15
|
-
lint: function (state, content, nodes, match) {
|
|
16
|
-
let text = match[0];
|
|
17
|
-
while (text.length) {
|
|
18
|
-
const index = text.indexOf("\\\\");
|
|
19
|
-
if (index === -1) {
|
|
20
|
-
// No more backslash pairs, so we found no lint
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
text = text.substring(index + 2);
|
|
24
|
-
|
|
25
|
-
// Now we expect to find optional spaces, another pair of
|
|
26
|
-
// backslashes, and more optional spaces not followed immediately
|
|
27
|
-
// by another pair of backslashes.
|
|
28
|
-
const nextpair = text.match(/^\s*\\\\\s*(?!\\\\)/);
|
|
29
|
-
|
|
30
|
-
// If that does not match then we either have too few or too
|
|
31
|
-
// many pairs of backslashes.
|
|
32
|
-
if (!nextpair) {
|
|
33
|
-
return "Use four backslashes between lines of an align block";
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// If it did match, then, shorten the string and continue looping
|
|
37
|
-
// (because a single align block may have multiple lines that
|
|
38
|
-
// all must be separated by two sets of double backslashes).
|
|
39
|
-
text = text.substring(nextpair[0].length);
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
}) as Rule;
|
package/src/rules/math-empty.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-font-size",
|
|
5
|
-
severity: Rule.Severity.GUIDELINE,
|
|
6
|
-
selector: "math, blockMath",
|
|
7
|
-
pattern:
|
|
8
|
-
/\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
|
|
9
|
-
message: `Math font size:
|
|
10
|
-
Don't change the default font size with \\Large{} or similar commands`,
|
|
11
|
-
}) as Rule;
|
package/src/rules/math-frac.ts
DELETED
package/src/rules/math-nested.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-nested",
|
|
5
|
-
severity: Rule.Severity.ERROR,
|
|
6
|
-
selector: "math, blockMath",
|
|
7
|
-
pattern: /\\text{[^$}]*\$[^$}]*\$[^}]*}/,
|
|
8
|
-
message: `Nested math:
|
|
9
|
-
Don't nest math expressions inside \\text{} blocks`,
|
|
10
|
-
}) as Rule;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "math-starts-with-space",
|
|
5
|
-
severity: Rule.Severity.GUIDELINE,
|
|
6
|
-
selector: "math, blockMath",
|
|
7
|
-
pattern: /^\s*(~|\\qquad|\\quad|\\,|\\;|\\:|\\ |\\!|\\enspace|\\phantom)/,
|
|
8
|
-
message: `Math starts with space:
|
|
9
|
-
math should not be indented. Do not begin math expressions with
|
|
10
|
-
LaTeX space commands like ~, \\;, \\quad, or \\phantom`,
|
|
11
|
-
}) as Rule;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
// Because no selector is specified, this rule only applies to text nodes.
|
|
4
|
-
// Math and code hold their content directly and do not have text nodes
|
|
5
|
-
// beneath them (unlike the HTML DOM) so this rule automatically does not
|
|
6
|
-
// apply inside $$ or ``.
|
|
7
|
-
export default Rule.makeRule({
|
|
8
|
-
name: "math-without-dollars",
|
|
9
|
-
severity: Rule.Severity.GUIDELINE,
|
|
10
|
-
pattern: /\\\w+{[^}]*}|{|}/,
|
|
11
|
-
message: `This looks like LaTeX:
|
|
12
|
-
did you mean to put it inside dollar signs?`,
|
|
13
|
-
}) as Rule;
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "nested-lists",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "list list",
|
|
7
|
-
message: `Nested lists:
|
|
8
|
-
nested lists are hard to read on mobile devices;
|
|
9
|
-
do not use additional indentation.`,
|
|
10
|
-
}) as Rule;
|
package/src/rules/profanity.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "profanity",
|
|
5
|
-
// This list could obviously be expanded a lot, but I figured we
|
|
6
|
-
// could start with https://en.wikipedia.org/wiki/Seven_dirty_words
|
|
7
|
-
pattern: /\b(shit|piss|fuck|cunt|cocksucker|motherfucker|tits)\b/i,
|
|
8
|
-
message: "Avoid profanity",
|
|
9
|
-
}) as Rule;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
export default Rule.makeRule({
|
|
4
|
-
name: "table-missing-cells",
|
|
5
|
-
severity: Rule.Severity.WARNING,
|
|
6
|
-
selector: "table",
|
|
7
|
-
lint: function (state, content, nodes, match) {
|
|
8
|
-
const table = nodes[0];
|
|
9
|
-
const headerLength = table.header.length;
|
|
10
|
-
const rowLengths = table.cells.map((r) => r.length);
|
|
11
|
-
for (let r = 0; r < rowLengths.length; r++) {
|
|
12
|
-
if (rowLengths[r] !== headerLength) {
|
|
13
|
-
return `Table rows don't match header:
|
|
14
|
-
The table header has ${headerLength} cells, but
|
|
15
|
-
Row ${r + 1} has ${rowLengths[r]} cells.`;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
}) as Rule;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import Rule from "../rule";
|
|
2
|
-
|
|
3
|
-
// Because no selector is specified, this rule only applies to text nodes.
|
|
4
|
-
// Math and code hold their content directly and do not have text nodes
|
|
5
|
-
// beneath them (unlike the HTML DOM) so this rule automatically does not
|
|
6
|
-
// apply inside $$ or ``.
|
|
7
|
-
export default Rule.makeRule({
|
|
8
|
-
name: "unbalanced-code-delimiters",
|
|
9
|
-
severity: Rule.Severity.ERROR,
|
|
10
|
-
pattern: /[`~]+/,
|
|
11
|
-
message: `Unbalanced code delimiters:
|
|
12
|
-
code blocks should begin and end with the same type and number of delimiters`,
|
|
13
|
-
}) as Rule;
|