@khanacademy/perseus-linter 5.4.4 → 6.0.0

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Linter engine for Perseus",
4
4
  "author": "Khan Academy",
5
5
  "license": "MIT",
6
- "version": "5.4.4",
6
+ "version": "6.0.0",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -15,21 +15,22 @@
15
15
  "bugs": {
16
16
  "url": "https://github.com/Khan/perseus/issues"
17
17
  },
18
- "module": "dist/es/index.js",
19
- "main": "dist/index.js",
20
- "source": "src/index.ts",
18
+ "type": "module",
19
+ "exports": {
20
+ ".": "./dist/index.js"
21
+ },
21
22
  "files": [
22
23
  "dist"
23
24
  ],
24
25
  "dependencies": {
25
- "@khanacademy/kas": "2.2.6",
26
- "@khanacademy/kmath": "2.4.54",
27
- "@khanacademy/perseus-core": "39.2.3",
28
- "@khanacademy/perseus-utils": "2.1.5",
29
- "@khanacademy/pure-markdown": "2.3.2"
26
+ "@khanacademy/kas": "3.0.0",
27
+ "@khanacademy/kmath": "3.0.0",
28
+ "@khanacademy/perseus-core": "40.0.0",
29
+ "@khanacademy/perseus-utils": "3.0.0",
30
+ "@khanacademy/pure-markdown": "3.0.0"
30
31
  },
31
32
  "devDependencies": {
32
- "perseus-build-settings": "0.9.1"
33
+ "perseus-build-settings": "0.9.2"
33
34
  },
34
35
  "keywords": [],
