@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 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/dist/es/index.js CHANGED
@@ -1,21 +1,7 @@
1
- import { PerseusError, Errors, addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-core';
1
+ import _extends from '@babel/runtime/helpers/extends';
2
+ import { PerseusError, Errors } from '@khanacademy/perseus-core';
2
3
  import PropTypes from 'prop-types';
3
4
 
4
- function _extends() {
5
- _extends = Object.assign ? Object.assign.bind() : function (target) {
6
- for (var i = 1; i < arguments.length; i++) {
7
- var source = arguments[i];
8
- for (var key in source) {
9
- if (Object.prototype.hasOwnProperty.call(source, key)) {
10
- target[key] = source[key];
11
- }
12
- }
13
- }
14
- return target;
15
- };
16
- return _extends.apply(this, arguments);
17
- }
18
-
19
5
  /* eslint-disable no-useless-escape */
20
6
  /**
21
7
  * This is the base class for all Selector types. The key method that all
@@ -535,22 +521,27 @@ class SiblingCombinator extends SelectorCombinator {
535
521
  // This represents the type returned by String.match(). It is an
536
522
  // array of strings, but also has index:number and input:string properties.
537
523
  // TypeScript doesn't handle it well, so we punt and just use any.
524
+
538
525
  // This is the return type of the check() method of a Rule object
526
+
539
527
  // This is the return type of the lint detection function passed as the 4th
540
528
  // argument to the Rule() constructor. It can return null or a string or an
541
529
  // object containing a string and two numbers.
542
530
  // prettier-ignore
543
531
  // (prettier formats this in a way that ka-lint does not like)
532
+
544
533
  // This is the type of the lint detection function that the Rule() constructor
545
534
  // expects as its fourth argument. It is passed the TraversalState object and
546
535
  // content string that were passed to check(), and is also passed the array of
547
536
  // nodes returned by the selector match and the array of strings returned by
548
537
  // the pattern match. It should return null if no lint is detected or an
549
538
  // error message or an object contining an error message.
539
+
550
540
  // An optional check to verify whether or not a particular rule should
551
541
  // be checked by context. For example, some rules only apply in exercises,
552
542
  // and should never be applied to articles. Defaults to true, so if we
553
543
  // omit the applies function in a rule, it'll be tested everywhere.
544
+
554
545
  /**
555
546
  * A Rule object describes a Perseus lint rule. See the comment at the top of
556
547
  * this file for detailed description.
@@ -639,7 +630,6 @@ class Rule {
639
630
  if (!error) {
640
631
  return null; // No lint; we're done
641
632
  }
642
-
643
633
  if (typeof error === "string") {
644
634
  // If the lint function returned a string we assume it
645
635
  // applies to the entire content of the node and return it.
@@ -804,12 +794,62 @@ var DoubleSpacingAfterTerminal = Rule.makeRule({
804
794
  any other kind of terminal punctuation.`
805
795
  });
806
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
+
807
847
  var ExtraContentSpacing = Rule.makeRule({
808
848
  name: "extra-content-spacing",
809
849
  selector: "paragraph",
810
850
  pattern: /\s+$/,
811
851
  applies: function (context) {
812
- return context.contentType === "article";
852
+ return (context == null ? void 0 : context.contentType) === "article";
813
853
  },
814
854
  message: `No extra whitespace at the end of content blocks.`
815
855
  });
@@ -974,6 +1014,28 @@ Whitespace in image URLs causes translation difficulties.`;
974
1014
  }
975
1015
  });
976
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
+
977
1039
  // Normally we have one rule per file. But since our selector class
978
1040
  // can't match specific widget types directly, this rule implements
979
1041
  // a number of image widget related rules in one place. This should
@@ -989,9 +1051,13 @@ var ImageWidget = Rule.makeRule({
989
1051
  if (state.currentNode().widgetType !== "image") {
990
1052
  return;
991
1053
  }
1054
+ const nodeId = state.currentNode().id;
1055
+ if (!nodeId) {
1056
+ return;
1057
+ }
992
1058
 
993
1059
  // If it can't find a definition for the widget it does nothing
994
- const widget = context && context.widgets && context.widgets[state.currentNode().id];
1060
+ const widget = context && context.widgets && context.widgets[nodeId];
995
1061
  if (!widget) {
996
1062
  return;
997
1063
  }
@@ -1075,7 +1141,7 @@ var MathAlignLinebreaks = Rule.makeRule({
1075
1141
  const index = text.indexOf("\\\\");
1076
1142
  if (index === -1) {
1077
1143
  // No more backslash pairs, so we found no lint
1078
- return null;
1144
+ return;
1079
1145
  }
1080
1146
  text = text.substring(index + 2);
1081
1147
 
@@ -1106,15 +1172,6 @@ var MathEmpty = Rule.makeRule({
1106
1172
  message: "Empty math: don't use $$ in your markdown."
1107
1173
  });
1108
1174
 
1109
- var MathFontSize = Rule.makeRule({
1110
- name: "math-font-size",
1111
- severity: Rule.Severity.GUIDELINE,
1112
- selector: "math, blockMath",
1113
- pattern: /\\(tiny|Tiny|small|large|Large|LARGE|huge|Huge|scriptsize|normalsize)\s*{/,
1114
- message: `Math font size:
1115
- Don't change the default font size with \\Large{} or similar commands`
1116
- });
1117
-
1118
1175
  var MathFrac = Rule.makeRule({
1119
1176
  name: "math-frac",
1120
1177
  severity: Rule.Severity.GUIDELINE,
@@ -1177,13 +1234,17 @@ var StaticWidgetInQuestionStem = Rule.makeRule({
1177
1234
  selector: "widget",
1178
1235
  lint: (state, content, nodes, match, context) => {
1179
1236
  var _context$widgets;
1180
- if (context.contentType !== "exercise") {
1237
+ if ((context == null ? void 0 : context.contentType) !== "exercise") {
1181
1238
  return;
1182
1239
  }
1183
1240
  if (context.stack.includes("hint")) {
1184
1241
  return;
1185
1242
  }
1186
- const widget = context == null || (_context$widgets = context.widgets) == null ? void 0 : _context$widgets[state.currentNode().id];
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];
1187
1248
  if (!widget) {
1188
1249
  return;
1189
1250
  }
@@ -1240,7 +1301,7 @@ do not put widgets inside of tables.`
1240
1301
  });
1241
1302
 
1242
1303
  // TODO(davidflanagan):
1243
- 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];
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];
1244
1305
 
1245
1306
  /**
1246
1307
  * TreeTransformer is a class for traversing and transforming trees. Create a
@@ -1302,9 +1363,11 @@ var AllRules = [AbsoluteUrl, BlockquotedMath, BlockquotedWidget, DoubleSpacingAf
1302
1363
 
1303
1364
  // TreeNode is the type of a node in a parse tree. The only real requirement is
1304
1365
  // that every node has a string-valued `type` property
1366
+
1305
1367
  // TraversalCallback is the type of the callback function passed to the
1306
1368
  // traverse() method. It is invoked with node, state, and content arguments
1307
1369
  // and is expected to return nothing.
1370
+
1308
1371
  // This is the TreeTransformer class described in detail at the
1309
1372
  // top of this file.
1310
1373
  class TreeTransformer {
@@ -1779,9 +1842,51 @@ class Stack {
1779
1842
  }
1780
1843
  }
1781
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
+
1782
1887
  // This file is processed by a Rollup plugin (replace) to inject the production
1783
1888
  const libName = "@khanacademy/perseus-linter";
1784
- const libVersion = "1.0.0";
1889
+ const libVersion = "1.2.18";
1785
1890
  addLibraryVersionToPerseusDebug(libName, libVersion);
1786
1891
 
1787
1892
  // Define the shape of the linter context object that is passed through the