@khanacademy/perseus-linter 0.0.0-PR862-20231207182234 → 0.0.0-PR875-20250221232857
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/LICENSE +18 -0
- package/README.md +7 -0
- package/dist/es/index.js +156 -38
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +181 -38
- package/dist/index.js.map +1 -1
- package/dist/proptypes.d.ts +1 -7
- package/dist/rule.d.ts +24 -8
- package/dist/rules/expression-widget.d.ts +9 -0
- package/dist/rules/lint-utils.d.ts +0 -1
- package/dist/shared-utils/add-library-version-to-perseus-debug.d.ts +9 -0
- package/dist/tree-transformer.d.ts +3 -1
- package/package.json +35 -35
- package/.eslintrc.js +0 -12
- package/CHANGELOG.md +0 -148
- 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
- /package/dist/rules/{math-font-size.d.ts → image-url-empty.d.ts} +0 -0
- /package/dist/rules/{profanity.d.ts → static-widget-in-question-stem.d.ts} +0 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright 2022 Khan Academy
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
5
|
+
the Software without restriction, including without limitation the rights to use,
|
|
6
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
|
7
|
+
Software, and to permit persons to whom the Software is furnished to do so, subject
|
|
8
|
+
to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
17
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
18
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
package/dist/es/index.js
CHANGED
|
@@ -1,22 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import _extends from '@babel/runtime/helpers/extends';
|
|
2
|
+
import { PerseusError, Errors } from '@khanacademy/perseus-core';
|
|
3
3
|
import PropTypes from 'prop-types';
|
|
4
4
|
|
|
5
|
-
function _extends() {
|
|
6
|
-
_extends = Object.assign ? Object.assign.bind() : function (target) {
|
|
7
|
-
for (var i = 1; i < arguments.length; i++) {
|
|
8
|
-
var source = arguments[i];
|
|
9
|
-
for (var key in source) {
|
|
10
|
-
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
11
|
-
target[key] = source[key];
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
return target;
|
|
16
|
-
};
|
|
17
|
-
return _extends.apply(this, arguments);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
5
|
/* eslint-disable no-useless-escape */
|
|
21
6
|
/**
|
|
22
7
|
* This is the base class for all Selector types. The key method that all
|
|
@@ -536,22 +521,27 @@ class SiblingCombinator extends SelectorCombinator {
|
|
|
536
521
|
// This represents the type returned by String.match(). It is an
|
|
537
522
|
// array of strings, but also has index:number and input:string properties.
|
|
538
523
|
// TypeScript doesn't handle it well, so we punt and just use any.
|
|
524
|
+
|
|
539
525
|
// This is the return type of the check() method of a Rule object
|
|
526
|
+
|
|
540
527
|
// This is the return type of the lint detection function passed as the 4th
|
|
541
528
|
// argument to the Rule() constructor. It can return null or a string or an
|
|
542
529
|
// object containing a string and two numbers.
|
|
543
530
|
// prettier-ignore
|
|
544
531
|
// (prettier formats this in a way that ka-lint does not like)
|
|
532
|
+
|
|
545
533
|
// This is the type of the lint detection function that the Rule() constructor
|
|
546
534
|
// expects as its fourth argument. It is passed the TraversalState object and
|
|
547
535
|
// content string that were passed to check(), and is also passed the array of
|
|
548
536
|
// nodes returned by the selector match and the array of strings returned by
|
|
549
537
|
// the pattern match. It should return null if no lint is detected or an
|
|
550
538
|
// error message or an object contining an error message.
|
|
539
|
+
|
|
551
540
|
// An optional check to verify whether or not a particular rule should
|
|
552
541
|
// be checked by context. For example, some rules only apply in exercises,
|
|
553
542
|
// and should never be applied to articles. Defaults to true, so if we
|
|
554
543
|
// omit the applies function in a rule, it'll be tested everywhere.
|
|
544
|
+
|
|
555
545
|
/**
|
|
556
546
|
* A Rule object describes a Perseus lint rule. See the comment at the top of
|
|
557
547
|
* this file for detailed description.
|
|
@@ -640,7 +630,6 @@ class Rule {
|
|
|
640
630
|
if (!error) {
|
|
641
631
|
return null; // No lint; we're done
|
|
642
632
|
}
|
|
643
|
-
|
|
644
633
|
if (typeof error === "string") {
|
|
645
634
|
// If the lint function returned a string we assume it
|
|
646
635
|
// applies to the entire content of the node and return it.
|
|
@@ -805,12 +794,62 @@ var DoubleSpacingAfterTerminal = Rule.makeRule({
|
|
|
805
794
|
any other kind of terminal punctuation.`
|
|
806
795
|
});
|
|
807
796
|
|
|
797
|
+
function buttonNotInButtonSet(name, set) {
|
|
798
|
+
return `Answer requires a button not found in the button sets: ${name} (in ${set})`;
|
|
799
|
+
}
|
|
800
|
+
const stringToButtonSet = {
|
|
801
|
+
"\\sqrt": "prealgebra",
|
|
802
|
+
"\\sin": "trig",
|
|
803
|
+
"\\cos": "trig",
|
|
804
|
+
"\\tan": "trig",
|
|
805
|
+
"\\log": "logarithms",
|
|
806
|
+
"\\ln": "logarithms"
|
|
807
|
+
};
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Rule to make sure that Expression questions that require
|
|
811
|
+
* a specific math symbol to answer have that math symbol
|
|
812
|
+
* available in the keypad (desktop learners can use a keyboard,
|
|
813
|
+
* but mobile learners must use the MathInput keypad)
|
|
814
|
+
*/
|
|
815
|
+
var ExpressionWidget = Rule.makeRule({
|
|
816
|
+
name: "expression-widget",
|
|
817
|
+
severity: Rule.Severity.WARNING,
|
|
818
|
+
selector: "widget",
|
|
819
|
+
lint: function (state, content, nodes, match, context) {
|
|
820
|
+
var _context$widgets;
|
|
821
|
+
// This rule only looks at image widgets
|
|
822
|
+
if (state.currentNode().widgetType !== "expression") {
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
const nodeId = state.currentNode().id;
|
|
826
|
+
if (!nodeId) {
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// If it can't find a definition for the widget it does nothing
|
|
831
|
+
const widget = context == null || (_context$widgets = context.widgets) == null ? void 0 : _context$widgets[nodeId];
|
|
832
|
+
if (!widget) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const answers = widget.options.answerForms;
|
|
836
|
+
const buttons = widget.options.buttonSets;
|
|
837
|
+
for (const answer of answers) {
|
|
838
|
+
for (const [str, set] of Object.entries(stringToButtonSet)) {
|
|
839
|
+
if (answer.value.includes(str) && !buttons.includes(set)) {
|
|
840
|
+
return buttonNotInButtonSet(str, set);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
808
847
|
var ExtraContentSpacing = Rule.makeRule({
|
|
809
848
|
name: "extra-content-spacing",
|
|
810
849
|
selector: "paragraph",
|
|
811
850
|
pattern: /\s+$/,
|
|
812
851
|
applies: function (context) {
|
|
813
|
-
return context.contentType === "article";
|
|
852
|
+
return (context == null ? void 0 : context.contentType) === "article";
|
|
814
853
|
},
|
|
815
854
|
message: `No extra whitespace at the end of content blocks.`
|
|
816
855
|
});
|
|
@@ -975,6 +1014,28 @@ Whitespace in image URLs causes translation difficulties.`;
|
|
|
975
1014
|
}
|
|
976
1015
|
});
|
|
977
1016
|
|
|
1017
|
+
var ImageUrlEmpty = Rule.makeRule({
|
|
1018
|
+
name: "image-url-empty",
|
|
1019
|
+
severity: Rule.Severity.ERROR,
|
|
1020
|
+
selector: "image",
|
|
1021
|
+
lint: function (state, content, nodes) {
|
|
1022
|
+
const image = nodes[0];
|
|
1023
|
+
const url = image.target;
|
|
1024
|
+
|
|
1025
|
+
// If no URL is provided, an infinite spinner will be shown in articles
|
|
1026
|
+
// overlaying the page where the image should be. This prevents the page
|
|
1027
|
+
// from fully loading. As a result, we check for URLS with all images.
|
|
1028
|
+
if (!url || !url.trim()) {
|
|
1029
|
+
return "Images should have a URL";
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// NOTE(TB): Ideally there would be a check to confirm the URL works
|
|
1033
|
+
// and leads to a valid resource, but fetching the URL would require
|
|
1034
|
+
// linting to be able to handle async functions, which it currently
|
|
1035
|
+
// cannot do.
|
|
1036
|
+
}
|
|
1037
|
+
});
|
|
1038
|
+
|
|
978
1039
|
// Normally we have one rule per file. But since our selector class
|
|
979
1040
|
// can't match specific widget types directly, this rule implements
|
|
980
1041
|
// a number of image widget related rules in one place. This should
|
|
@@ -990,9 +1051,13 @@ var ImageWidget = Rule.makeRule({
|
|
|
990
1051
|
if (state.currentNode().widgetType !== "image") {
|
|
991
1052
|
return;
|
|
992
1053
|
}
|
|
1054
|
+
const nodeId = state.currentNode().id;
|
|
1055
|
+
if (!nodeId) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
993
1058
|
|
|
994
1059
|
// If it can't find a definition for the widget it does nothing
|
|
995
|
-
const widget = context && context.widgets && context.widgets[
|
|
1060
|
+
const widget = context && context.widgets && context.widgets[nodeId];
|
|
996
1061
|
if (!widget) {
|
|
997
1062
|
return;
|
|
998
1063
|
}
|
|
@@ -1076,7 +1141,7 @@ var MathAlignLinebreaks = Rule.makeRule({
|
|
|
1076
1141
|
const index = text.indexOf("\\\\");
|
|
1077
1142
|
if (index === -1) {
|
|
1078
1143
|
// No more backslash pairs, so we found no lint
|
|
1079
|
-
return
|
|
1144
|
+
return;
|
|
1080
1145
|
}
|
|
1081
1146
|
text = text.substring(index + 2);
|
|
1082
1147
|
|
|
@@ -1107,15 +1172,6 @@ var MathEmpty = Rule.makeRule({
|
|
|
1107
1172
|
message: "Empty math: don't use $$ in your markdown."
|
|
1108
1173
|
});
|
|
1109
1174
|
|
|
1110
|
-
var MathFontSize = Rule.makeRule({
|
|
1111
|
-
name: "math-font-size",
|
|
1112
|
-
severity: Rule.Severity.GUIDELINE,
|
|
1113
|
-
selector: "math, blockMath",
|
|
1114
|
-
pattern: /\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
|
|
1115
|
-
message: `Math font size:
|
|
1116
|
-
Don't change the default font size with \\Large{} or similar commands`
|
|
1117
|
-
});
|
|
1118
|
-
|
|
1119
1175
|
var MathFrac = Rule.makeRule({
|
|
1120
1176
|
name: "math-frac",
|
|
1121
1177
|
severity: Rule.Severity.GUIDELINE,
|
|
@@ -1172,12 +1228,30 @@ nested lists are hard to read on mobile devices;
|
|
|
1172
1228
|
do not use additional indentation.`
|
|
1173
1229
|
});
|
|
1174
1230
|
|
|
1175
|
-
var
|
|
1176
|
-
name: "
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1231
|
+
var StaticWidgetInQuestionStem = Rule.makeRule({
|
|
1232
|
+
name: "static-widget-in-question-stem",
|
|
1233
|
+
severity: Rule.Severity.WARNING,
|
|
1234
|
+
selector: "widget",
|
|
1235
|
+
lint: (state, content, nodes, match, context) => {
|
|
1236
|
+
var _context$widgets;
|
|
1237
|
+
if ((context == null ? void 0 : context.contentType) !== "exercise") {
|
|
1238
|
+
return;
|
|
1239
|
+
}
|
|
1240
|
+
if (context.stack.includes("hint")) {
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const nodeId = state.currentNode().id;
|
|
1244
|
+
if (!nodeId) {
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const widget = context == null || (_context$widgets = context.widgets) == null ? void 0 : _context$widgets[nodeId];
|
|
1248
|
+
if (!widget) {
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
if (widget.static) {
|
|
1252
|
+
return `Widget in question stem is static (non-interactive).`;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1181
1255
|
});
|
|
1182
1256
|
|
|
1183
1257
|
var TableMissingCells = Rule.makeRule({
|
|
@@ -1227,7 +1301,7 @@ do not put widgets inside of tables.`
|
|
|
1227
1301
|
});
|
|
1228
1302
|
|
|
1229
1303
|
// TODO(davidflanagan):
|
|
1230
|
-
var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAfterTerminal, ExtraContentSpacing, HeadingLevel1, HeadingLevelSkip, HeadingSentenceCase, HeadingTitleCase, ImageAltText, ImageInTable, LinkClickHere, LongParagraph, MathAdjacent, MathAlignExtraBreak, MathAlignLinebreaks, MathEmpty,
|
|
1304
|
+
var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAfterTerminal, ImageUrlEmpty, ExpressionWidget, ExtraContentSpacing, HeadingLevel1, HeadingLevelSkip, HeadingSentenceCase, HeadingTitleCase, ImageAltText, ImageInTable, LinkClickHere, LongParagraph, MathAdjacent, MathAlignExtraBreak, MathAlignLinebreaks, MathEmpty, MathFrac, MathNested, MathStartsWithSpace, MathTextEmpty, NestedLists, StaticWidgetInQuestionStem, TableMissingCells, UnescapedDollar, WidgetInTable, MathWithoutDollars, UnbalancedCodeDelimiters, ImageSpacesAroundUrls, ImageWidget];
|
|
1231
1305
|
|
|
1232
1306
|
/**
|
|
1233
1307
|
* TreeTransformer is a class for traversing and transforming trees. Create a
|
|
@@ -1289,9 +1363,11 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
|
|
|
1289
1363
|
|
|
1290
1364
|
// TreeNode is the type of a node in a parse tree. The only real requirement is
|
|
1291
1365
|
// that every node has a string-valued `type` property
|
|
1366
|
+
|
|
1292
1367
|
// TraversalCallback is the type of the callback function passed to the
|
|
1293
1368
|
// traverse() method. It is invoked with node, state, and content arguments
|
|
1294
1369
|
// and is expected to return nothing.
|
|
1370
|
+
|
|
1295
1371
|
// This is the TreeTransformer class described in detail at the
|
|
1296
1372
|
// top of this file.
|
|
1297
1373
|
class TreeTransformer {
|
|
@@ -1766,9 +1842,51 @@ class Stack {
|
|
|
1766
1842
|
}
|
|
1767
1843
|
}
|
|
1768
1844
|
|
|
1845
|
+
/**
|
|
1846
|
+
* Adds the given perseus library version information to the __perseus_debug__
|
|
1847
|
+
* object and ensures that the object is attached to `globalThis` (`window` in
|
|
1848
|
+
* browser environments).
|
|
1849
|
+
*
|
|
1850
|
+
* This allows each library to provide runtime version information to assist in
|
|
1851
|
+
* debugging in production environments.
|
|
1852
|
+
*/
|
|
1853
|
+
const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
|
|
1854
|
+
// If the library version is the default value, then we don't want to
|
|
1855
|
+
// prefix it with a "v" to indicate that it is a version number.
|
|
1856
|
+
let prefix = "v";
|
|
1857
|
+
if (libraryVersion === "__lib_version__") {
|
|
1858
|
+
prefix = "";
|
|
1859
|
+
}
|
|
1860
|
+
const formattedVersion = `${prefix}${libraryVersion}`;
|
|
1861
|
+
if (typeof globalThis !== "undefined") {
|
|
1862
|
+
var _globalThis$__perseus;
|
|
1863
|
+
globalThis.__perseus_debug__ = (_globalThis$__perseus = globalThis.__perseus_debug__) != null ? _globalThis$__perseus : {};
|
|
1864
|
+
const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
|
|
1865
|
+
if (existingVersionEntry) {
|
|
1866
|
+
// If we already have an entry and it doesn't match the registered
|
|
1867
|
+
// version, we morph the entry into an array and log a warning.
|
|
1868
|
+
if (existingVersionEntry !== formattedVersion) {
|
|
1869
|
+
// Existing entry might be an array already (oops, at least 2
|
|
1870
|
+
// versions of the library already loaded!).
|
|
1871
|
+
const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
|
|
1872
|
+
allVersions.push(formattedVersion);
|
|
1873
|
+
globalThis.__perseus_debug__[libraryName] = allVersions;
|
|
1874
|
+
|
|
1875
|
+
// eslint-disable-next-line no-console
|
|
1876
|
+
console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
|
|
1877
|
+
}
|
|
1878
|
+
} else {
|
|
1879
|
+
globalThis.__perseus_debug__[libraryName] = formattedVersion;
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
// eslint-disable-next-line no-console
|
|
1883
|
+
console.warn(`globalThis not found found (${formattedVersion})`);
|
|
1884
|
+
}
|
|
1885
|
+
};
|
|
1886
|
+
|
|
1769
1887
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
1770
1888
|
const libName = "@khanacademy/perseus-linter";
|
|
1771
|
-
const libVersion = "
|
|
1889
|
+
const libVersion = "1.2.18";
|
|
1772
1890
|
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
1773
1891
|
|
|
1774
1892
|
// Define the shape of the linter context object that is passed through the
|