35
36
  "khan": {
package/dist/es/index.js DELETED
@@ -1,172 +0,0 @@
1
- import { addLibraryVersionToPerseusDebug } from '@khanacademy/perseus-utils';
2
- import { PerseusError, Errors, CoreWidgetRegistry, makeSafeUrl, SORTER_MAX_CARDS } from '@khanacademy/perseus-core';
3
- import * as KAS from '@khanacademy/kas';
4
- import { parse, traverseContent } from '@khanacademy/pure-markdown';
5
- import { vector } from '@khanacademy/kmath';
6
-
7
- const libName="@khanacademy/perseus-linter";const libVersion="5.4.4";addLibraryVersionToPerseusDebug(libName,libVersion);
8
-
9
- const linterContextDefault={contentType:"",highlightLint:false,stack:[]};
10
-
11
- class Selector{static parse(selectorText){return new Parser(selectorText).parse()}match(state){throw new PerseusError("Selector subclasses must implement match()",Errors.NotAllowed)}toString(){return "Unknown selector class"}}class Parser{nextToken(){return this.tokens[this.tokenIndex]||""}consume(){this.tokenIndex++;}isIdentifier(){const c=this.tokens[this.tokenIndex][0];return c>="a"&&c<="z"||c>="A"&&c<="Z"}skipSpace(){while(this.nextToken()===" "){this.consume();}}parse(){const ts=this.parseTreeSelector();let token=this.nextToken();if(!token){return ts}const treeSelectors=[ts];while(token){if(token===","){this.consume();}else {throw new ParseError("Expected comma")}treeSelectors.push(this.parseTreeSelector());token=this.nextToken();}return new SelectorList(treeSelectors)}parseTreeSelector(){this.skipSpace();let ns=this.parseNodeSelector();for(;;){const token=this.nextToken();if(!token||token===","){break}else if(token===" "){this.consume();ns=new AncestorCombinator(ns,this.parseNodeSelector());}else if(token===">"){this.consume();ns=new ParentCombinator(ns,this.parseNodeSelector());}else if(token==="+"){this.consume();ns=new PreviousCombinator(ns,this.parseNodeSelector());}else if(token==="~"){this.consume();ns=new SiblingCombinator(ns,this.parseNodeSelector());}else {throw new ParseError("Unexpected token: "+token)}}return ns}parseNodeSelector(){this.skipSpace();const t=this.nextToken();if(t==="*"){this.consume();return new AnyNode}if(this.isIdentifier()){this.consume();return new TypeSelector(t)}throw new ParseError("Expected node type")}constructor(s){s=s.trim().replace(/\s+/g," ");this.tokens=s.match(Parser.TOKENS)||[];this.tokenIndex=0;}}Parser.TOKENS=/([a-zA-Z][\w-]*)|(\d+)|[^\s]|(\s(?=[a-zA-Z*]))/g;class ParseError extends Error{constructor(message){super(message);}}class SelectorList extends Selector{match(state){for(let i=0;i<this.selectors.length;i++){const s=this.selectors[i];const result=s.match(state);if(result){return result}}return null}toString(){let result="";for(let i=0;i<this.selectors.length;i++){result+=i>0?", ":"";result+=this.selectors[i].toString();}return result}constructor(selectors){super();this.selectors=selectors;}}class AnyNode extends Selector{match(state){return [state.currentNode()]}toString(){return "*"}}class TypeSelector extends Selector{match(state){const node=state.currentNode();if(node.type===this.type){return [node]}return null}toString(){return this.type}constructor(type){super();this.type=type;}}class SelectorCombinator extends Selector{constructor(left,right){super();this.left=left;this.right=right;}}class AncestorCombinator extends SelectorCombinator{match(state){const rightResult=this.right.match(state);if(rightResult){state=state.clone();while(state.hasParent()){state.goToParent();const leftResult=this.left.match(state);if(leftResult){return leftResult.concat(rightResult)}}}return null}toString(){return this.left.toString()+" "+this.right.toString()}constructor(left,right){super(left,right);}}class ParentCombinator extends SelectorCombinator{match(state){const rightResult=this.right.match(state);if(rightResult){if(state.hasParent()){state=state.clone();state.goToParent();const leftResult=this.left.match(state);if(leftResult){return leftResult.concat(rightResult)}}}return null}toString(){return this.left.toString()+" > "+this.right.toString()}constructor(left,right){super(left,right);}}class PreviousCombinator extends SelectorCombinator{match(state){const rightResult=this.right.match(state);if(rightResult){if(state.hasPreviousSibling()){state=state.clone();state.goToPreviousSibling();const leftResult=this.left.match(state);if(leftResult){return leftResult.concat(rightResult)}}}return null}toString(){return this.left.toString()+" + "+this.right.toString()}constructor(left,right){super(left,right);}}class SiblingCombinator extends SelectorCombinator{match(state){const rightResult=this.right.match(state);if(rightResult){state=state.clone();while(state.hasPreviousSibling()){state.goToPreviousSibling();const leftResult=this.left.match(state);if(leftResult){return leftResult.concat(rightResult)}}}return null}toString(){return this.left.toString()+" ~ "+this.right.toString()}constructor(left,right){super(left,right);}}
12
-
13
- class Rule{static makeRule(options){return new Rule(options.name,options.severity,options.selector?Selector.parse(options.selector):null,Rule.makePattern(options.pattern),options.lint||options.message,options.applies)}check(node,traversalState,content,context){const selectorMatch=this.selector.match(traversalState);if(!selectorMatch){return null}let patternMatch;if(this.pattern){patternMatch=content.match(this.pattern);}else {patternMatch=Rule.FakePatternMatch(content,content,0);}if(!patternMatch){return null}try{const error=this.lint(traversalState,content,selectorMatch,patternMatch,context);if(!error){return null}if(typeof error==="string"){return {rule:this.name,severity:this.severity,message:error,start:0,end:content.length}}return {rule:this.name,severity:this.severity,message:error.message,start:error.start,end:error.end,metadata:error.metadata}}catch(e){return {rule:"lint-rule-failure",severity:this.severity,message:`Exception in rule ${this.name}: ${e.message}
14
- Stack trace:
15
- ${e.stack}`,start:0,end:content.length}}}_defaultLintFunction(state,content,selectorMatch,patternMatch,context){return {message:this.message||"",start:patternMatch.index,end:patternMatch.index+patternMatch[0].length}}static makePattern(pattern){if(!pattern){return null}if(pattern instanceof RegExp){return pattern}if(pattern[0]==="/"){const lastSlash=pattern.lastIndexOf("/");const expression=pattern.substring(1,lastSlash);const flags=pattern.substring(lastSlash+1);return new RegExp(expression,flags)}return new RegExp(pattern)}static FakePatternMatch(input,match,index){const result=[match];result.index=index;result.input=input;return result}constructor(name,severity,selector,pattern,lint,applies){if(!selector&&!pattern){throw new PerseusError("Lint rules must have a selector or pattern",Errors.InvalidInput,{metadata:{name}})}this.name=name||"unnamed rule";this.severity=severity||Rule.Severity.BULK_WARNING;this.selector=selector||Rule.DEFAULT_SELECTOR;this.pattern=pattern||null;if(typeof lint==="function"){this.lint=lint;this.message=null;}else {this.lint=(...args)=>this._defaultLintFunction(...args);this.message=lint;}this.applies=applies||function(){return true};}}Rule.Severity={ERROR:1,WARNING:2,GUIDELINE:3,BULK_WARNING:4};Rule.DEFAULT_SELECTOR=Selector.parse("text");
16
-
17
- const HOSTNAME=/\/\/([^/]+)/;function getHostname(url){if(!url){return ""}const match=url.match(HOSTNAME);return match?match[1]:""}
18
-
19
- var AbsoluteUrl = Rule.makeRule({name:"absolute-url",severity:Rule.Severity.GUIDELINE,selector:"link, image",lint:function(state,content,nodes,match){const url=nodes[0].target;const hostname=getHostname(url);if(hostname==="khanacademy.org"||hostname.endsWith(".khanacademy.org")){return `Don't use absolute URLs:
20
- When linking to KA content or images, omit the
21
- https://www.khanacademy.org URL prefix.
22
- Use a relative URL beginning with / instead.`}}});
23
-
24
- var DoubleSpacingAfterTerminal = Rule.makeRule({name:"double-spacing-after-terminal",severity:Rule.Severity.BULK_WARNING,selector:"paragraph",pattern:/[.!?] {2}/i,message:`Use a single space after a sentence-ending period, or
25
- any other kind of terminal punctuation.`});
26
-
27
- function buttonNotInButtonSet(name,set){return `Answer requires a button not found in the button sets: ${name} (in ${set})`}const stringToButtonSet={"\\sqrt":"prealgebra","^":"prealgebra","\\sin":"trig","\\cos":"trig","\\tan":"trig","\\log":"logarithms","\\ln":"logarithms"};var ExpressionWidget = Rule.makeRule({name:"expression-widget",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="expression"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const answers=widget.options.answerForms;const buttons=widget.options.buttonSets;for(const answer of answers){for(const[str,set]of Object.entries(stringToButtonSet)){if(answer.value.includes(str)&&!buttons.includes(set)){return buttonNotInButtonSet(str,set)}}}}});
28
-
29
- var ExpressionWidgetError = Rule.makeRule({name:"expression-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="expression"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context&&context.widgets&&context.widgets[nodeId];if(!widget){return}const issues=[];const{answerForms,functions}=widget.options;if(answerForms.length===0){issues.push("No answers specified.");}else {const hasCorrect=answerForms.some(form=>{return form.considered==="correct"});if(!hasCorrect){issues.push("No correct answer specified.");}answerForms.forEach((form,ix)=>{if(form.value===""){issues.push(`Answer ${ix+1} is empty.`);}else {const expression=KAS.parse(form.value,{functions:functions});if(!expression.parsed){issues.push(`Couldn't parse ${form.value}.`);}else if(form.simplify&&!expression.expr.isSimplified()){issues.push(`${form.value} isn't simplified, but is required to be.`);}}});}const allWarningsString=issues.join("\n\n");return allWarningsString}});
30
-
31
- var ExtraContentSpacing = Rule.makeRule({name:"extra-content-spacing",selector:"paragraph",pattern:/\s+$/,applies:function(context){return context?.contentType==="article"},message:`No extra whitespace at the end of content blocks.`});
32
-
33
- const rWidgetRule=/\[\[\u2603 (([a-z-]+) ([0-9]+))\]\]/;var FreeResponseWidgetError = Rule.makeRule({name:"free-response-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="free-response"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context&&context.widgets&&context.widgets[nodeId];if(!widget){return}const question=widget.options.question;if(!question){return "The question is empty"}if(question.match(rWidgetRule)!=null){return "The question contains a widget"}}});
34
-
35
- var HeadingLevel1 = Rule.makeRule({name:"heading-level-1",severity:Rule.Severity.WARNING,selector:"heading",lint:function(state,content,nodes,match){if(nodes[0].level===1){return `Don't use level-1 headings:
36
- Begin headings with two or more # characters.`}}});
37
-
38
- var HeadingLevelSkip = Rule.makeRule({name:"heading-level-skip",severity:Rule.Severity.WARNING,selector:"heading ~ heading",lint:function(state,content,nodes,match){const currentHeading=nodes[1];const previousHeading=nodes[0];if(currentHeading.level>previousHeading.level+1){return `Skipped heading level:
39
- this heading is level ${currentHeading.level} but
40
- the previous heading was level ${previousHeading.level}`}}});
41
-
42
- var HeadingSentenceCase = Rule.makeRule({name:"heading-sentence-case",severity:Rule.Severity.GUIDELINE,selector:"heading",pattern:/^\W*[a-z]/,message:`First letter is lowercase:
43
- the first letter of a heading should be capitalized.`});
44
-
45
- const littleWords={and:true,nor:true,but:true,the:true,for:true};function isCapitalized(word){const c=word[0];return c===c.toUpperCase()}var HeadingTitleCase = Rule.makeRule({name:"heading-title-case",severity:Rule.Severity.GUIDELINE,selector:"heading",pattern:/[^\s:]\s+[A-Z]+[a-z]/,locale:"en",lint:function(state,content,nodes,match){const heading=content.trim();let words=heading.split(/\s+/);words.shift();words=words.filter(w=>w.length>2&&!littleWords.hasOwnProperty(w));if(words.length>=3&&words.every(w=>isCapitalized(w))){return `Title-case heading:
46
- This heading appears to be in title-case, but should be sentence-case.
47
- Only capitalize the first letter and proper nouns.`}}});
48
-
49
- var ImageAltText = Rule.makeRule({name:"image-alt-text",severity:Rule.Severity.WARNING,selector:"image",lint:function(state,content,nodes,match){const image=nodes[0];if(!image.alt||!image.alt.trim()){return `Images should have alt text:
50
- for accessibility, all images should have alt text.
51
- Specify alt text inside square brackets after the !.`}if(image.alt.length<8){return `Images should have alt text:
52
- for accessibility, all images should have descriptive alt text.
53
- This image's alt text is only ${image.alt.length} characters long.`}}});
54
-
55
- var ImageInTable = Rule.makeRule({name:"image-in-table",severity:Rule.Severity.BULK_WARNING,selector:"table image",message:`Image in table:
56
- do not put images inside of tables.`});
57
-
58
- var ImageMarkdown = Rule.makeRule({name:"image-markdown",severity:Rule.Severity.WARNING,selector:"image",lint:function(state,content,nodes,match,context){if(context?.stack&&context.stack.includes("widget")){return null}const parsedMarkdown=parse(context?.content||"",{});let hasInlineImageNode=false;traverseContent(parsedMarkdown,node=>{if(node.type==="image"){hasInlineImageNode=true;}});if(hasInlineImageNode){return `[Inline markdown image]:
59
- Markdown images are not recommended - these are images that use the
60
- ![alt](url) format. Please use the Image widget instead.`}return null}});
61
-
62
- var ImageSpacesAroundUrls = Rule.makeRule({name:"image-spaces-around-urls",severity:Rule.Severity.ERROR,selector:"image",lint:function(state,content,nodes,match,context){const image=nodes[0];const url=image.target;if(context&&context.content){const index=context.content.indexOf(url);if(index===-1){return}if(context.content[index-1]!=="("||context.content[index+url.length]!==")"){return `Whitespace before or after image url:
63
- For images, don't include any space or newlines after '(' or before ')'.
64
- Whitespace in image URLs causes translation difficulties.`}}}});
65
-
66
- var ImageUrlEmpty = Rule.makeRule({name:"image-url-empty",severity:Rule.Severity.ERROR,selector:"image",lint:function(state,content,nodes){const image=nodes[0];const url=image.target;if(!url||!url.trim()){return "Images should have a URL"}}});
67
-
68
- var ImageWidgetAltText = Rule.makeRule({name:"image-widget-alt-text",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="image"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context&&context.widgets&&context.widgets[nodeId];if(!widget){return}if(widget.options.decorative){return}const alt=widget.options.alt;if(!alt){return `Images should have alt text:
69
- for accessibility, all images should have a text description.
70
- Add a description in the "Alt Text" box of the image widget.`}if(alt.trim().length<8){return `Images should have alt text:
71
- for accessibility, all images should have descriptive alt text.
72
- This image's alt text is only ${alt.trim().length} characters long.`}if(alt.trim().length>125){return `Images should have alt text:
73
- for accessibility, image alt text should not exceed 125 characters.
74
- This image's alt text is ${alt.trim().length} characters long.
75
- Please pair your alt with a long description below if you need significantly
76
- more text to sufficiently describe the image.`}}});
77
-
78
- var ImageWidgetEmptySize = Rule.makeRule({name:"image-widget-empty-size",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="image"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context&&context.widgets&&context.widgets[nodeId];if(!widget){return}if(!widget.options.backgroundImage.url){return}const{width,height}=widget.options.backgroundImage;if(!width||!height){return `Images should have a non-zero size.
79
- This image has a width of ${width} and a height of ${height}.
80
- Please use the "Reset to original size" button to set the size of the image.`}}});
81
-
82
- var InaccessibleWidget = Rule.makeRule({name:"inaccessible-widget",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){const node=state.currentNode();const widgetType=node.widgetType;const widgetId=node.id;if(!widgetType||!widgetId){return}const widgetInfo=context?.widgets?.[widgetId];if(!widgetInfo){return}const accessible=CoreWidgetRegistry.isAccessible(widgetType,widgetInfo.options);if(!accessible){return {message:`The "${widgetType}" widget is not accessible.`,start:0,end:content.length,metadata:{widgetType:widgetType,widgetId:widgetId}}}}});
83
-
84
- var InteractiveGraphWidgetError = Rule.makeRule({name:"interactive-graph-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="interactive-graph"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const issues=[];const{correct,graph,lockedFigures}=widget.options;for(const figure of lockedFigures??[]){if(figure.type==="line"&&vector.equal(figure.points[0].coord,figure.points[1].coord)){issues.push("Locked line cannot have length 0.");}if(figure.type==="polygon"){if(figure.points.every(point=>vector.equal(point,figure.points[0]))){issues.push("Locked polygon cannot have all coordinates be the same.");}}if(figure.type==="ellipse"){if(figure.radius[0]<=0||figure.radius[1]<=0){issues.push("Locked ellipse must have positive radius values.");}}}if(correct?.type==="polygon"&&correct.numSides==="unlimited"&&correct.coords==null){issues.push("Polygon must be closed.");}if(graph?.type==="exponential"&&graph.startCoords!=null){const{coords,asymptote}=graph.startCoords;const asymptoteY=asymptote;const minY=Math.min(coords[0][1],coords[1][1]);const maxY=Math.max(coords[0][1],coords[1][1]);if(asymptoteY>=minY&&asymptoteY<=maxY){issues.push("The exponential start asymptote must not fall between or on the curve's start points.");}}checkShowPointLabelsHasLabels(graph,issues);checkShowPointLabelsHasLabels(correct,issues);const allIssuesString=issues.join("\n\n");return allIssuesString}});function checkShowPointLabelsHasLabels(g,issues){if(g==null||g.type==="none"||g.type==="vector"){return}if(g.showPointLabels!==true){return}const labels=g.pointLabels;if(labels===undefined||labels.every(l=>l==null||l==="")){issues.push("showPointLabels is true but pointLabels has no labels. Provide a label for at least one point.");return}const expected=expectedLabelCount(g);if(expected!==undefined&&labels.length!==expected){issues.push(`pointLabels has ${labels.length} entries but this graph type expects ${expected}. Use empty strings ("") to skip labels for specific points, e.g. ["A", "", "C"].`);}}function expectedLabelCount(g){switch(g.type){case "angle":case "quadratic":return 3;case "linear":case "ray":case "sinusoid":case "absolute-value":case "exponential":case "logarithm":case "tangent":return 2;case "circle":return 1;case "linear-system":return 4;case "segment":return (g.numSegments??1)*2;case "point":if(g.numPoints==="unlimited"||g.numPoints==null){return undefined}return g.numPoints;case "polygon":if(g.numSides==="unlimited"||g.numSides==null){return undefined}return g.numSides;default:return undefined}}
85
-
86
- var LabelImageWidgetError = Rule.makeRule({name:"label-image-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="label-image"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const warnings=[];const{choices,imageAlt,imageUrl,markers}=widget.options;if(choices.length<2){warnings.push("label-image widget must have at least two answer choices");}if(!imageUrl){warnings.push("No image url provided");}else if(!imageAlt){warnings.push("No image alt text provided");}if(!markers.length){warnings.push("label-image widget requires at least one marker");}else {let numNoAnswers=0;let numNoLabels=0;for(const marker of markers){if(!marker.answers.length){numNoAnswers++;}if(!marker.label){numNoLabels++;}}if(numNoAnswers>0){warnings.push(`label-image widget has ${numNoAnswers} markers with no answers selected`);}if(numNoLabels>0){warnings.push(`label-image widget has ${numNoLabels} markers with no ARIA label`);}}const allWarningsString=warnings.join("\n\n");return allWarningsString}});
87
-
88
- const LEGACY_HOSTNAME="ka-perseus-images.s3.amazonaws.com";var LegacyGifUrl = Rule.makeRule({name:"legacy-gif-url",severity:Rule.Severity.GUIDELINE,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="image"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context&&context.widgets&&context.widgets[nodeId];if(!widget){return}const url=widget.options?.backgroundImage?.url;if(!url){return}if(getHostname(url)===LEGACY_HOSTNAME&&url.toLowerCase().endsWith(".gif")){return `Legacy gif URL:
89
- gifs hosted at ${LEGACY_HOSTNAME} might not appear in the editor preview,
90
- but can still be viewed in the published content. Please use a
91
- cdn.kastatic.org/ka-perseus-images/ URL instead or confirm the
92
- image displays correctly in the published content.`}}});
93
-
94
- var LinkClickHere = Rule.makeRule({name:"link-click-here",severity:Rule.Severity.WARNING,selector:"link",pattern:/click here/i,message:`Inappropriate link text:
95
- Do not use the words "click here" in links.`});
96
-
97
- var LongParagraph = Rule.makeRule({name:"long-paragraph",severity:Rule.Severity.GUIDELINE,selector:"paragraph",pattern:/^.{501,}/,lint:function(state,content,nodes,match){return `Paragraph too long:
98
- This paragraph is ${content.length} characters long.
99
- Shorten it to 500 characters or fewer.`}});
100
-
101
- var MatcherWidgetError = Rule.makeRule({name:"matcher-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="matcher"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}if(widget.options.left.length!==widget.options.right.length){return "The two halves of the matcher have different numbers of cards."}}});
102
-
103
- var MathAdjacent = Rule.makeRule({name:"math-adjacent",severity:Rule.Severity.WARNING,selector:"blockMath+blockMath",message:`Adjacent math blocks:
104
- combine the blocks between \\begin{align} and \\end{align}`});
105
-
106
- var MathAlignExtraBreak = Rule.makeRule({name:"math-align-extra-break",severity:Rule.Severity.WARNING,selector:"blockMath",pattern:/(\\{2,})\s*\\end{align}/,message:`Extra space at end of block:
107
- Don't end an align block with backslashes`});
108
-
109
- var MathAlignLinebreaks = Rule.makeRule({name:"math-align-linebreaks",severity:Rule.Severity.WARNING,selector:"blockMath",pattern:/\\begin{align}[\s\S]*\\\\[\s\S]+\\end{align}/,lint:function(state,content,nodes,match){let text=match[0];while(text.length){const index=text.indexOf("\\\\");if(index===-1){return}text=text.substring(index+2);const nextpair=text.match(/^\s*\\\\\s*(?!\\\\)/);if(!nextpair){return "Use four backslashes between lines of an align block"}text=text.substring(nextpair[0].length);}}});
110
-
111
- var MathEmpty = Rule.makeRule({name:"math-empty",severity:Rule.Severity.WARNING,selector:"math, blockMath",pattern:/^$/,message:"Empty math: don't use $$ in your markdown."});
112
-
113
- var MathFrac = Rule.makeRule({name:"math-frac",severity:Rule.Severity.GUIDELINE,selector:"math, blockMath",pattern:/\\frac[ {]/,message:"Use \\dfrac instead of \\frac in your math expressions."});
114
-
115
- var MathNested = Rule.makeRule({name:"math-nested",severity:Rule.Severity.ERROR,selector:"math, blockMath",pattern:/\\text{[^$}]*\$[^$}]*\$[^}]*}/,message:`Nested math:
116
- Don't nest math expressions inside \\text{} blocks`});
117
-
118
- var MathStartsWithSpace = Rule.makeRule({name:"math-starts-with-space",severity:Rule.Severity.GUIDELINE,selector:"math, blockMath",pattern:/^\s*(~|\\qquad|\\quad|\\,|\\;|\\:|\\ |\\!|\\enspace|\\phantom)/,message:`Math starts with space:
119
- math should not be indented. Do not begin math expressions with
120
- LaTeX space commands like ~, \\;, \\quad, or \\phantom`});
121
-
122
- var MathTextEmpty = Rule.makeRule({name:"math-text-empty",severity:Rule.Severity.WARNING,selector:"math, blockMath",pattern:/\\text{\s*}/,message:"Empty \\text{} block in math expression"});
123
-
124
- var MathWithoutDollars = Rule.makeRule({name:"math-without-dollars",severity:Rule.Severity.GUIDELINE,pattern:/\\\w+{[^}]*}|{|}/,message:`This looks like LaTeX:
125
- did you mean to put it inside dollar signs?`});
126
-
127
- var NestedLists = Rule.makeRule({name:"nested-lists",severity:Rule.Severity.WARNING,selector:"list list",message:`Nested lists:
128
- nested lists are hard to read on mobile devices;
129
- do not use additional indentation.`});
130
-
131
- var NumericInputMissingLabel = Rule.makeRule({name:"numeric-input-missing-label",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="numeric-input"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}if(!widget.options.labelText){return `Inputs should have an aria label:
132
- While screen readers will fallback to a default label of 'Your answer', a more descriptive label would improve accessibility.`}}});
133
-
134
- var NumericInputWidgetError = Rule.makeRule({name:"numeric-input-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="numeric-input"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const issues=[];const answers=widget.options.answers;if(answers.some(answer=>answer.value==null)){issues.push("One or more answers is empty");}answers.forEach((answer,i)=>{const formatError=answer.strict&&(!answer.answerForms||answer.answerForms.length===0);if(formatError){issues.push(`Answer ${i+1} requires a format, but no format was selected`);}});const allWarningsString=issues.join("\n\n");return allWarningsString}});
135
-
136
- var PhetSimulationWidgetError = Rule.makeRule({name:"phet-simulation-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="phet-simulation"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}if(makeSafeUrl(widget.options.url,"en","https://phet.colorado.edu")===null){return "The URL is not from the PhET domain."}}});
137
-
138
- var PythonProgramWidgetError = Rule.makeRule({name:"python-program-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="python-program"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const errors=[];const height=widget.options.height;const programID=widget.options.programID;if(programID===""){errors.push("The program ID is required.");}if(!Number.isInteger(height)||height<1){errors.push("The height must be a positive integer.");}const allErrorsString=errors.join("\n\n");return allErrorsString}});
139
-
140
- var RadioWidgetError = Rule.makeRule({name:"radio-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="radio"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const choices=widget.options.choices;if(!choices.some(choice=>choice.correct)){return "No choice is marked as correct."}const correctChoices=choices.filter(choice=>choice.correct);const hasNotaCorrect=correctChoices.some(choice=>choice.isNoneOfTheAbove);if(hasNotaCorrect&&correctChoices.length>1){return "Cannot mark both None of the Above and another choice as correct."}return}});
141
-
142
- const minCards=2;function hasMixedContent(card){const contentNodes=[];traverseContent(parse(card.trim(),{}),node=>{if(node.type!=="paragraph"){contentNodes.push(node);}});return contentNodes.length>1&&contentNodes.some(node=>node.type==="image")}var SorterWidgetError = Rule.makeRule({name:"sorter-widget-error",severity:Rule.Severity.ERROR,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="sorter"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const warnings=[];const correct=widget.options.correct??[];if(correct.length<minCards){warnings.push(`Sorter requires at least ${minCards} cards.`);}if(correct.some(card=>card.trim()==="")){warnings.push("Sorter cards cannot be blank.");}if(correct.some(hasMixedContent)){warnings.push("Sorter cards cannot mix images with other content.");}return warnings.join("\n\n")}});
143
-
144
- const sorterMaxIdealCards=5;const sorterMaxHorizontalCards=5;var SorterWidgetWarning = Rule.makeRule({name:"sorter-widget-warning",severity:Rule.Severity.WARNING,selector:"widget",lint:function(state,content,nodes,match,context){if(state.currentNode().widgetType!=="sorter"){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}const warnings=[];const options=widget.options;const{layout,correct=[]}=options;if(correct.length>SORTER_MAX_CARDS){warnings.push(`Sorter cannot have more than ${SORTER_MAX_CARDS} cards.`);}if(layout==="horizontal"&&correct.length>sorterMaxHorizontalCards){warnings.push(`Sorter cannot have more than ${sorterMaxHorizontalCards} cards in horizontal layout.`);}if(correct.length>sorterMaxIdealCards){warnings.push(`Having more than ${sorterMaxIdealCards} cards in Sorter is discouraged.`);}return warnings.join("\n\n")}});
145
-
146
- var StaticWidgetInQuestionStem = Rule.makeRule({name:"static-widget-in-question-stem",severity:Rule.Severity.WARNING,selector:"widget",lint:(state,content,nodes,match,context)=>{if(context?.contentType!=="exercise"){return}if(context.stack.includes("hint")){return}const nodeId=state.currentNode().id;if(!nodeId){return}const widget=context?.widgets?.[nodeId];if(!widget){return}if(widget.static){return `Widget in question stem is static (non-interactive).`}}});
147
-
148
- var TableMissingCells = Rule.makeRule({name:"table-missing-cells",severity:Rule.Severity.WARNING,selector:"table",lint:function(state,content,nodes,match){const table=nodes[0];const headerLength=table.header.length;const rowLengths=table.cells.map(r=>r.length);for(let r=0;r<rowLengths.length;r++){if(rowLengths[r]!==headerLength){return `Table rows don't match header:
149
- The table header has ${headerLength} cells, but
150
- Row ${r+1} has ${rowLengths[r]} cells.`}}}});
151
-
152
- var UnbalancedCodeDelimiters = Rule.makeRule({name:"unbalanced-code-delimiters",severity:Rule.Severity.ERROR,pattern:/[`~]+/,message:`Unbalanced code delimiters:
153
- code blocks should begin and end with the same type and number of delimiters`});
154
-
155
- var UnescapedDollar = Rule.makeRule({name:"unescaped-dollar",severity:Rule.Severity.ERROR,selector:"unescapedDollar",message:`Unescaped dollar sign:
156
- Dollar signs must appear in pairs or be escaped as \\$`});
157
-
158
- var WidgetInTable = Rule.makeRule({name:"widget-in-table",severity:Rule.Severity.BULK_WARNING,selector:"table widget",message:`Widget in table:
159
- do not put widgets inside of tables.`});
160
-
161
- var AllRules = [AbsoluteUrl,DoubleSpacingAfterTerminal,ImageUrlEmpty,ExpressionWidget,ExtraContentSpacing,HeadingLevel1,HeadingLevelSkip,HeadingSentenceCase,HeadingTitleCase,ImageAltText,ImageMarkdown,ImageInTable,LinkClickHere,LongParagraph,MathAdjacent,MathAlignExtraBreak,MathAlignLinebreaks,MathEmpty,MathFrac,MathNested,MathStartsWithSpace,MathTextEmpty,NestedLists,StaticWidgetInQuestionStem,TableMissingCells,UnescapedDollar,WidgetInTable,MathWithoutDollars,UnbalancedCodeDelimiters,ImageSpacesAroundUrls,ImageWidgetAltText,ImageWidgetEmptySize,LegacyGifUrl,InaccessibleWidget,RadioWidgetError,ExpressionWidgetError,FreeResponseWidgetError,MatcherWidgetError,NumericInputMissingLabel,NumericInputWidgetError,PhetSimulationWidgetError,PythonProgramWidgetError,LabelImageWidgetError,InteractiveGraphWidgetError,SorterWidgetError,SorterWidgetWarning];
162
-
163
- class TreeTransformer{static isNode(n){return n&&typeof n==="object"&&typeof n.type==="string"}static isTextNode(n){return TreeTransformer.isNode(n)&&n.type==="text"&&typeof n.content==="string"}traverse(f){this._traverse(this.root,new TraversalState(this.root),f);}_traverse(n,state,f){let content="";if(TreeTransformer.isNode(n)){const node=n;state._containers.push(node);state._ancestors.push(node);if(typeof node.content==="string"){content=node.content;}const keys=Object.keys(node);keys.forEach(key=>{if(key==="type"){return}const value=node[key];if(value&&typeof value==="object"){state._indexes.push(key);content+=this._traverse(value,state,f);state._indexes.pop();}});state._currentNode=state._ancestors.pop();state._containers.pop();f(node,state,content);}else if(Array.isArray(n)){const nodes=n;state._containers.push(nodes);let index=0;while(index<nodes.length){state._indexes.push(index);content+=this._traverse(nodes[index],state,f);index=state._indexes.pop()+1;}state._containers.pop();}return content}constructor(root){this.root=root;}}class TraversalState{currentNode(){return this._currentNode||this.root}parent(){return this._ancestors.top()}ancestors(){return this._ancestors.values()}nextSibling(){const siblings=this._containers.top();if(!siblings||!Array.isArray(siblings)){return null}const index=this._indexes.top();if(siblings.length>index+1){return siblings[index+1]}return null}previousSibling(){const siblings=this._containers.top();if(!siblings||!Array.isArray(siblings)){return null}const index=this._indexes.top();if(index>0){return siblings[index-1]}return null}removeNextSibling(){const siblings=this._containers.top();if(siblings&&Array.isArray(siblings)){const index=this._indexes.top();if(siblings.length>index+1){return siblings.splice(index+1,1)[0]}}return null}replace(...replacements){const parent=this._containers.top();if(!parent){throw new PerseusError("Can't replace the root of the tree",Errors.Internal)}if(Array.isArray(parent)){const index=this._indexes.top();parent.splice(index,1,...replacements);this._indexes.pop();this._indexes.push(index+replacements.length-1);}else {const property=this._indexes.top();if(replacements.length===0){delete parent[property];}else if(replacements.length===1){parent[property]=replacements[0];}else {parent[property]=replacements;}}}hasPreviousSibling(){return Array.isArray(this._containers.top())&&this._indexes.top()>0}goToPreviousSibling(){if(!this.hasPreviousSibling()){throw new PerseusError("goToPreviousSibling(): node has no previous sibling",Errors.Internal)}this._currentNode=this.previousSibling();const index=this._indexes.pop();this._indexes.push(index-1);}hasParent(){return this._ancestors.size()!==0}goToParent(){if(!this.hasParent()){throw new PerseusError("goToParent(): node has no ancestor",Errors.NotAllowed)}this._currentNode=this._ancestors.pop();while(this._containers.size()&&this._containers.top()[this._indexes.top()]!==this._currentNode){this._containers.pop();this._indexes.pop();}}clone(){const clone=new TraversalState(this.root);clone._currentNode=this._currentNode;clone._containers=this._containers.clone();clone._indexes=this._indexes.clone();clone._ancestors=this._ancestors.clone();return clone}equals(that){return this.root===that.root&&this._currentNode===that._currentNode&&this._containers.equals(that._containers)&&this._indexes.equals(that._indexes)&&this._ancestors.equals(that._ancestors)}constructor(root){this.root=root;this._currentNode=null;this._containers=new Stack;this._indexes=new Stack;this._ancestors=new Stack;}}class Stack{push(v){this.stack.push(v);}pop(){return this.stack.pop()}top(){return this.stack[this.stack.length-1]}values(){return this.stack.slice(0)}size(){return this.stack.length}toString(){return this.stack.toString()}clone(){return new Stack(this.stack)}equals(that){if(!that||!that.stack||that.stack.length!==this.stack.length){return false}for(let i=0;i<this.stack.length;i++){if(this.stack[i]!==that.stack[i]){return false}}return true}constructor(array){this.stack=array?array.slice(0):[];}}
164
-
165
- const allLintRules=AllRules.filter(r=>r.severity<Rule.Severity.BULK_WARNING);function runLinter(tree,context,highlight,rules=allLintRules){const warnings=[];const tt=new TreeTransformer(tree);tt.traverse((node,state,content)=>{if(TreeTransformer.isTextNode(node)){let next=state.nextSibling();while(TreeTransformer.isTextNode(next)){node.content+=next.content;state.removeNextSibling();next=state.nextSibling();}}});let tableWarnings=[];let insideTable=false;tt.traverse((node,state,content)=>{const nodeWarnings=[];const applicableRules=rules.filter(r=>r.applies(context));const stack=[...context.stack];stack.push(node.type);const nodeContext={...context,stack:stack.join(".")};applicableRules.forEach(rule=>{const warning=rule.check(node,state,content,nodeContext);if(warning){if(warning.start||warning.end){warning.target=content.substring(warning.start,warning.end);}warnings.push(warning);if(highlight){nodeWarnings.push(warning);}}});if(!highlight){return}if(node.type==="table"){if(tableWarnings.length){nodeWarnings.push(...tableWarnings);}insideTable=false;tableWarnings=[];}else if(!insideTable){insideTable=state.ancestors().some(n=>n.type==="table");}if(insideTable&&nodeWarnings.length>0){tableWarnings.push(...nodeWarnings);}if(nodeWarnings.length>0){nodeWarnings.sort((a,b)=>{return a.severity-b.severity});if(node.type!=="text"||nodeWarnings.length>1){state.replace({type:"lint",content:node,message:nodeWarnings.map(w=>w.message).join("\n\n"),ruleName:nodeWarnings[0].rule,blockHighlight:nodeContext.blockHighlight,insideTable:insideTable,severity:nodeWarnings[0].severity});}else {const content=node.content;const warning=nodeWarnings[0];const start=warning.start;const end=warning.end;const prefix=content.substring(0,start);const lint=content.substring(start,end);const suffix=content.substring(end);const replacements=[];if(prefix){replacements.push({type:"text",content:prefix});}replacements.push({type:"lint",content:{type:"text",content:lint},message:warning.message,ruleName:warning.rule,insideTable:insideTable,severity:warning.severity});if(suffix){replacements.push({type:"text",content:suffix});}state.replace(...replacements);}}});return warnings}
166
-
167
- function lintPerseusItem(parsedPerseusItem){const questionLint=lintPerseusRenderer(parsedPerseusItem.question,"exercise");const hintLint=parsedPerseusItem.hints.map(hint=>lintPerseusRenderer(hint,"exercise"));return {question:questionLint,hints:hintLint}}function lintPerseusArticle(parsedPerseusArticle){const articleSections=Array.isArray(parsedPerseusArticle)?parsedPerseusArticle:[parsedPerseusArticle];return articleSections.map(section=>lintPerseusRenderer(section,"article"))}function lintPerseusRenderer(parsedPerseusRenderer,contentType){const tree=parse(parsedPerseusRenderer.content);const context={content:parsedPerseusRenderer.content,widgets:parsedPerseusRenderer.widgets,stack:[],contentType};return [...runLinter(tree,context,false)]}
168
-
169
- function pushContextStack(context,name){const stack=context.stack||[];return {...context,stack:stack.concat(name)}}
170
-
171
- export { Rule, libVersion, lintPerseusArticle, lintPerseusItem, lintPerseusRenderer, linterContextDefault, pushContextStack, allLintRules as rules, runLinter };
172
- //# sourceMappingURL=index.js.map