@html-eslint/template-syntax-parser
Version:
Parse template syntax
219 lines (201 loc) • 5.38 kB
JavaScript
/**
* @import {Range} from "@html-eslint/types"
* @import {
* CloseSyntax,
* OpenSyntax,
* SyntaxConfigItem,
* TemplateSyntax,
* TemplateSyntaxParserResult
* } from "./types"
*/
module.exports = class Parser {
/**
* @param {string} code
* @param {SyntaxConfigItem[]} syntaxPairs
* @param {Range[]} skipRanges
*/
constructor(code, syntaxPairs, skipRanges) {
/** @type {string} */
this.code = code;
/** @type {SyntaxConfigItem[]} */
this.syntaxPairs = syntaxPairs || [];
/** @type {Range[]} */
this.skipRanges = skipRanges;
/** @type {OpenSyntax[]} */
this.syntaxStack = [];
/** @type {TemplateSyntax[]} */
this.result = [];
}
/**
* @private
* @param {number} index
* @returns {Range | undefined}
*/
findSkipRange(index) {
return this.skipRanges.find(
(range) => range[0] <= index && index < range[1]
);
}
/**
* @param {string} syntax
* @param {number} position
* @returns {number}
*/
indexOf(syntax, position) {
const index = this.code.indexOf(syntax, position);
if (index < 0) return index;
const skipRange = this.findSkipRange(index);
if (skipRange) {
return this.indexOf(syntax, skipRange[1]);
}
return index;
}
/**
* @param {number} position
* @returns {OpenSyntax | null}
*/
findOpenSyntax(position) {
/** @type {string | null} */
let value = null;
let rangeStart = Infinity;
/** @type {boolean | undefined} */
let isComment = undefined;
for (let i = 0; i < this.syntaxPairs.length; i++) {
const syntax = this.syntaxPairs[i];
const open = syntax.open;
const openIndex = this.indexOf(open, position);
if (openIndex >= 0 && openIndex < rangeStart) {
value = open;
rangeStart = openIndex;
isComment = syntax.isComment;
}
}
if (!value) return null;
return {
type: "open",
value,
isComment,
range: [rangeStart, rangeStart + value.length],
};
}
/**
* @param {number} position
* @returns {CloseSyntax | null}
*/
findCloseSyntax(position) {
/** @type {string | null} */
let value = null;
let rangeStart = Infinity;
/** @type {boolean | undefined} */
let isComment = undefined;
for (let i = 0; i < this.syntaxPairs.length; i++) {
const syntax = this.syntaxPairs[i];
const close = syntax.close;
const closeIndex = this.indexOf(close, position);
if (closeIndex >= 0 && closeIndex < rangeStart) {
value = close;
rangeStart = closeIndex;
isComment = syntax.isComment;
}
}
if (!value) return null;
return {
type: "close",
value,
isComment,
range: [rangeStart, rangeStart + value.length],
};
}
/** @returns {OpenSyntax[]} */
findAllOpenSyntax() {
/** @type {OpenSyntax[]} */
const result = [];
let position = 0;
while (position < this.code.length) {
const openSyntax = this.findOpenSyntax(position);
if (!openSyntax) {
break;
}
result.push(openSyntax);
position = openSyntax.range[1];
}
return result;
}
/** @returns {CloseSyntax[]} */
findAllCloseSyntax() {
/** @type {CloseSyntax[]} */
const result = [];
let position = 0;
while (position < this.code.length) {
const closeSyntax = this.findCloseSyntax(position);
if (!closeSyntax) {
break;
}
result.push(closeSyntax);
position = closeSyntax.range[1];
}
return result;
}
/** @returns {(CloseSyntax | OpenSyntax)[]} */
findAllSyntax() {
/** @type {(CloseSyntax | OpenSyntax)[]} */
const result = this.findAllOpenSyntax();
return result
.concat(this.findAllCloseSyntax())
.sort((a, b) => a.range[0] - b.range[0]);
}
/**
* @param {OpenSyntax} open
* @returns {string}
*/
getPossibleCloseValueOf(open) {
const found = this.syntaxPairs.find((syntax) => syntax.open === open.value);
if (!found) {
/* istanbul ignore next */
throw new Error("Unexpected open value: " + open);
}
return found.close;
}
/** @param {CloseSyntax | OpenSyntax} syntax */
eatSyntax(syntax) {
if (syntax.type === "open") {
const top = this.syntaxStack[this.syntaxStack.length - 1];
if (!top) {
this.syntaxStack.push(syntax);
return;
}
if (top.isComment) {
return;
}
throw new Error(
`Expecting "${this.getPossibleCloseValueOf(top)}", but found "${
syntax.value
}" at (${syntax.range[0]}, ${syntax.range[1]}).`
);
} else if (syntax.type === "close") {
const top = this.syntaxStack[this.syntaxStack.length - 1];
if (!top) {
return;
}
if (top.isComment && !syntax.isComment) {
return;
}
this.syntaxStack.pop();
if (this.getPossibleCloseValueOf(top) === syntax.value) {
this.result.push({
open: [top.range[0], top.range[0] + top.value.length],
close: [syntax.range[1] - syntax.value.length, syntax.range[1]],
});
}
}
}
/** @returns {TemplateSyntaxParserResult} */
parse() {
for (const syntax of this.findAllSyntax()) {
this.eatSyntax(syntax);
}
return {
syntax: this.result,
};
}
};