UNPKG

@yusufkandemir/eslint-plugin-lodash-template

Version:

ESLint plugin for John Resig-style micro template, Lodash's template, Underscore's template and EJS.

409 lines (371 loc) 12.6 kB
"use strict"; const path = require("path"); const sharedContainer = require("../shared-container"); const MicroTemplateService = require("../services/micro-template-service"); const SourceCodeStore = require("../services/source-code-store"); const ast = require("../ast/micro-template-nodes"); const parseScript = require("./parse-script"); const parseTemplateScript = require("./parse-template-script"); const MicroTemplateEvaluate = ast.MicroTemplateEvaluate; const MicroTemplateInterpolate = ast.MicroTemplateInterpolate; const MicroTemplateEscape = ast.MicroTemplateEscape; const MicroTemplateComment = ast.MicroTemplateComment; const MicroTemplateExpressionStart = ast.MicroTemplateExpressionStart; const MicroTemplateExpressionEnd = ast.MicroTemplateExpressionEnd; /** * Get delimiters from the given text and inner text. * * @param {string} text The hit text. * @param {string} innerCode The hit inner text. * @returns {Array} The delimiters result. */ function getDelimitersForFallback(text, innerCode) { const codeStart = text.indexOf(innerCode); const codeEnd = codeStart + innerCode.length; return [text.slice(0, codeStart), text.slice(codeEnd)]; } /** * Escape RegExp to given value * @param {string|string[]} value The base value * @returns {string} The escape string */ function escapeRegExp(value) { if (Array.isArray(value)) { return `(?:${value.map(escapeRegExp).join("|")})`; } return value.replace(/[$(-+.?[-^{-}]/gu, "\\$&"); // $& means the whole matched string } /** * Delimiters setting to RegExp source * * @param {*} val The delimiter settings. * @param {Array} defaultDelimiters The default delimiters. * @returns {string} The delimiters RegExp source. */ function settingToRegExpInfo(val, defaultDelimiters) { if (!val) { if (!defaultDelimiters) { // matches none return null; } return { pattern: `${defaultDelimiters[0]}([\\s\\S]*?)${defaultDelimiters[1]}`, getDelimiters: () => defaultDelimiters, }; } if (Array.isArray(val)) { const startDelim = Array.isArray(val[0]) ? [...val[0]].sort((a, b) => b.length - a.length) : val[0]; const endDelim = Array.isArray(val[1]) ? [...val[1]].sort((a, b) => b.length - a.length) : val[1]; const pattern = `${escapeRegExp(startDelim)}([\\s\\S]*?)${escapeRegExp( endDelim, )}`; if (!Array.isArray(startDelim) && !Array.isArray(endDelim)) { return { pattern, getDelimiters: () => val, }; } return { pattern, getDelimiters(text) { return [ Array.isArray(startDelim) ? startDelim.find((d) => text.startsWith(d)) : startDelim, Array.isArray(endDelim) ? endDelim.find((d) => text.endsWith(d)) : endDelim, ]; }, }; } const source = val instanceof RegExp ? val.source : `${val}`; const pattern = source.indexOf("([\\s\\S]+?)") >= 0 ? source.replace("([\\s\\S]+?)", "([\\s\\S]*?)") : source.indexOf("([\\S\\s]+?)") >= 0 ? source.replace("([\\S\\s]+?)", "([\\s\\S]*?)") : source.indexOf("([\\S\\s]*?)") >= 0 ? source.replace("([\\S\\s]*?)", "([\\s\\S]*?)") : source; let getDelimiters = undefined; const delimiterPattern = pattern.split("([\\s\\S]*?)"); if (delimiterPattern.length === 2) { const re = new RegExp( delimiterPattern.map((s) => `(${s})`).join("([\\s\\S]*?)"), "u", ); getDelimiters = (text, innerCode) => { const r = text.match(re); if (r) { return [r[1], r[3]]; } return getDelimitersForFallback(text, innerCode); }; } else { getDelimiters = getDelimitersForFallback; } return { pattern, getDelimiters, }; } /** * Generate micro template tokens iterator * * @param {string} code The template to parse. * @param {object} options The parser options. * @param {SourceCodeStore} sourceCodeStore The sourceCodeStore. * @returns {object} The parsing result. */ function* genMicroTemplateTokens(code, options, sourceCodeStore) { const templateSettings = options.templateSettings || {}; const parserDataList = [ { type: null, regExpInfo: templateSettings.literal ? { pattern: `(${escapeRegExp(templateSettings.literal)})`, } : null, }, { type: MicroTemplateComment, regExpInfo: settingToRegExpInfo(templateSettings.comment), }, { type: MicroTemplateInterpolate, regExpInfo: settingToRegExpInfo(templateSettings.interpolate, [ "<%=", "%>", ]), }, { type: MicroTemplateEscape, regExpInfo: settingToRegExpInfo(templateSettings.escape, [ "<%-", "%>", ]), }, { type: MicroTemplateEvaluate, regExpInfo: settingToRegExpInfo(templateSettings.evaluate, [ "<%", "%>", ]), }, ].filter((t) => Boolean(t.regExpInfo)); const re = new RegExp( parserDataList.map((t) => t.regExpInfo.pattern).join("|"), "gu", ); const indexes = parserDataList.map((_t, i) => i + 1); let r = undefined; while ((r = re.exec(code)) !== null) { const text = r[0]; // eslint-disable-next-line no-loop-func -- ignore const matchIndex = indexes.find((i) => r[i] != null); const parserData = parserDataList[matchIndex - 1]; const NodeType = parserData.type; if (!NodeType) { continue; } const innerCode = r[matchIndex]; const start = r.index; const end = re.lastIndex; const node = new NodeType(start, end, sourceCodeStore, { code: innerCode, }); const delimiters = parserData.regExpInfo.getDelimiters(text, innerCode); node.expressionStart = new MicroTemplateExpressionStart( start, start + delimiters[0].length, sourceCodeStore, { parent: node, }, ); node.expressionEnd = new MicroTemplateExpressionEnd( end - delimiters[1].length, end, sourceCodeStore, { parent: node, }, ); yield node; } } /** * Replace to whitespaces * @param {string} s string * @returns {string} whitespaces */ function replaceToWhitespace(s) { // DON'T use `u`/`v` flag!! // Characters with surrogate pairs must be replaced with two spaces. return s.replace(/[^\t\n\f\r \u2028\u2029]/g, " "); } /** * Parse the given template. * @param {string} code The template to parse. * @param {object} options The parser options. * @returns {object} The parsing result. */ function parseTemplate(code, options) { const parserOptions = normalizeOptions(options); const sourceCodeStore = new SourceCodeStore(code); // Decompose script and html with character positions intact let script = ""; let pre = 0; let template = ""; const microTemplateTokens = []; for (const token of genMicroTemplateTokens( code, parserOptions, sourceCodeStore, )) { microTemplateTokens.push(token); const start = token.start; const end = token.end; const part = code.slice(pre, start); script += replaceToWhitespace(part); template += part; const scriptBeforeBase = token.expressionStart.chars; const scriptAfterBase = token.expressionEnd.chars; const scriptBefore = replaceToWhitespace(scriptBeforeBase); let scriptAfter = replaceToWhitespace(scriptAfterBase); let inner = token.code; if ( token.type === "MicroTemplateInterpolate" || token.type === "MicroTemplateEscape" ) { scriptAfter = scriptAfter.replace(/ /u, ";"); } else if (token.type === "MicroTemplateComment") { inner = replaceToWhitespace(inner); } script += `${scriptBefore}${inner}${scriptAfter}`; template += replaceToWhitespace(code.slice(start, end)); pre = end; } const part = code.slice(pre, code.length); script += replaceToWhitespace(part); template += part; const scriptResult = parseScript(script, parserOptions); sourceCodeStore.template = template; sourceCodeStore.script = script; const service = new MicroTemplateService({ code, template, script, microTemplateTokens, sourceCodeStore, ast: scriptResult.ast, }); scriptResult.services = Object.assign(scriptResult.services || {}, { getMicroTemplateService() { return service; }, }); return scriptResult; } /** * Check whether the code is a template file. * @param {string} code The source code to check. * @param {object} options The parser options. * @returns {number} `1` if the source code is a template file. `2` if the source code is the script template. */ function getParseTargetKind(code, options) { const filePath = typeof options.filePath === "string" ? options.filePath : "unknown.js"; if (sharedContainer.getPathCoveredTemplate(filePath)) { return 2; } const container = sharedContainer.get(filePath); if (container && container.isHtml()) { return 1; } if (container && container.isParseTarget()) { return 1; } if (filePath.toLowerCase().endsWith(".html")) { return 1; } if (filePath.toLowerCase().endsWith(".vue")) { return 0; } return /^\s*</u.test(code) ? 1 : 0; } /** * Parse the given source code. * @param {string} code The source code to parse. * @param {object} options The parser options. * @returns {object} The parsing result. */ function parseForESLint(code, options) { const parserOptions = normalizeOptions(options); const targetKind = getParseTargetKind(code, parserOptions); if (targetKind === 0) { // Not a micro template. return parseScript(code, parserOptions); } if (targetKind === 2) { const templatePath = path.dirname(parserOptions.filePath); const container = sharedContainer.get(templatePath); const service = container && container.getService(); // Micro template. Parse the script with the template tag removed. return parseTemplateScript( sharedContainer.getPathCoveredTemplate(options.filePath), code, parserOptions, service || parseTemplate( code, parserOptions, ).services.getMicroTemplateService(), ); } // targetKind === 1 // Micro template. Parse template tags. const result = parseTemplate(code, parserOptions); // Store the service for use in postprocess. const service = result.services.getMicroTemplateService(); const container = sharedContainer.get(parserOptions.filePath); if (container) { container.setService(service); } return result; } /** * Normalize parser options * @param {*} options options */ function normalizeOptions(options) { return Object.assign( { comment: true, ecmaVersion: 2018, eslintScopeManager: true, eslintVisitorKeys: true, loc: true, range: true, raw: true, tokens: true, }, options, ); } //------------------------------------------------------------------------------ // Public //------------------------------------------------------------------------------ module.exports.parse = (code, options) => parseForESLint(code, options).ast; module.exports.parseForESLint = parseForESLint; module.exports.parseTemplate = parseTemplate; module.exports.meta = { name: __filename, version: require("../../package.json").version, };