@khanacademy/perseus-linter 3.0.11 → 4.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/dist/index.d.ts CHANGED
@@ -1,32 +1,6 @@
1
- import Rule from "./rule";
2
1
  export { libVersion } from "./version";
3
- export { linterContextProps, linterContextDefault } from "./proptypes";
2
+ export { linterContextDefault } from "./proptypes";
3
+ export { default as Rule } from "./rule";
4
+ export { runLinter, allLintRules as rules } from "./run-linter";
4
5
  export type { LinterContextProps } from "./types";
5
- declare const allLintRules: ReadonlyArray<any>;
6
- export { Rule, allLintRules as rules };
7
- /**
8
- * Run the Perseus linter over the specified markdown parse tree,
9
- * with the specified context object, and
10
- * return a (possibly empty) array of lint warning objects. If the
11
- * highlight argument is true, this function also modifies the parse
12
- * tree to add "lint" nodes that can be visually rendered,
13
- * highlighting the problems for the user. The optional rules argument
14
- * is an array of Rule objects specifying which lint rules should be
15
- * applied to this parse tree. When omitted, a default set of rules is used.
16
- *
17
- * The context object may have additional properties that some lint
18
- * rules require:
19
- *
20
- * context.content is the source content string that was parsed to create
21
- * the parse tree.
22
- *
23
- * context.widgets is the widgets object associated
24
- * with the content string
25
- *
26
- * TODO: to make this even more general, allow the first argument to be
27
- * a string and run the parser over it in that case? (but ignore highlight
28
- * in that case). This would allow the one function to be used for both
29
- * online linting and batch linting.
30
- */
31
- export declare function runLinter(tree: any, context: any, highlight: boolean, rules?: ReadonlyArray<any>): ReadonlyArray<any>;
32
6
  export declare function pushContextStack(context: any, name: string): any;
package/dist/index.js CHANGED
@@ -2,17 +2,16 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var perseusCore = require('@khanacademy/perseus-core');
6
5
  var perseusUtils = require('@khanacademy/perseus-utils');
7
- var PropTypes = require('prop-types');
6
+ var perseusCore = require('@khanacademy/perseus-core');
8
7
 
9
- function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+ const libName="@khanacademy/perseus-linter";const libVersion="4.0.0";perseusUtils.addLibraryVersionToPerseusDebug(libName,libVersion);
10
9
 
11
- var PropTypes__default = /*#__PURE__*/_interopDefaultCompat(PropTypes);
10
+ const linterContextDefault={contentType:"",highlightLint:false,paths:[],stack:[]};
12
11
 
13
12
  class Selector{static parse(selectorText){return new Parser(selectorText).parse()}match(state){throw new perseusCore.PerseusError("Selector subclasses must implement match()",perseusCore.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);}}
14
13
 
15
- 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}}catch(e){return {rule:"lint-rule-failure",message:`Exception in rule ${this.name}: ${e.message}
14
+ 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",message:`Exception in rule ${this.name}: ${e.message}
16
15
  Stack trace:
17
16
  ${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 perseusCore.PerseusError("Lint rules must have a selector or pattern",perseusCore.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");
18
17
 
@@ -72,6 +71,8 @@ for accessibility, all images should have descriptive alt text.
72
71
  This image's alt text is only ${alt.trim().length} characters long.`}if(widget.options.caption&&widget.options.caption.match(/[^\\]\$/)){return `No math in image captions:
73
72
  Don't include math expressions in image captions.`}}});
74
73
 
74
+ 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=perseusCore.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}}}}});
75
+
75
76
  var LinkClickHere = Rule.makeRule({name:"link-click-here",severity:Rule.Severity.WARNING,selector:"link",pattern:/click here/i,message:`Inappropriate link text:
76
77
  Do not use the words "click here" in links.`});
77
78
 
@@ -122,20 +123,17 @@ Dollar signs must appear in pairs or be escaped as \\$`});
122
123
  var WidgetInTable = Rule.makeRule({name:"widget-in-table",severity:Rule.Severity.BULK_WARNING,selector:"table widget",message:`Widget in table:
123
124
  do not put widgets inside of tables.`});
124
125
 
125
- 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];
126
+ 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,InaccessibleWidget];
126
127
 
127
128
  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 perseusCore.PerseusError("Can't replace the root of the tree",perseusCore.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 perseusCore.PerseusError("goToPreviousSibling(): node has no previous sibling",perseusCore.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 perseusCore.PerseusError("goToParent(): node has no ancestor",perseusCore.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):[];}}
128
129
 
129
- const libName="@khanacademy/perseus-linter";const libVersion="3.0.11";perseusUtils.addLibraryVersionToPerseusDebug(libName,libVersion);
130
-
131
- const linterContextProps=PropTypes__default.default.shape({contentType:PropTypes__default.default.string,highlightLint:PropTypes__default.default.bool,paths:PropTypes__default.default.arrayOf(PropTypes__default.default.string),stack:PropTypes__default.default.arrayOf(PropTypes__default.default.string)});const linterContextDefault={contentType:"",highlightLint:false,paths:[],stack:[]};
130
+ 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||0;const end=warning.end||content.length;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}
132
131
 
133
- 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){tableWarnings.push(...nodeWarnings);}if(nodeWarnings.length){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||0;const end=warning.end||content.length;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}function pushContextStack(context,name){const stack=context.stack||[];return {...context,stack:stack.concat(name)}}
132
+ function pushContextStack(context,name){const stack=context.stack||[];return {...context,stack:stack.concat(name)}}
134
133
 
135
134
  exports.Rule = Rule;
136
135
  exports.libVersion = libVersion;
137
136
  exports.linterContextDefault = linterContextDefault;
138
- exports.linterContextProps = linterContextProps;
139
137
  exports.pushContextStack = pushContextStack;
140
138
  exports.rules = allLintRules;
141
139
  exports.runLinter = runLinter;