diff --git a/dist/core/rules/tagname-lowercase.js b/dist/core/rules/tagname-lowercase.js index 4a9513453..e577277fe 100644 --- a/dist/core/rules/tagname-lowercase.js +++ b/dist/core/rules/tagname-lowercase.js @@ -1,19 +1,72 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +const svgTagNameIgnores = [ + 'animateMotion', + 'animateTransform', + 'clipPath', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'foreignObject', + 'linearGradient', + 'radialGradient', + 'textPath', +]; +function testAgainstStringOrRegExp(value, comparison) { + if (comparison instanceof RegExp) { + return comparison.test(value) + ? { match: value, pattern: comparison } + : false; + } + const firstComparisonChar = comparison[0]; + const lastComparisonChar = comparison[comparison.length - 1]; + const secondToLastComparisonChar = comparison[comparison.length - 2]; + const comparisonIsRegex = firstComparisonChar === '/' && + (lastComparisonChar === '/' || + (secondToLastComparisonChar === '/' && lastComparisonChar === 'i')); + const hasCaseInsensitiveFlag = comparisonIsRegex && lastComparisonChar === 'i'; + if (comparisonIsRegex) { + const valueMatches = hasCaseInsensitiveFlag + ? new RegExp(comparison.slice(1, -2), 'i').test(value) + : new RegExp(comparison.slice(1, -1)).test(value); + return valueMatches; + } + return value === comparison; +} exports.default = { id: 'tagname-lowercase', description: 'All html element names must be in lowercase.', init(parser, reporter, options) { - const exceptions = Array.isArray(options) - ? options - : []; + const exceptions = (Array.isArray(options) ? options : []).concat(svgTagNameIgnores); parser.addListener('tagstart,tagend', (event) => { const tagName = event.tagName; - if (exceptions.indexOf(tagName) === -1 && + if (!exceptions.find((exp) => testAgainstStringOrRegExp(tagName, exp)) && tagName !== tagName.toLowerCase()) { reporter.error(`The html element name of [ ${tagName} ] must be in lowercase.`, event.line, event.col, this, event.raw); } }); }, }; -//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFnbmFtZS1sb3dlcmNhc2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy90YWduYW1lLWxvd2VyY2FzZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLGtCQUFlO0lBQ2IsRUFBRSxFQUFFLG1CQUFtQjtJQUN2QixXQUFXLEVBQUUsOENBQThDO0lBQzNELElBQUksQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU87UUFDNUIsTUFBTSxVQUFVLEdBQTRCLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDO1lBQ2hFLENBQUMsQ0FBQyxPQUFPO1lBQ1QsQ0FBQyxDQUFDLEVBQUUsQ0FBQTtRQUVOLE1BQU0sQ0FBQyxXQUFXLENBQUMsaUJBQWlCLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRTtZQUM5QyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFBO1lBQzdCLElBQ0UsVUFBVSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsS0FBSyxDQUFDLENBQUM7Z0JBQ2xDLE9BQU8sS0FBSyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQ2pDLENBQUM7Z0JBQ0QsUUFBUSxDQUFDLEtBQUssQ0FDWiw4QkFBOEIsT0FBTywwQkFBMEIsRUFDL0QsS0FBSyxDQUFDLElBQUksRUFDVixLQUFLLENBQUMsR0FBRyxFQUNULElBQUksRUFDSixLQUFLLENBQUMsR0FBRyxDQUNWLENBQUE7WUFDSCxDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDO0NBQ00sQ0FBQSJ9 \ No newline at end of file +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidGFnbmFtZS1sb3dlcmNhc2UuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9zcmMvY29yZS9ydWxlcy90YWduYW1lLWxvd2VyY2FzZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUVBLE1BQU0saUJBQWlCLEdBQUc7SUFDeEIsZUFBZTtJQUNmLGtCQUFrQjtJQUNsQixVQUFVO0lBQ1YsU0FBUztJQUNULGVBQWU7SUFDZixxQkFBcUI7SUFDckIsYUFBYTtJQUNiLGtCQUFrQjtJQUNsQixtQkFBbUI7SUFDbkIsbUJBQW1CO0lBQ25CLGdCQUFnQjtJQUNoQixjQUFjO0lBQ2QsU0FBUztJQUNULFNBQVM7SUFDVCxTQUFTO0lBQ1QsU0FBUztJQUNULFNBQVM7SUFDVCxnQkFBZ0I7SUFDaEIsU0FBUztJQUNULFNBQVM7SUFDVCxhQUFhO0lBQ2IsY0FBYztJQUNkLFVBQVU7SUFDVixjQUFjO0lBQ2Qsb0JBQW9CO0lBQ3BCLGFBQWE7SUFDYixRQUFRO0lBQ1IsY0FBYztJQUNkLGVBQWU7SUFDZixnQkFBZ0I7SUFDaEIsZ0JBQWdCO0lBQ2hCLFVBQVU7Q0FDWCxDQUFBO0FBUUQsU0FBUyx5QkFBeUIsQ0FBQyxLQUFhLEVBQUUsVUFBMkI7SUFFM0UsSUFBSSxVQUFVLFlBQVksTUFBTSxFQUFFLENBQUM7UUFDakMsT0FBTyxVQUFVLENBQUMsSUFBSSxDQUFDLEtBQUssQ0FBQztZQUMzQixDQUFDLENBQUMsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLE9BQU8sRUFBRSxVQUFVLEVBQUU7WUFDdkMsQ0FBQyxDQUFDLEtBQUssQ0FBQTtJQUNYLENBQUM7SUFHRCxNQUFNLG1CQUFtQixHQUFHLFVBQVUsQ0FBQyxDQUFDLENBQUMsQ0FBQTtJQUN6QyxNQUFNLGtCQUFrQixHQUFHLFVBQVUsQ0FBQyxVQUFVLENBQUMsTUFBTSxHQUFHLENBQUMsQ0FBQyxDQUFBO0lBQzVELE1BQU0sMEJBQTBCLEdBQUcsVUFBVSxDQUFDLFVBQVUsQ0FBQyxNQUFNLEdBQUcsQ0FBQyxDQUFDLENBQUE7SUFFcEUsTUFBTSxpQkFBaUIsR0FDckIsbUJBQW1CLEtBQUssR0FBRztRQUMzQixDQUFDLGtCQUFrQixLQUFLLEdBQUc7WUFDekIsQ0FBQywwQkFBMEIsS0FBSyxHQUFHLElBQUksa0JBQWtCLEtBQUssR0FBRyxDQUFDLENBQUMsQ0FBQTtJQUV2RSxNQUFNLHNCQUFzQixHQUFHLGlCQUFpQixJQUFJLGtCQUFrQixLQUFLLEdBQUcsQ0FBQTtJQUc5RSxJQUFJLGlCQUFpQixFQUFFLENBQUM7UUFDdEIsTUFBTSxZQUFZLEdBQUcsc0JBQXNCO1lBQ3pDLENBQUMsQ0FBQyxJQUFJLE1BQU0sQ0FBQyxVQUFVLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxFQUFFLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUM7WUFDdEQsQ0FBQyxDQUFDLElBQUksTUFBTSxDQUFDLFVBQVUsQ0FBQyxLQUFLLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUE7UUFFbkQsT0FBTyxZQUFZLENBQUE7SUFDckIsQ0FBQztJQUdELE9BQU8sS0FBSyxLQUFLLFVBQVUsQ0FBQTtBQUM3QixDQUFDO0FBRUQsa0JBQWU7SUFDYixFQUFFLEVBQUUsbUJBQW1CO0lBQ3ZCLFdBQVcsRUFBRSw4Q0FBOEM7SUFDM0QsSUFBSSxDQUFDLE1BQU0sRUFBRSxRQUFRLEVBQUUsT0FBTztRQUM1QixNQUFNLFVBQVUsR0FBRyxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsTUFBTSxDQUMvRCxpQkFBaUIsQ0FDbEIsQ0FBQTtRQUVELE1BQU0sQ0FBQyxXQUFXLENBQUMsaUJBQWlCLEVBQUUsQ0FBQyxLQUFLLEVBQUUsRUFBRTtZQUM5QyxNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsT0FBTyxDQUFBO1lBQzdCLElBQ0UsQ0FBQyxVQUFVLENBQUMsSUFBSSxDQUFDLENBQUMsR0FBRyxFQUFFLEVBQUUsQ0FBQyx5QkFBeUIsQ0FBQyxPQUFPLEVBQUUsR0FBRyxDQUFDLENBQUM7Z0JBQ2xFLE9BQU8sS0FBSyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQ2pDLENBQUM7Z0JBQ0QsUUFBUSxDQUFDLEtBQUssQ0FDWiw4QkFBOEIsT0FBTywwQkFBMEIsRUFDL0QsS0FBSyxDQUFDLElBQUksRUFDVixLQUFLLENBQUMsR0FBRyxFQUNULElBQUksRUFDSixLQUFLLENBQUMsR0FBRyxDQUNWLENBQUE7WUFDSCxDQUFDO1FBQ0gsQ0FBQyxDQUFDLENBQUE7SUFDSixDQUFDO0NBQ00sQ0FBQSJ9 \ No newline at end of file diff --git a/src/core/rules/tagname-lowercase.ts b/src/core/rules/tagname-lowercase.ts index fefaa2d0a..99af0a736 100644 --- a/src/core/rules/tagname-lowercase.ts +++ b/src/core/rules/tagname-lowercase.ts @@ -1,17 +1,91 @@ import { Rule } from '../types' +const svgTagNameIgnores = [ + 'animateMotion', + 'animateTransform', + 'clipPath', + 'feBlend', + 'feColorMatrix', + 'feComponentTransfer', + 'feComposite', + 'feConvolveMatrix', + 'feDiffuseLighting', + 'feDisplacementMap', + 'feDistantLight', + 'feDropShadow', + 'feFlood', + 'feFuncA', + 'feFuncB', + 'feFuncG', + 'feFuncR', + 'feGaussianBlur', + 'feImage', + 'feMerge', + 'feMergeNode', + 'feMorphology', + 'feOffset', + 'fePointLight', + 'feSpecularLighting', + 'feSpotLight', + 'feTile', + 'feTurbulence', + 'foreignObject', + 'linearGradient', + 'radialGradient', + 'textPath', +] + +/** + * testAgainstStringOrRegExp + * + * @param value string to test + * @param comparison raw string or regex string + */ +function testAgainstStringOrRegExp(value: string, comparison: string | RegExp) { + // If it's a RegExp, test directly + if (comparison instanceof RegExp) { + return comparison.test(value) + ? { match: value, pattern: comparison } + : false + } + + // Check if it's RegExp in a string + const firstComparisonChar = comparison[0] + const lastComparisonChar = comparison[comparison.length - 1] + const secondToLastComparisonChar = comparison[comparison.length - 2] + + const comparisonIsRegex = + firstComparisonChar === '/' && + (lastComparisonChar === '/' || + (secondToLastComparisonChar === '/' && lastComparisonChar === 'i')) + + const hasCaseInsensitiveFlag = comparisonIsRegex && lastComparisonChar === 'i' + + // If so, create a new RegExp from it + if (comparisonIsRegex) { + const valueMatches = hasCaseInsensitiveFlag + ? new RegExp(comparison.slice(1, -2), 'i').test(value) + : new RegExp(comparison.slice(1, -1)).test(value) + + return valueMatches + } + + // Otherwise, it's a string. Do a strict comparison + return value === comparison +} + export default { id: 'tagname-lowercase', description: 'All html element names must be in lowercase.', init(parser, reporter, options) { - const exceptions: Array = Array.isArray(options) - ? options - : [] + const exceptions = (Array.isArray(options) ? options : []).concat( + svgTagNameIgnores + ) parser.addListener('tagstart,tagend', (event) => { const tagName = event.tagName if ( - exceptions.indexOf(tagName) === -1 && + !exceptions.find((exp) => testAgainstStringOrRegExp(tagName, exp)) && tagName !== tagName.toLowerCase() ) { reporter.error( diff --git a/src/core/types.ts b/src/core/types.ts index 003476994..e4b6d83fd 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -56,7 +56,7 @@ export interface Ruleset { 'tag-no-obsolete'?: boolean 'tag-pair'?: boolean 'tag-self-close'?: boolean - 'tagname-lowercase'?: boolean + 'tagname-lowercase'?: boolean | Array 'tagname-specialchars'?: boolean 'tags-check'?: { [tagName: string]: Record } 'title-require'?: boolean diff --git a/test/rules/tagname-lowercase.spec.js b/test/rules/tagname-lowercase.spec.js index dd116d851..0ccdbd2dd 100644 --- a/test/rules/tagname-lowercase.spec.js +++ b/test/rules/tagname-lowercase.spec.js @@ -29,4 +29,27 @@ describe(`Rules: ${ruleId}`, () => { const messages = HTMLHint.verify(code, ruleOptions) expect(messages.length).toBe(0) }) + + it('Known SVG elements should be ignored with no config', () => { + const code = + '' + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + }) + + it('Known SVG elements should be ignored with a config override', () => { + const code = '' + ruleOptions[ruleId] = ['customTag'] + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + ruleOptions[ruleId] = true + }) + + it('Set to array list should not result in an error', () => { + const code = '' + ruleOptions[ruleId] = ['CustomComponent', 'AnotherCamelCase'] + const messages = HTMLHint.verify(code, ruleOptions) + expect(messages.length).toBe(0) + ruleOptions[ruleId] = true + }) }) diff --git a/website/src/content/docs/rules/tagname-lowercase.mdx b/website/src/content/docs/rules/tagname-lowercase.mdx index f0d26d7f4..283a9cc13 100644 --- a/website/src/content/docs/rules/tagname-lowercase.mdx +++ b/website/src/content/docs/rules/tagname-lowercase.mdx @@ -14,7 +14,7 @@ Level: - `true`: enable rule - `false`: disable rule -- `['clipPath', 'data-Test']`: Ignore some tagname name +- `['clipPath', 'data-Test']`: enable rule except for the given tag names. All SVG camelCase elements are included, for example `linearGradient`, `foreignObject` ### The following patterns are **not** considered rule violations @@ -22,6 +22,12 @@ Level:
``` +SVG elements with camelCase names (e.g. `linearGradient`, `foreignObject`) are allowed: + +```html + +``` + ### The following pattern is considered a rule violation: ```html