@khanacademy/perseus-linter 0.0.0-PR875-20240813215114 → 0.0.0-PR875-20250222011102
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/dist/es/index.js +138 -33
- package/dist/es/index.js.map +1 -1
- package/dist/index.js +135 -18
- 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 -37
- /package/dist/rules/{math-font-size.d.ts → image-url-empty.d.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -525,22 +525,27 @@ class SiblingCombinator extends SelectorCombinator {
|
|
|
525
525
|
// This represents the type returned by String.match(). It is an
|
|
526
526
|
// array of strings, but also has index:number and input:string properties.
|
|
527
527
|
// TypeScript doesn't handle it well, so we punt and just use any.
|
|
528
|
+
|
|
528
529
|
// This is the return type of the check() method of a Rule object
|
|
530
|
+
|
|
529
531
|
// This is the return type of the lint detection function passed as the 4th
|
|
530
532
|
// argument to the Rule() constructor. It can return null or a string or an
|
|
531
533
|
// object containing a string and two numbers.
|
|
532
534
|
// prettier-ignore
|
|
533
535
|
// (prettier formats this in a way that ka-lint does not like)
|
|
536
|
+
|
|
534
537
|
// This is the type of the lint detection function that the Rule() constructor
|
|
535
538
|
// expects as its fourth argument. It is passed the TraversalState object and
|
|
536
539
|
// content string that were passed to check(), and is also passed the array of
|
|
537
540
|
// nodes returned by the selector match and the array of strings returned by
|
|
538
541
|
// the pattern match. It should return null if no lint is detected or an
|
|
539
542
|
// error message or an object contining an error message.
|
|
543
|
+
|
|
540
544
|
// An optional check to verify whether or not a particular rule should
|
|
541
545
|
// be checked by context. For example, some rules only apply in exercises,
|
|
542
546
|
// and should never be applied to articles. Defaults to true, so if we
|
|
543
547
|
// omit the applies function in a rule, it'll be tested everywhere.
|
|
548
|
+
|
|
544
549
|
/**
|
|
545
550
|
* A Rule object describes a Perseus lint rule. See the comment at the top of
|
|
546
551
|
* this file for detailed description.
|
|
@@ -628,7 +633,6 @@ class Rule {
|
|
|
628
633
|
if (!error) {
|
|
629
634
|
return null; // No lint; we're done
|
|
630
635
|
}
|
|
631
|
-
|
|
632
636
|
if (typeof error === "string") {
|
|
633
637
|
// If the lint function returned a string we assume it
|
|
634
638
|
// applies to the entire content of the node and return it.
|
|
@@ -791,12 +795,61 @@ var DoubleSpacingAfterTerminal = Rule.makeRule({
|
|
|
791
795
|
any other kind of terminal punctuation.`
|
|
792
796
|
});
|
|
793
797
|
|
|
798
|
+
function buttonNotInButtonSet(name, set) {
|
|
799
|
+
return `Answer requires a button not found in the button sets: ${name} (in ${set})`;
|
|
800
|
+
}
|
|
801
|
+
const stringToButtonSet = {
|
|
802
|
+
"\\sqrt": "prealgebra",
|
|
803
|
+
"\\sin": "trig",
|
|
804
|
+
"\\cos": "trig",
|
|
805
|
+
"\\tan": "trig",
|
|
806
|
+
"\\log": "logarithms",
|
|
807
|
+
"\\ln": "logarithms"
|
|
808
|
+
};
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Rule to make sure that Expression questions that require
|
|
812
|
+
* a specific math symbol to answer have that math symbol
|
|
813
|
+
* available in the keypad (desktop learners can use a keyboard,
|
|
814
|
+
* but mobile learners must use the MathInput keypad)
|
|
815
|
+
*/
|
|
816
|
+
var ExpressionWidget = Rule.makeRule({
|
|
817
|
+
name: "expression-widget",
|
|
818
|
+
severity: Rule.Severity.WARNING,
|
|
819
|
+
selector: "widget",
|
|
820
|
+
lint: function (state, content, nodes, match, context) {
|
|
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?.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
|
+
|
|
794
847
|
var ExtraContentSpacing = Rule.makeRule({
|
|
795
848
|
name: "extra-content-spacing",
|
|
796
849
|
selector: "paragraph",
|
|
797
850
|
pattern: /\s+$/,
|
|
798
851
|
applies: function (context) {
|
|
799
|
-
return context
|
|
852
|
+
return context?.contentType === "article";
|
|
800
853
|
},
|
|
801
854
|
message: `No extra whitespace at the end of content blocks.`
|
|
802
855
|
});
|
|
@@ -961,6 +1014,28 @@ Whitespace in image URLs causes translation difficulties.`;
|
|
|
961
1014
|
}
|
|
962
1015
|
});
|
|
963
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
|
+
|
|
964
1039
|
// Normally we have one rule per file. But since our selector class
|
|
965
1040
|
// can't match specific widget types directly, this rule implements
|
|
966
1041
|
// a number of image widget related rules in one place. This should
|
|
@@ -976,9 +1051,13 @@ var ImageWidget = Rule.makeRule({
|
|
|
976
1051
|
if (state.currentNode().widgetType !== "image") {
|
|
977
1052
|
return;
|
|
978
1053
|
}
|
|
1054
|
+
const nodeId = state.currentNode().id;
|
|
1055
|
+
if (!nodeId) {
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
979
1058
|
|
|
980
1059
|
// If it can't find a definition for the widget it does nothing
|
|
981
|
-
const widget = context && context.widgets && context.widgets[
|
|
1060
|
+
const widget = context && context.widgets && context.widgets[nodeId];
|
|
982
1061
|
if (!widget) {
|
|
983
1062
|
return;
|
|
984
1063
|
}
|
|
@@ -1062,7 +1141,7 @@ var MathAlignLinebreaks = Rule.makeRule({
|
|
|
1062
1141
|
const index = text.indexOf("\\\\");
|
|
1063
1142
|
if (index === -1) {
|
|
1064
1143
|
// No more backslash pairs, so we found no lint
|
|
1065
|
-
return
|
|
1144
|
+
return;
|
|
1066
1145
|
}
|
|
1067
1146
|
text = text.substring(index + 2);
|
|
1068
1147
|
|
|
@@ -1093,15 +1172,6 @@ var MathEmpty = Rule.makeRule({
|
|
|
1093
1172
|
message: "Empty math: don't use $$ in your markdown."
|
|
1094
1173
|
});
|
|
1095
1174
|
|
|
1096
|
-
var MathFontSize = Rule.makeRule({
|
|
1097
|
-
name: "math-font-size",
|
|
1098
|
-
severity: Rule.Severity.GUIDELINE,
|
|
1099
|
-
selector: "math, blockMath",
|
|
1100
|
-
pattern: /\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
|
|
1101
|
-
message: `Math font size:
|
|
1102
|
-
Don't change the default font size with \\Large{} or similar commands`
|
|
1103
|
-
});
|
|
1104
|
-
|
|
1105
1175
|
var MathFrac = Rule.makeRule({
|
|
1106
1176
|
name: "math-frac",
|
|
1107
1177
|
severity: Rule.Severity.GUIDELINE,
|
|
@@ -1163,13 +1233,17 @@ var StaticWidgetInQuestionStem = Rule.makeRule({
|
|
|
1163
1233
|
severity: Rule.Severity.WARNING,
|
|
1164
1234
|
selector: "widget",
|
|
1165
1235
|
lint: (state, content, nodes, match, context) => {
|
|
1166
|
-
if (context
|
|
1236
|
+
if (context?.contentType !== "exercise") {
|
|
1167
1237
|
return;
|
|
1168
1238
|
}
|
|
1169
1239
|
if (context.stack.includes("hint")) {
|
|
1170
1240
|
return;
|
|
1171
1241
|
}
|
|
1172
|
-
const
|
|
1242
|
+
const nodeId = state.currentNode().id;
|
|
1243
|
+
if (!nodeId) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const widget = context?.widgets?.[nodeId];
|
|
1173
1247
|
if (!widget) {
|
|
1174
1248
|
return;
|
|
1175
1249
|
}
|
|
@@ -1226,7 +1300,7 @@ do not put widgets inside of tables.`
|
|
|
1226
1300
|
});
|
|
1227
1301
|
|
|
1228
1302
|
// TODO(davidflanagan):
|
|
1229
|
-
var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAfterTerminal, ExtraContentSpacing, HeadingLevel1, HeadingLevelSkip, HeadingSentenceCase, HeadingTitleCase, ImageAltText, ImageInTable, LinkClickHere, LongParagraph, MathAdjacent, MathAlignExtraBreak, MathAlignLinebreaks, MathEmpty,
|
|
1303
|
+
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];
|
|
1230
1304
|
|
|
1231
1305
|
/**
|
|
1232
1306
|
* TreeTransformer is a class for traversing and transforming trees. Create a
|
|
@@ -1288,9 +1362,11 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
|
|
|
1288
1362
|
|
|
1289
1363
|
// TreeNode is the type of a node in a parse tree. The only real requirement is
|
|
1290
1364
|
// that every node has a string-valued `type` property
|
|
1365
|
+
|
|
1291
1366
|
// TraversalCallback is the type of the callback function passed to the
|
|
1292
1367
|
// traverse() method. It is invoked with node, state, and content arguments
|
|
1293
1368
|
// and is expected to return nothing.
|
|
1369
|
+
|
|
1294
1370
|
// This is the TreeTransformer class described in detail at the
|
|
1295
1371
|
// top of this file.
|
|
1296
1372
|
class TreeTransformer {
|
|
@@ -1771,10 +1847,51 @@ class Stack {
|
|
|
1771
1847
|
}
|
|
1772
1848
|
}
|
|
1773
1849
|
|
|
1850
|
+
/**
|
|
1851
|
+
* Adds the given perseus library version information to the __perseus_debug__
|
|
1852
|
+
* object and ensures that the object is attached to `globalThis` (`window` in
|
|
1853
|
+
* browser environments).
|
|
1854
|
+
*
|
|
1855
|
+
* This allows each library to provide runtime version information to assist in
|
|
1856
|
+
* debugging in production environments.
|
|
1857
|
+
*/
|
|
1858
|
+
const addLibraryVersionToPerseusDebug = (libraryName, libraryVersion) => {
|
|
1859
|
+
// If the library version is the default value, then we don't want to
|
|
1860
|
+
// prefix it with a "v" to indicate that it is a version number.
|
|
1861
|
+
let prefix = "v";
|
|
1862
|
+
if (libraryVersion === "__lib_version__") {
|
|
1863
|
+
prefix = "";
|
|
1864
|
+
}
|
|
1865
|
+
const formattedVersion = `${prefix}${libraryVersion}`;
|
|
1866
|
+
if (typeof globalThis !== "undefined") {
|
|
1867
|
+
globalThis.__perseus_debug__ = globalThis.__perseus_debug__ ?? {};
|
|
1868
|
+
const existingVersionEntry = globalThis.__perseus_debug__[libraryName];
|
|
1869
|
+
if (existingVersionEntry) {
|
|
1870
|
+
// If we already have an entry and it doesn't match the registered
|
|
1871
|
+
// version, we morph the entry into an array and log a warning.
|
|
1872
|
+
if (existingVersionEntry !== formattedVersion) {
|
|
1873
|
+
// Existing entry might be an array already (oops, at least 2
|
|
1874
|
+
// versions of the library already loaded!).
|
|
1875
|
+
const allVersions = Array.isArray(existingVersionEntry) ? existingVersionEntry : [existingVersionEntry];
|
|
1876
|
+
allVersions.push(formattedVersion);
|
|
1877
|
+
globalThis.__perseus_debug__[libraryName] = allVersions;
|
|
1878
|
+
|
|
1879
|
+
// eslint-disable-next-line no-console
|
|
1880
|
+
console.warn(`Multiple versions of ${libraryName} loaded on this page: ${allVersions.sort().join(", ")}`);
|
|
1881
|
+
}
|
|
1882
|
+
} else {
|
|
1883
|
+
globalThis.__perseus_debug__[libraryName] = formattedVersion;
|
|
1884
|
+
}
|
|
1885
|
+
} else {
|
|
1886
|
+
// eslint-disable-next-line no-console
|
|
1887
|
+
console.warn(`globalThis not found found (${formattedVersion})`);
|
|
1888
|
+
}
|
|
1889
|
+
};
|
|
1890
|
+
|
|
1774
1891
|
// This file is processed by a Rollup plugin (replace) to inject the production
|
|
1775
1892
|
const libName = "@khanacademy/perseus-linter";
|
|
1776
|
-
const libVersion = "1.
|
|
1777
|
-
|
|
1893
|
+
const libVersion = "1.2.18";
|
|
1894
|
+
addLibraryVersionToPerseusDebug(libName, libVersion);
|
|
1778
1895
|
|
|
1779
1896
|
// Define the shape of the linter context object that is passed through the
|
|
1780
1897
|
const linterContextProps = PropTypes__default["default"].shape({
|