@khanacademy/perseus-linter 4.8.7 → 4.9.1
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/es/index.js +11 -5
- package/dist/es/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/lint-content.d.ts +33 -0
- package/dist/rules/image-widget-empty-size.d.ts +3 -0
- package/package.json +3 -3
- /package/dist/rules/{image-widget.d.ts → image-widget-alt-text.d.ts} +0 -0
package/dist/es/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import * as KAS from '@khanacademy/kas';
|
|
|
4
4
|
import { parse, traverseContent } from '@khanacademy/pure-markdown';
|
|
5
5
|
import { vector } from '@khanacademy/kmath';
|
|
6
6
|
|
|
7
|
-
const libName="@khanacademy/perseus-linter";const libVersion="4.
|
|
7
|
+
const libName="@khanacademy/perseus-linter";const libVersion="4.9.1";addLibraryVersionToPerseusDebug(libName,libVersion);
|
|
8
8
|
|
|
9
9
|
const linterContextDefault={contentType:"",highlightLint:false,paths:[],stack:[]};
|
|
10
10
|
|
|
@@ -65,7 +65,7 @@ Whitespace in image URLs causes translation difficulties.`}}}});
|
|
|
65
65
|
|
|
66
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
67
|
|
|
68
|
-
var
|
|
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
69
|
for accessibility, all images should have a text description.
|
|
70
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
71
|
for accessibility, all images should have descriptive alt text.
|
|
@@ -75,9 +75,13 @@ This image's alt text is ${alt.trim().length} characters long.
|
|
|
75
75
|
Please pair your alt with a long description below if you need significantly
|
|
76
76
|
more text to sufficiently describe the image.`}}});
|
|
77
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
|
+
|
|
78
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}}}}});
|
|
79
83
|
|
|
80
|
-
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,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.");}const allIssuesString=issues.join("\n\n");return allIssuesString}});
|
|
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.");}}const allIssuesString=issues.join("\n\n");return allIssuesString}});
|
|
81
85
|
|
|
82
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}});
|
|
83
87
|
|
|
@@ -141,13 +145,15 @@ Dollar signs must appear in pairs or be escaped as \\$`});
|
|
|
141
145
|
var WidgetInTable = Rule.makeRule({name:"widget-in-table",severity:Rule.Severity.BULK_WARNING,selector:"table widget",message:`Widget in table:
|
|
142
146
|
do not put widgets inside of tables.`});
|
|
143
147
|
|
|
144
|
-
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,
|
|
148
|
+
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,InaccessibleWidget,RadioWidgetError,ExpressionWidgetError,FreeResponseWidgetError,MatcherWidgetError,NumericInputWidgetError,PhetSimulationWidgetError,PythonProgramWidgetError,LabelImageWidgetError,InteractiveGraphWidgetError];
|
|
145
149
|
|
|
146
150
|
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):[];}}
|
|
147
151
|
|
|
148
152
|
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}
|
|
149
153
|
|
|
154
|
+
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)]}
|
|
155
|
+
|
|
150
156
|
function pushContextStack(context,name){const stack=context.stack||[];return {...context,stack:stack.concat(name)}}
|
|
151
157
|
|
|
152
|
-
export { Rule, libVersion, linterContextDefault, pushContextStack, allLintRules as rules, runLinter };
|
|
158
|
+
export { Rule, libVersion, lintPerseusArticle, lintPerseusItem, lintPerseusRenderer, linterContextDefault, pushContextStack, allLintRules as rules, runLinter };
|
|
153
159
|
//# sourceMappingURL=index.js.map
|