eslint-plugin-vue
Version:
Official ESLint plugin for Vue.js
235 lines (232 loc) • 6.44 kB
JavaScript
'use strict';
const require_rolldown_runtime = require('../_virtual/rolldown_runtime.js');
const require_index = require('../utils/index.js');
const require_casing$1 = require('../utils/casing.js');
const require_regexp$1 = require('../utils/regexp.js');
//#region lib/rules/no-bare-strings-in-template.js
/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
var require_no_bare_strings_in_template = /* @__PURE__ */ require_rolldown_runtime.__commonJSMin(((exports, module) => {
const utils = require_index.default;
const regexp = require_regexp$1.default;
const casing = require_casing$1.default;
/**
* @typedef { { names: { [tagName in string]: Set<string> }, regexps: { name: RegExp, attrs: Set<string> }[], cache: { [tagName in string]: Set<string> } } } TargetAttrs
*/
const DEFAULT_ALLOWLIST = [
"(",
")",
",",
".",
"&",
"+",
"-",
"=",
"*",
"/",
"#",
"%",
"!",
"?",
":",
"[",
"]",
"{",
"}",
"<",
">",
"·",
"•",
"‐",
"–",
"—",
"−",
"|"
];
const DEFAULT_ATTRIBUTES = {
"/.+/": [
"title",
"aria-label",
"aria-placeholder",
"aria-roledescription",
"aria-valuetext"
],
input: ["placeholder"],
img: ["alt"]
};
const DEFAULT_DIRECTIVES = ["v-text"];
/**
* Parse attributes option
* @param {any} options
* @returns {TargetAttrs}
*/
function parseTargetAttrs(options) {
/** @type {TargetAttrs} */
const result = {
names: {},
regexps: [],
cache: {}
};
for (const tagName of Object.keys(options)) {
/** @type { Set<string> } */
const attrs = new Set(options[tagName]);
if (regexp.isRegExp(tagName)) result.regexps.push({
name: regexp.toRegExp(tagName),
attrs
});
else result.names[tagName] = attrs;
}
return result;
}
/**
* Get a string from given expression container node
* @param {VExpressionContainer} value
* @returns { string | null }
*/
function getStringValue(value) {
const expression = value.expression;
if (!expression) return null;
if (expression.type !== "Literal") return null;
if (typeof expression.value === "string") return expression.value;
return null;
}
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow the use of bare strings in `<template>`",
categories: void 0,
url: "https://eslint.vuejs.org/rules/no-bare-strings-in-template.html"
},
schema: [{
type: "object",
properties: {
allowlist: {
type: "array",
items: { type: "string" },
uniqueItems: true
},
attributes: {
type: "object",
patternProperties: { "^(?:\\S+|/.*/[a-z]*)$": {
type: "array",
items: { type: "string" },
uniqueItems: true
} },
additionalProperties: false
},
directives: {
type: "array",
items: {
type: "string",
pattern: "^v-"
},
uniqueItems: true
}
},
additionalProperties: false
}],
messages: {
unexpected: "Unexpected non-translated string used.",
unexpectedInAttr: "Unexpected non-translated string used in `{{attr}}`."
}
},
create(context) {
/**
* @typedef { { upper: ElementStack | null, name: string, attrs: Set<string> } } ElementStack
*/
const opts = context.options[0] || {};
/** @type {string[]} */
const rawAllowlist = opts.allowlist || DEFAULT_ALLOWLIST;
const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES);
const directives = opts.directives || DEFAULT_DIRECTIVES;
/** @type {string[]} */
const stringAllowlist = [];
/** @type {RegExp[]} */
const regexAllowlist = [];
for (const item of rawAllowlist) if (regexp.isRegExp(item)) regexAllowlist.push(regexp.toRegExp(item));
else stringAllowlist.push(item);
const allowlistRe = stringAllowlist.length > 0 ? new RegExp(stringAllowlist.map((w) => regexp.escape(w)).sort((a, b) => b.length - a.length).join("|"), "gu") : null;
/** @type {ElementStack | null} */
let elementStack = null;
/**
* Gets the bare string from given string
* @param {string} str
*/
function getBareString(str) {
let result = str.trim();
if (allowlistRe) result = result.replace(allowlistRe, "");
for (const regex of regexAllowlist) {
const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
const globalRegex = new RegExp(regex.source, flags);
result = result.replace(globalRegex, "");
}
return result.trim();
}
/**
* Get the attribute to be verified from the element name.
* @param {string} tagName
* @returns {Set<string>}
*/
function getTargetAttrs(tagName) {
if (attributes.cache[tagName]) return attributes.cache[tagName];
/** @type {string[]} */
const result = [];
if (attributes.names[tagName]) result.push(...attributes.names[tagName]);
for (const { name, attrs } of attributes.regexps) {
name.lastIndex = 0;
if (name.test(tagName)) result.push(...attrs);
}
if (casing.isKebabCase(tagName)) result.push(...getTargetAttrs(casing.pascalCase(tagName)));
return attributes.cache[tagName] = new Set(result);
}
return utils.defineTemplateBodyVisitor(context, {
VText(node) {
if (getBareString(node.value)) context.report({
node,
messageId: "unexpected"
});
},
VElement(node) {
elementStack = {
upper: elementStack,
name: node.rawName,
attrs: getTargetAttrs(node.rawName)
};
},
"VElement:exit"() {
elementStack = elementStack && elementStack.upper;
},
VAttribute(node) {
if (!node.value || !elementStack) return;
if (node.directive === false) {
if (!elementStack.attrs.has(node.key.rawName)) return;
if (getBareString(node.value.value)) context.report({
node: node.value,
messageId: "unexpectedInAttr",
data: { attr: node.key.rawName }
});
} else {
const directive = `v-${node.key.name.name}`;
if (!directives.includes(directive)) return;
const str = getStringValue(node.value);
if (str && getBareString(str)) context.report({
node: node.value,
messageId: "unexpectedInAttr",
data: { attr: directive }
});
}
}
});
}
};
}));
//#endregion
Object.defineProperty(exports, 'default', {
enumerable: true,
get: function () {
return require_no_bare_strings_in_template();
}
});