This repository has been archived on 2020-11-02. You can view files and clone it, but cannot push or open issues or pull requests.
TripSit_Suite/node_modules/eslint-plugin-vue/lib/rules/no-bare-strings-in-template.js
2020-11-01 22:46:04 +00:00

276 lines
6.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @author Yosuke Ota
* See LICENSE file in root directory for full license.
*/
'use strict'
// ------------------------------------------------------------------------------
// Requirements
// ------------------------------------------------------------------------------
const utils = require('../utils')
const regexp = require('../utils/regexp')
const casing = require('../utils/casing')
/**
* @typedef { { names: { [tagName in string]: Set<string> }, regexps: { name: RegExp, attrs: Set<string> }[], cache: { [tagName in string]: Set<string> } } } TargetAttrs
*/
// ------------------------------------------------------------------------------
// Constants
// ------------------------------------------------------------------------------
// https://dev.w3.org/html5/html-author/charref
const DEFAULT_ALLOWLIST = [
'(',
')',
',',
'.',
'&',
'+',
'-',
'=',
'*',
'/',
'#',
'%',
'!',
'?',
':',
'[',
']',
'{',
'}',
'<',
'>',
'\u00b7', // "·"
'\u2022', // "•"
'\u2010', // ""
'\u2013', // ""
'\u2014', // "—"
'\u2212', // ""
'|'
]
const DEFAULT_ATTRIBUTES = {
'/.+/': [
'title',
'aria-label',
'aria-placeholder',
'aria-roledescription',
'aria-valuetext'
],
input: ['placeholder'],
img: ['alt']
}
const DEFAULT_DIRECTIVES = ['v-text']
// --------------------------------------------------------------------------
// Helpers
// --------------------------------------------------------------------------
/**
* 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
}
// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow the use of bare strings in `<template>`',
categories: undefined,
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
}
}
}
],
messages: {
unexpected: 'Unexpected non-translated string used.',
unexpectedInAttr: 'Unexpected non-translated string used in `{{attr}}`.'
}
},
/** @param {RuleContext} context */
create(context) {
/**
* @typedef { { upper: ElementStack | null, name: string, attrs: Set<string> } } ElementStack
*/
const opts = context.options[0] || {}
/** @type {string[]} */
const allowlist = opts.allowlist || DEFAULT_ALLOWLIST
const attributes = parseTargetAttrs(opts.attributes || DEFAULT_ATTRIBUTES)
const directives = opts.directives || DEFAULT_DIRECTIVES
const allowlistRe = new RegExp(
allowlist.map((w) => regexp.escape(w)).join('|'),
'gu'
)
/** @type {ElementStack | null} */
let elementStack = null
/**
* Gets the bare string from given string
* @param {string} str
*/
function getBareString(str) {
return str.trim().replace(allowlistRe, '').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, {
/** @param {VText} node */
VText(node) {
if (getBareString(node.value)) {
context.report({
node,
messageId: 'unexpected'
})
}
},
/**
* @param {VElement} node
*/
VElement(node) {
elementStack = {
upper: elementStack,
name: node.rawName,
attrs: getTargetAttrs(node.rawName)
}
},
'VElement:exit'() {
elementStack = elementStack && elementStack.upper
},
/** @param {VAttribute|VDirective} node */
VAttribute(node) {
if (!node.value || !elementStack) {
return
}
if (node.directive === false) {
const attrs = elementStack.attrs
if (!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
}
})
}
}
}
})
}
}