From 0c55a301c8aa22481cf2ef5245bb0d3184b338ab Mon Sep 17 00:00:00 2001 From: Shinigami Date: Thu, 25 Jun 2020 19:54:18 +0200 Subject: [PATCH] feat: new configuration structure (#445) * feat: rebuild ruleset interface * feat: update default ruleset * feat: check is rule enabled * feat: pass reporter message callback to init * feat: implement new options * feat: bind reporter to callback * feat: update tests * feat: fix inline comments * feat: fix test --- src/core/core.ts | 50 ++++--- src/core/reporter.ts | 8 ++ src/core/rules/alt-require.ts | 6 +- src/core/rules/attr-lowercase.ts | 12 +- src/core/rules/attr-no-duplication.ts | 4 +- .../rules/attr-no-unnecessary-whitespace.ts | 6 +- src/core/rules/attr-sorted.ts | 4 +- src/core/rules/attr-unsafe-chars.ts | 4 +- src/core/rules/attr-value-double-quotes.ts | 4 +- src/core/rules/attr-value-not-empty.ts | 4 +- src/core/rules/attr-value-single-quotes.ts | 4 +- src/core/rules/attr-whitespace.ts | 10 +- src/core/rules/doctype-first.ts | 4 +- src/core/rules/doctype-html5.ts | 4 +- src/core/rules/head-script-disabled.ts | 4 +- src/core/rules/href-abs-or-rel.ts | 10 +- src/core/rules/id-class-ad-disabled.ts | 4 +- src/core/rules/id-class-value.ts | 29 ++-- src/core/rules/id-unique.ts | 4 +- src/core/rules/inline-script-disabled.ts | 6 +- src/core/rules/inline-style-disabled.ts | 4 +- src/core/rules/input-requires-label.ts | 4 +- src/core/rules/script-disabled.ts | 4 +- src/core/rules/space-tab-mixed-disabled.ts | 28 ++-- src/core/rules/spec-char-escape.ts | 4 +- src/core/rules/src-not-empty.ts | 4 +- src/core/rules/style-disabled.ts | 4 +- src/core/rules/tag-pair.ts | 8 +- src/core/rules/tag-self-close.ts | 4 +- src/core/rules/tagname-lowercase.ts | 8 +- src/core/rules/tagname-specialchars.ts | 4 +- src/core/rules/tags-check.ts | 20 +-- src/core/rules/title-require.ts | 6 +- src/core/types.ts | 127 +++++++++++------- test/core.spec.js | 22 +-- test/rules/alt-require.spec.js | 2 +- test/rules/attr-lowercase.spec.js | 10 +- test/rules/attr-no-duplication.spec.js | 2 +- .../attr-no-unnecessary-whitespace.spec.js | 2 +- test/rules/attr-sort.spec.js | 2 +- test/rules/attr-unsafe-chars.spec.js | 2 +- test/rules/attr-value-double-quotes.spec.js | 2 +- test/rules/attr-value-not-empty.spec.js | 2 +- test/rules/attr-value-single-quotes.spec.js | 2 +- test/rules/attr-whitespace.spec.js | 2 +- test/rules/doctype-first.spec.js | 2 +- test/rules/doctype-html5.spec.js | 2 +- test/rules/head-require.spec.js | 2 +- test/rules/head-script-disabled.spec.js | 2 +- test/rules/href-abs-or-rel.spec.js | 8 +- test/rules/id-class-ad-disabled.spec.js | 2 +- test/rules/id-class-value.spec.js | 21 +-- test/rules/id-unique.spec.js | 2 +- test/rules/inline-script-disabled.spec.js | 2 +- test/rules/inline-style-disabled.spec.js | 2 +- test/rules/input-requires-label.spec.js | 2 +- test/rules/script-disabled.spec.js | 2 +- test/rules/space-tab-mixed-disabled.spec.js | 10 +- test/rules/spec-char-escape.spec.js | 2 +- test/rules/src-not-empty.spec.js | 2 +- test/rules/style-disabled.spec.js | 2 +- test/rules/tag-pair.spec.js | 2 +- test/rules/tag-self-close.spec.js | 2 +- test/rules/tagname-lowercase.spec.js | 2 +- test/rules/tagname-specialchars.spec.js | 2 +- test/rules/tags-check.spec.js | 13 +- test/rules/title-require.spec.js | 2 +- 67 files changed, 310 insertions(+), 238 deletions(-) diff --git a/src/core/core.ts b/src/core/core.ts index 6299880f3..cd78c6cfd 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -1,7 +1,7 @@ import HTMLParser from './htmlparser' -import Reporter from './reporter' +import Reporter, { ReportMessageCallback } from './reporter' import * as HTMLRules from './rules' -import { Hint, Rule, Ruleset } from './types' +import { Hint, isRuleSeverity, Rule, Ruleset, RuleSeverity } from './types' export interface FormatOptions { colors?: boolean @@ -11,16 +11,16 @@ export interface FormatOptions { class HTMLHintCore { public rules: { [id: string]: Rule } = {} public readonly defaultRuleset: Ruleset = { - 'tagname-lowercase': true, - 'attr-lowercase': true, - 'attr-value-double-quotes': true, - 'doctype-first': true, - 'tag-pair': true, - 'spec-char-escape': true, - 'id-unique': true, - 'src-not-empty': true, - 'attr-no-duplication': true, - 'title-require': true, + 'tagname-lowercase': 'error', + 'attr-lowercase': 'error', + 'attr-value-double-quotes': 'error', + 'doctype-first': 'error', + 'tag-pair': 'error', + 'spec-char-escape': 'error', + 'id-unique': 'error', + 'src-not-empty': 'error', + 'attr-no-duplication': 'error', + 'title-require': 'error', } public addRule(rule: Rule) { @@ -37,18 +37,17 @@ class HTMLHintCore { /^\s*/i, (all, strRuleset: string) => { // For example: - // all is '' - // strRuleset is 'alt-require:true' + // all is '' + // strRuleset is 'alt-require:warn' strRuleset.replace( /(?:^|,)\s*([^:,]+)\s*(?:\:\s*([^,\s]+))?/g, (all, ruleId: string, value: string | undefined) => { // For example: - // all is 'alt-require:true' + // all is 'alt-require:warn' // ruleId is 'alt-require' - // value is 'true' + // value is 'warn' - ruleset[ruleId] = - value !== undefined && value.length > 0 ? JSON.parse(value) : true + ruleset[ruleId] = isRuleSeverity(value) ? value : 'error' return '' } @@ -66,8 +65,19 @@ class HTMLHintCore { for (const id in ruleset) { rule = rules[id] - if (rule !== undefined && ruleset[id] !== false) { - rule.init(parser, reporter, ruleset[id]) + const ruleConfig = ruleset[id] + const ruleSeverity: RuleSeverity = Array.isArray(ruleConfig) + ? ruleConfig[0] + : ruleConfig + if (rule !== undefined && ruleSeverity !== 'off') { + const reportMessageCallback: ReportMessageCallback = reporter[ + ruleSeverity + ].bind(reporter) + rule.init( + parser, + reportMessageCallback, + Array.isArray(ruleConfig) ? ruleConfig[1] : undefined + ) } } diff --git a/src/core/reporter.ts b/src/core/reporter.ts index de85f2976..5e7c6b110 100644 --- a/src/core/reporter.ts +++ b/src/core/reporter.ts @@ -1,5 +1,13 @@ import { Hint, ReportType, Rule, Ruleset } from './types' +export type ReportMessageCallback = ( + message: string, + line: number, + col: number, + rule: Rule, + raw: string +) => void + export default class Reporter { public html: string public lines: string[] diff --git a/src/core/rules/alt-require.ts b/src/core/rules/alt-require.ts index cc51c4a9a..49716c5d1 100644 --- a/src/core/rules/alt-require.ts +++ b/src/core/rules/alt-require.ts @@ -4,7 +4,7 @@ export default { id: 'alt-require', description: 'The alt attribute of an element must be present and alt attribute of area[href] and input[type=image] must have a value.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const tagName = event.tagName.toLowerCase() const mapAttrs = parser.getMapAttrs(event.attrs) @@ -12,7 +12,7 @@ export default { let selector if (tagName === 'img' && !('alt' in mapAttrs)) { - reporter.warn( + reportMessageCallback( 'An alt attribute must be present on elements.', event.line, col, @@ -25,7 +25,7 @@ export default { ) { if (!('alt' in mapAttrs) || mapAttrs['alt'] === '') { selector = tagName === 'area' ? 'area[href]' : 'input[type=image]' - reporter.warn( + reportMessageCallback( `The alt attribute of ${selector} must have a value.`, event.line, col, diff --git a/src/core/rules/attr-lowercase.ts b/src/core/rules/attr-lowercase.ts index d8fb51d28..e910ddc40 100644 --- a/src/core/rules/attr-lowercase.ts +++ b/src/core/rules/attr-lowercase.ts @@ -1,4 +1,4 @@ -import { Rule } from '../types' +import { Rule, RuleConfig } from '../types' /** * testAgainstStringOrRegExp @@ -42,8 +42,12 @@ function testAgainstStringOrRegExp(value: string, comparison: string | RegExp) { export default { id: 'attr-lowercase', description: 'All attribute names must be in lowercase.', - init(parser, reporter, options) { - const exceptions = Array.isArray(options) ? options : [] + init( + parser, + reportMessageCallback, + options?: { exceptions: Array } + ) { + const exceptions = options?.exceptions ?? [] parser.addListener('tagstart', (event) => { const attrs = event.attrs @@ -58,7 +62,7 @@ export default { !exceptions.find((exp) => testAgainstStringOrRegExp(attrName, exp)) && attrName !== attrName.toLowerCase() ) { - reporter.error( + reportMessageCallback( `The attribute name of [ ${attrName} ] must be in lowercase.`, event.line, col + attr.index, diff --git a/src/core/rules/attr-no-duplication.ts b/src/core/rules/attr-no-duplication.ts index f7b4e4d9c..8545715b1 100644 --- a/src/core/rules/attr-no-duplication.ts +++ b/src/core/rules/attr-no-duplication.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-no-duplication', description: 'Elements cannot have duplicate attributes.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const attrs = event.attrs let attr @@ -17,7 +17,7 @@ export default { attrName = attr.name if (mapAttrName[attrName] === true) { - reporter.error( + reportMessageCallback( `Duplicate of attribute name [ ${attr.name} ] was found.`, event.line, col + attr.index, diff --git a/src/core/rules/attr-no-unnecessary-whitespace.ts b/src/core/rules/attr-no-unnecessary-whitespace.ts index bacc0f2e5..82a7d3844 100644 --- a/src/core/rules/attr-no-unnecessary-whitespace.ts +++ b/src/core/rules/attr-no-unnecessary-whitespace.ts @@ -3,8 +3,8 @@ import { Rule } from '../types' export default { id: 'attr-no-unnecessary-whitespace', description: 'No spaces between attribute names and values.', - init(parser, reporter, options) { - const exceptions: string[] = Array.isArray(options) ? options : [] + init(parser, reportMessageCallback, options?: { exceptions: string[] }) { + const exceptions: string[] = options?.exceptions ?? [] parser.addListener('tagstart', (event) => { const attrs = event.attrs @@ -14,7 +14,7 @@ export default { if (exceptions.indexOf(attrs[i].name) === -1) { const match = /(\s*)=(\s*)/.exec(attrs[i].raw.trim()) if (match && (match[1].length !== 0 || match[2].length !== 0)) { - reporter.error( + reportMessageCallback( `The attribute '${attrs[i].name}' must not have spaces between the name and value.`, event.line, col + attrs[i].index, diff --git a/src/core/rules/attr-sorted.ts b/src/core/rules/attr-sorted.ts index a1f85df98..c311717d0 100644 --- a/src/core/rules/attr-sorted.ts +++ b/src/core/rules/attr-sorted.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-sorted', description: 'Attribute tags must be in proper order.', - init(parser, reporter) { + init(parser, reportMessageCallback) { const orderMap: { [key: string]: number } = {} const sortOrder = [ 'class', @@ -45,7 +45,7 @@ export default { }) if (originalAttrs !== JSON.stringify(listOfAttributes)) { - reporter.error( + reportMessageCallback( `Inaccurate order ${originalAttrs} should be in hierarchy ${JSON.stringify( listOfAttributes )} `, diff --git a/src/core/rules/attr-unsafe-chars.ts b/src/core/rules/attr-unsafe-chars.ts index c41b3c6d6..c1df80c75 100644 --- a/src/core/rules/attr-unsafe-chars.ts +++ b/src/core/rules/attr-unsafe-chars.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-unsafe-chars', description: 'Attribute values cannot contain unsafe chars.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const attrs = event.attrs let attr @@ -21,7 +21,7 @@ export default { const unsafeCode = escape(match[0]) .replace(/%u/, '\\u') .replace(/%/, '\\x') - reporter.warn( + reportMessageCallback( `The value of attribute [ ${attr.name} ] cannot contain an unsafe char [ ${unsafeCode} ].`, event.line, col + attr.index, diff --git a/src/core/rules/attr-value-double-quotes.ts b/src/core/rules/attr-value-double-quotes.ts index 659c8f946..e6c2cef33 100644 --- a/src/core/rules/attr-value-double-quotes.ts +++ b/src/core/rules/attr-value-double-quotes.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-value-double-quotes', description: 'Attribute values must be in double quotes.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const attrs = event.attrs let attr @@ -16,7 +16,7 @@ export default { (attr.value !== '' && attr.quote !== '"') || (attr.value === '' && attr.quote === "'") ) { - reporter.error( + reportMessageCallback( `The value of attribute [ ${attr.name} ] must be in double quotes.`, event.line, col + attr.index, diff --git a/src/core/rules/attr-value-not-empty.ts b/src/core/rules/attr-value-not-empty.ts index 65157af79..9a2735064 100644 --- a/src/core/rules/attr-value-not-empty.ts +++ b/src/core/rules/attr-value-not-empty.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-value-not-empty', description: 'All attributes must have values.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const attrs = event.attrs let attr @@ -13,7 +13,7 @@ export default { attr = attrs[i] if (attr.quote === '' && attr.value === '') { - reporter.warn( + reportMessageCallback( `The attribute [ ${attr.name} ] must have a value.`, event.line, col + attr.index, diff --git a/src/core/rules/attr-value-single-quotes.ts b/src/core/rules/attr-value-single-quotes.ts index 740c2616a..59d900734 100644 --- a/src/core/rules/attr-value-single-quotes.ts +++ b/src/core/rules/attr-value-single-quotes.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-value-single-quotes', description: 'Attribute values must be in single quotes.', - init(parser, reporter) { + init(parser, reportMessageCallback) { parser.addListener('tagstart', (event) => { const attrs = event.attrs let attr @@ -16,7 +16,7 @@ export default { (attr.value !== '' && attr.quote !== "'") || (attr.value === '' && attr.quote === '"') ) { - reporter.error( + reportMessageCallback( `The value of attribute [ ${attr.name} ] must be in single quotes.`, event.line, col + attr.index, diff --git a/src/core/rules/attr-whitespace.ts b/src/core/rules/attr-whitespace.ts index 0a2494e80..e53b13b43 100644 --- a/src/core/rules/attr-whitespace.ts +++ b/src/core/rules/attr-whitespace.ts @@ -4,10 +4,8 @@ export default { id: 'attr-whitespace', description: 'All attributes should be separated by only one space and not have leading/trailing whitespace.', - init(parser, reporter, options) { - const exceptions: Array = Array.isArray(options) - ? options - : [] + init(parser, reportMessageCallback, options?: { exceptions: string[] }) { + const exceptions: string[] = options?.exceptions ?? [] parser.addListener('tagstart', (event) => { const attrs = event.attrs @@ -24,7 +22,7 @@ export default { // Check first and last characters for spaces if (elem.value.trim() !== elem.value) { - reporter.error( + reportMessageCallback( `The attributes of [ ${attrName} ] must not have trailing whitespace.`, event.line, col + attr.index, @@ -34,7 +32,7 @@ export default { } if (elem.value.replace(/ +(?= )/g, '') !== elem.value) { - reporter.error( + reportMessageCallback( `The attributes of [ ${attrName} ] must be separated by only one space.`, event.line, col + attr.index, diff --git a/src/core/rules/doctype-first.ts b/src/core/rules/doctype-first.ts index 1f6286181..61c847e32 100644 --- a/src/core/rules/doctype-first.ts +++ b/src/core/rules/doctype-first.ts @@ -4,7 +4,7 @@ import { Rule } from '../types' export default { id: 'doctype-first', description: 'Doctype must be declared first.', - init(parser, reporter) { + init(parser, reportMessageCallback) { const allEvent: Listener = (event) => { if ( event.type === 'start' || @@ -17,7 +17,7 @@ export default { (event.type !== 'comment' && event.long === false) || /^DOCTYPE\s+/i.test(event.content) === false ) { - reporter.error( + reportMessageCallback( 'Doctype must be declared first.', event.line, event.col, diff --git a/src/core/rules/doctype-html5.ts b/src/core/rules/doctype-html5.ts index c6e438ddc..30c9d9fca 100644 --- a/src/core/rules/doctype-html5.ts +++ b/src/core/rules/doctype-html5.ts @@ -4,13 +4,13 @@ import { Rule } from '../types' export default { id: 'doctype-html5', description: 'Invalid doctype. Use: ""', - init(parser, reporter) { + init(parser, reportMessageCallback) { const onComment: Listener = (event) => { if ( event.long === false && event.content.toLowerCase() !== 'doctype html' ) { - reporter.warn( + reportMessageCallback( 'Invalid doctype. Use: ""', event.line, event.col, diff --git a/src/core/rules/head-script-disabled.ts b/src/core/rules/head-script-disabled.ts index 5b3ab6be7..62e53327a 100644 --- a/src/core/rules/head-script-disabled.ts +++ b/src/core/rules/head-script-disabled.ts @@ -4,7 +4,7 @@ import { Rule } from '../types' export default { id: 'head-script-disabled', description: 'The