From da12822a13fcefdb1c7b827d70d19ecb9cf675cc Mon Sep 17 00:00:00 2001 From: Christopher Quadflieg Date: Tue, 2 Jun 2020 16:32:50 +0200 Subject: [PATCH 1/9] feat: rebuild ruleset interface --- src/core/types.ts | 103 ++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/src/core/types.ts b/src/core/types.ts index b7d230076..e46b658f6 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -7,57 +7,62 @@ export interface Rule { init(parser: HTMLParser, reporter: Reporter, options: unknown): void } -export interface Ruleset { - 'alt-require'?: boolean - 'attr-lowercase'?: boolean | Array - 'attr-no-duplication'?: boolean - 'attr-no-unnecessary-whitespace'?: boolean - 'attr-sorted'?: boolean - 'attr-unsafe-chars'?: boolean - 'attr-value-double-quotes'?: boolean - 'attr-value-not-empty'?: boolean - 'attr-value-single-quotes'?: boolean - 'attr-whitespace'?: boolean - 'doctype-first'?: boolean - 'doctype-html5'?: boolean - 'head-script-disabled'?: boolean - 'href-abs-or-rel'?: 'abs' | 'rel' - 'id-class-ad-disabled'?: boolean - 'id-class-value'?: - | 'underline' - | 'dash' - | 'hump' - | { regId: RegExp; message: string } - 'id-unique'?: boolean - 'inline-script-disabled'?: boolean - 'inline-style-disabled'?: boolean - 'input-requires-label'?: boolean - 'script-disabled'?: boolean - 'space-tab-mixed-disabled'?: - | boolean - | 'space' - | 'space1' - | 'space2' - | 'space3' - | 'space4' - | 'space5' - | 'space6' - | 'space7' - | 'space8' - | 'tab' - 'spec-char-escape'?: boolean - 'src-not-empty'?: boolean - 'style-disabled'?: boolean - 'tag-pair'?: boolean - 'tag-self-close'?: boolean - 'tagname-lowercase'?: boolean - 'tagname-specialchars'?: boolean - 'tags-check'?: { [tagName: string]: Record } - 'title-require'?: boolean - // There may be other unknown rules - [ruleId: string]: unknown +export type RuleSeverity = 'off' | 'warn' | 'error' +export type RuleConfig> = + | RuleSeverity + | [RuleSeverity, Options | undefined] + +export interface BaseRuleset { + 'alt-require'?: RuleConfig + 'attr-lowercase'?: RuleConfig<{ exceptions: Array }> + 'attr-no-duplication'?: RuleConfig + 'attr-no-unnecessary-whitespace'?: RuleConfig + 'attr-sorted'?: RuleConfig + 'attr-unsafe-chars'?: RuleConfig + 'attr-value-double-quotes'?: RuleConfig + 'attr-value-not-empty'?: RuleConfig + 'attr-value-single-quotes'?: RuleConfig + 'attr-whitespace'?: RuleConfig + 'doctype-first'?: RuleConfig + 'doctype-html5'?: RuleConfig + 'head-script-disabled'?: RuleConfig + 'href-abs-or-rel'?: RuleConfig<{ mode: 'absolute' | 'relative' }> + 'id-class-ad-disabled'?: RuleConfig + 'id-class-value'?: RuleConfig<{ + mode: 'underline' | 'dash' | 'hump' | { regId: RegExp; message: string } + }> + 'id-unique'?: RuleConfig + 'inline-script-disabled'?: RuleConfig + 'inline-style-disabled'?: RuleConfig + 'input-requires-label'?: RuleConfig + 'script-disabled'?: RuleConfig + 'space-tab-mixed-disabled'?: RuleConfig< + { mode: 'tab' } | { mode: 'space'; size?: number } + > + 'spec-char-escape'?: RuleConfig + 'src-not-empty'?: RuleConfig + 'style-disabled'?: RuleConfig + 'tag-pair'?: RuleConfig + 'tag-self-close'?: RuleConfig + 'tagname-lowercase'?: RuleConfig + 'tagname-specialchars'?: RuleConfig + 'tags-check'?: RuleConfig<{ + [tagName: string]: { + selfclosing?: boolean + redundantAttrs?: string[] + attrsRequired?: string[] + attrsOptional?: string[][] + } + }> + 'title-require'?: RuleConfig +} + +export interface CustomRuleset { + [ruleId: string]: RuleConfig } +export type Ruleset = BaseRuleset & CustomRuleset + export const enum ReportType { error = 'error', warning = 'warning', From 916be791ae3b70cc31011f1b8ea5b8c157b44601 Mon Sep 17 00:00:00 2001 From: Christopher Quadflieg Date: Tue, 2 Jun 2020 16:37:59 +0200 Subject: [PATCH 2/9] feat: update default ruleset --- src/core/core.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/core.ts b/src/core/core.ts index 6299880f3..3694ad785 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -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) { From a5b8c2e4d2f6329b00eacf700e200c3db502c53a Mon Sep 17 00:00:00 2001 From: Christopher Quadflieg Date: Tue, 2 Jun 2020 16:42:47 +0200 Subject: [PATCH 3/9] feat: check is rule enabled --- src/core/core.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/core.ts b/src/core/core.ts index 3694ad785..75d6a4c0a 100644 --- a/src/core/core.ts +++ b/src/core/core.ts @@ -66,7 +66,11 @@ class HTMLHintCore { for (const id in ruleset) { rule = rules[id] - if (rule !== undefined && ruleset[id] !== false) { + if ( + rule !== undefined && + (ruleset[id] !== 'off' || + (Array.isArray(ruleset[id]) && ruleset[id] !== 'off')) + ) { rule.init(parser, reporter, ruleset[id]) } } From 160f11e977d838b22585d16ab4c95d78e9a410f9 Mon Sep 17 00:00:00 2001 From: Christopher Quadflieg Date: Tue, 2 Jun 2020 17:23:17 +0200 Subject: [PATCH 4/9] feat: pass reporter message callback to init --- src/core/core.ts | 18 +++++++++-------- src/core/reporter.ts | 8 ++++++++ src/core/rules/alt-require.ts | 6 +++--- src/core/rules/attr-lowercase.ts | 4 ++-- src/core/rules/attr-no-duplication.ts | 4 ++-- .../rules/attr-no-unnecessary-whitespace.ts | 4 ++-- 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 | 6 +++--- 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 | 4 ++-- src/core/rules/id-class-ad-disabled.ts | 4 ++-- src/core/rules/id-class-value.ts | 6 +++--- 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 | 10 +++++----- 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 | 4 ++-- 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 | 9 +++++++-- 34 files changed, 107 insertions(+), 88 deletions(-) diff --git a/src/core/core.ts b/src/core/core.ts index 75d6a4c0a..a845b949a 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, Rule, Ruleset, RuleSeverity } from './types' export interface FormatOptions { colors?: boolean @@ -66,12 +66,14 @@ class HTMLHintCore { for (const id in ruleset) { rule = rules[id] - if ( - rule !== undefined && - (ruleset[id] !== 'off' || - (Array.isArray(ruleset[id]) && ruleset[id] !== 'off')) - ) { - 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] + rule.init(parser, reportMessageCallback, ruleConfig) } } 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..cd3b1b5cb 100644 --- a/src/core/rules/attr-lowercase.ts +++ b/src/core/rules/attr-lowercase.ts @@ -42,7 +42,7 @@ function testAgainstStringOrRegExp(value: string, comparison: string | RegExp) { export default { id: 'attr-lowercase', description: 'All attribute names must be in lowercase.', - init(parser, reporter, options) { + init(parser, reportMessageCallback, options) { const exceptions = Array.isArray(options) ? options : [] parser.addListener('tagstart', (event) => { @@ -58,7 +58,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..3a9e46e8a 100644 --- a/src/core/rules/attr-no-unnecessary-whitespace.ts +++ b/src/core/rules/attr-no-unnecessary-whitespace.ts @@ -3,7 +3,7 @@ import { Rule } from '../types' export default { id: 'attr-no-unnecessary-whitespace', description: 'No spaces between attribute names and values.', - init(parser, reporter, options) { + init(parser, reportMessageCallback, options) { const exceptions: string[] = Array.isArray(options) ? options : [] parser.addListener('tagstart', (event) => { @@ -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..7c4eaec70 100644 --- a/src/core/rules/attr-whitespace.ts +++ b/src/core/rules/attr-whitespace.ts @@ -4,7 +4,7 @@ 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) { + init(parser, reportMessageCallback, options) { const exceptions: Array = Array.isArray(options) ? options : [] @@ -24,7 +24,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 +34,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