@khanacademy/perseus-linter 0.0.0-PR875-20240813215114 → 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/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.contentType === "article";
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[state.currentNode().id];
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 null;
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.contentType !== "exercise") {
1236
+ if (context?.contentType !== "exercise") {
1167
1237
  return;
1168
1238
  }
1169
1239
  if (context.stack.includes("hint")) {
1170
1240
  return;
1171
1241
  }
1172
- const widget = context?.widgets?.[state.currentNode().id];
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, MathFontSize, MathFrac, MathNested, MathStartsWithSpace, MathTextEmpty, NestedLists, StaticWidgetInQuestionStem, TableMissingCells, UnescapedDollar, WidgetInTable, MathWithoutDollars, UnbalancedCodeDelimiters, ImageSpacesAroundUrls, ImageWidget];
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.0.0";
1777
- perseusCore.addLibraryVersionToPerseusDebug(libName, libVersion);
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({