Skip to content

Commit 91bf3e4

Browse files
author
Shinigami
committed
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
1 parent 1f1f448 commit 91bf3e4

File tree

67 files changed

+310
-238
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+310
-238
lines changed

src/core/core.ts

+30-20
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import HTMLParser from './htmlparser'
2-
import Reporter from './reporter'
2+
import Reporter, { ReportMessageCallback } from './reporter'
33
import * as HTMLRules from './rules'
4-
import { Hint, Rule, Ruleset } from './types'
4+
import { Hint, isRuleSeverity, Rule, Ruleset, RuleSeverity } from './types'
55

66
export interface FormatOptions {
77
colors?: boolean
@@ -11,16 +11,16 @@ export interface FormatOptions {
1111
class HTMLHintCore {
1212
public rules: { [id: string]: Rule } = {}
1313
public readonly defaultRuleset: Ruleset = {
14-
'tagname-lowercase': true,
15-
'attr-lowercase': true,
16-
'attr-value-double-quotes': true,
17-
'doctype-first': true,
18-
'tag-pair': true,
19-
'spec-char-escape': true,
20-
'id-unique': true,
21-
'src-not-empty': true,
22-
'attr-no-duplication': true,
23-
'title-require': true,
14+
'tagname-lowercase': 'error',
15+
'attr-lowercase': 'error',
16+
'attr-value-double-quotes': 'error',
17+
'doctype-first': 'error',
18+
'tag-pair': 'error',
19+
'spec-char-escape': 'error',
20+
'id-unique': 'error',
21+
'src-not-empty': 'error',
22+
'attr-no-duplication': 'error',
23+
'title-require': 'error',
2424
}
2525

2626
public addRule(rule: Rule) {
@@ -37,18 +37,17 @@ class HTMLHintCore {
3737
/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i,
3838
(all, strRuleset: string) => {
3939
// For example:
40-
// all is '<!-- htmlhint alt-require:true-->'
41-
// strRuleset is 'alt-require:true'
40+
// all is '<!-- htmlhint alt-require:warn-->'
41+
// strRuleset is 'alt-require:warn'
4242
strRuleset.replace(
4343
/(?:^|,)\s*([^:,]+)\s*(?:\:\s*([^,\s]+))?/g,
4444
(all, ruleId: string, value: string | undefined) => {
4545
// For example:
46-
// all is 'alt-require:true'
46+
// all is 'alt-require:warn'
4747
// ruleId is 'alt-require'
48-
// value is 'true'
48+
// value is 'warn'
4949

50-
ruleset[ruleId] =
51-
value !== undefined && value.length > 0 ? JSON.parse(value) : true
50+
ruleset[ruleId] = isRuleSeverity(value) ? value : 'error'
5251

5352
return ''
5453
}
@@ -66,8 +65,19 @@ class HTMLHintCore {
6665

6766
for (const id in ruleset) {
6867
rule = rules[id]
69-
if (rule !== undefined && ruleset[id] !== false) {
70-
rule.init(parser, reporter, ruleset[id])
68+
const ruleConfig = ruleset[id]
69+
const ruleSeverity: RuleSeverity = Array.isArray(ruleConfig)
70+
? ruleConfig[0]
71+
: ruleConfig
72+
if (rule !== undefined && ruleSeverity !== 'off') {
73+
const reportMessageCallback: ReportMessageCallback = reporter[
74+
ruleSeverity
75+
].bind(reporter)
76+
rule.init(
77+
parser,
78+
reportMessageCallback,
79+
Array.isArray(ruleConfig) ? ruleConfig[1] : undefined
80+
)
7181
}
7282
}
7383

src/core/reporter.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
import { Hint, ReportType, Rule, Ruleset } from './types'
22

3+
export type ReportMessageCallback = (
4+
message: string,
5+
line: number,
6+
col: number,
7+
rule: Rule,
8+
raw: string
9+
) => void
10+
311
export default class Reporter {
412
public html: string
513
public lines: string[]

src/core/rules/alt-require.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ export default {
44
id: 'alt-require',
55
description:
66
'The alt attribute of an <img> element must be present and alt attribute of area[href] and input[type=image] must have a value.',
7-
init(parser, reporter) {
7+
init(parser, reportMessageCallback) {
88
parser.addListener('tagstart', (event) => {
99
const tagName = event.tagName.toLowerCase()
1010
const mapAttrs = parser.getMapAttrs(event.attrs)
1111
const col = event.col + tagName.length + 1
1212
let selector
1313

1414
if (tagName === 'img' && !('alt' in mapAttrs)) {
15-
reporter.warn(
15+
reportMessageCallback(
1616
'An alt attribute must be present on <img> elements.',
1717
event.line,
1818
col,
@@ -25,7 +25,7 @@ export default {
2525
) {
2626
if (!('alt' in mapAttrs) || mapAttrs['alt'] === '') {
2727
selector = tagName === 'area' ? 'area[href]' : 'input[type=image]'
28-
reporter.warn(
28+
reportMessageCallback(
2929
`The alt attribute of ${selector} must have a value.`,
3030
event.line,
3131
col,

src/core/rules/attr-lowercase.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Rule } from '../types'
1+
import { Rule, RuleConfig } from '../types'
22

33
/**
44
* testAgainstStringOrRegExp
@@ -42,8 +42,12 @@ function testAgainstStringOrRegExp(value: string, comparison: string | RegExp) {
4242
export default {
4343
id: 'attr-lowercase',
4444
description: 'All attribute names must be in lowercase.',
45-
init(parser, reporter, options) {
46-
const exceptions = Array.isArray(options) ? options : []
45+
init(
46+
parser,
47+
reportMessageCallback,
48+
options?: { exceptions: Array<string | RegExp> }
49+
) {
50+
const exceptions = options?.exceptions ?? []
4751

4852
parser.addListener('tagstart', (event) => {
4953
const attrs = event.attrs
@@ -58,7 +62,7 @@ export default {
5862
!exceptions.find((exp) => testAgainstStringOrRegExp(attrName, exp)) &&
5963
attrName !== attrName.toLowerCase()
6064
) {
61-
reporter.error(
65+
reportMessageCallback(
6266
`The attribute name of [ ${attrName} ] must be in lowercase.`,
6367
event.line,
6468
col + attr.index,

src/core/rules/attr-no-duplication.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-no-duplication',
55
description: 'Elements cannot have duplicate attributes.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
parser.addListener('tagstart', (event) => {
88
const attrs = event.attrs
99
let attr
@@ -17,7 +17,7 @@ export default {
1717
attrName = attr.name
1818

1919
if (mapAttrName[attrName] === true) {
20-
reporter.error(
20+
reportMessageCallback(
2121
`Duplicate of attribute name [ ${attr.name} ] was found.`,
2222
event.line,
2323
col + attr.index,

src/core/rules/attr-no-unnecessary-whitespace.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-no-unnecessary-whitespace',
55
description: 'No spaces between attribute names and values.',
6-
init(parser, reporter, options) {
7-
const exceptions: string[] = Array.isArray(options) ? options : []
6+
init(parser, reportMessageCallback, options?: { exceptions: string[] }) {
7+
const exceptions: string[] = options?.exceptions ?? []
88

99
parser.addListener('tagstart', (event) => {
1010
const attrs = event.attrs
@@ -14,7 +14,7 @@ export default {
1414
if (exceptions.indexOf(attrs[i].name) === -1) {
1515
const match = /(\s*)=(\s*)/.exec(attrs[i].raw.trim())
1616
if (match && (match[1].length !== 0 || match[2].length !== 0)) {
17-
reporter.error(
17+
reportMessageCallback(
1818
`The attribute '${attrs[i].name}' must not have spaces between the name and value.`,
1919
event.line,
2020
col + attrs[i].index,

src/core/rules/attr-sorted.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-sorted',
55
description: 'Attribute tags must be in proper order.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
const orderMap: { [key: string]: number } = {}
88
const sortOrder = [
99
'class',
@@ -45,7 +45,7 @@ export default {
4545
})
4646

4747
if (originalAttrs !== JSON.stringify(listOfAttributes)) {
48-
reporter.error(
48+
reportMessageCallback(
4949
`Inaccurate order ${originalAttrs} should be in hierarchy ${JSON.stringify(
5050
listOfAttributes
5151
)} `,

src/core/rules/attr-unsafe-chars.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-unsafe-chars',
55
description: 'Attribute values cannot contain unsafe chars.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
parser.addListener('tagstart', (event) => {
88
const attrs = event.attrs
99
let attr
@@ -21,7 +21,7 @@ export default {
2121
const unsafeCode = escape(match[0])
2222
.replace(/%u/, '\\u')
2323
.replace(/%/, '\\x')
24-
reporter.warn(
24+
reportMessageCallback(
2525
`The value of attribute [ ${attr.name} ] cannot contain an unsafe char [ ${unsafeCode} ].`,
2626
event.line,
2727
col + attr.index,

src/core/rules/attr-value-double-quotes.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-value-double-quotes',
55
description: 'Attribute values must be in double quotes.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
parser.addListener('tagstart', (event) => {
88
const attrs = event.attrs
99
let attr
@@ -16,7 +16,7 @@ export default {
1616
(attr.value !== '' && attr.quote !== '"') ||
1717
(attr.value === '' && attr.quote === "'")
1818
) {
19-
reporter.error(
19+
reportMessageCallback(
2020
`The value of attribute [ ${attr.name} ] must be in double quotes.`,
2121
event.line,
2222
col + attr.index,

src/core/rules/attr-value-not-empty.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-value-not-empty',
55
description: 'All attributes must have values.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
parser.addListener('tagstart', (event) => {
88
const attrs = event.attrs
99
let attr
@@ -13,7 +13,7 @@ export default {
1313
attr = attrs[i]
1414

1515
if (attr.quote === '' && attr.value === '') {
16-
reporter.warn(
16+
reportMessageCallback(
1717
`The attribute [ ${attr.name} ] must have a value.`,
1818
event.line,
1919
col + attr.index,

src/core/rules/attr-value-single-quotes.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Rule } from '../types'
33
export default {
44
id: 'attr-value-single-quotes',
55
description: 'Attribute values must be in single quotes.',
6-
init(parser, reporter) {
6+
init(parser, reportMessageCallback) {
77
parser.addListener('tagstart', (event) => {
88
const attrs = event.attrs
99
let attr
@@ -16,7 +16,7 @@ export default {
1616
(attr.value !== '' && attr.quote !== "'") ||
1717
(attr.value === '' && attr.quote === '"')
1818
) {
19-
reporter.error(
19+
reportMessageCallback(
2020
`The value of attribute [ ${attr.name} ] must be in single quotes.`,
2121
event.line,
2222
col + attr.index,

src/core/rules/attr-whitespace.ts

+4-6
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,8 @@ export default {
44
id: 'attr-whitespace',
55
description:
66
'All attributes should be separated by only one space and not have leading/trailing whitespace.',
7-
init(parser, reporter, options) {
8-
const exceptions: Array<string | boolean> = Array.isArray(options)
9-
? options
10-
: []
7+
init(parser, reportMessageCallback, options?: { exceptions: string[] }) {
8+
const exceptions: string[] = options?.exceptions ?? []
119

1210
parser.addListener('tagstart', (event) => {
1311
const attrs = event.attrs
@@ -24,7 +22,7 @@ export default {
2422

2523
// Check first and last characters for spaces
2624
if (elem.value.trim() !== elem.value) {
27-
reporter.error(
25+
reportMessageCallback(
2826
`The attributes of [ ${attrName} ] must not have trailing whitespace.`,
2927
event.line,
3028
col + attr.index,
@@ -34,7 +32,7 @@ export default {
3432
}
3533

3634
if (elem.value.replace(/ +(?= )/g, '') !== elem.value) {
37-
reporter.error(
35+
reportMessageCallback(
3836
`The attributes of [ ${attrName} ] must be separated by only one space.`,
3937
event.line,
4038
col + attr.index,

src/core/rules/doctype-first.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Rule } from '../types'
44
export default {
55
id: 'doctype-first',
66
description: 'Doctype must be declared first.',
7-
init(parser, reporter) {
7+
init(parser, reportMessageCallback) {
88
const allEvent: Listener = (event) => {
99
if (
1010
event.type === 'start' ||
@@ -17,7 +17,7 @@ export default {
1717
(event.type !== 'comment' && event.long === false) ||
1818
/^DOCTYPE\s+/i.test(event.content) === false
1919
) {
20-
reporter.error(
20+
reportMessageCallback(
2121
'Doctype must be declared first.',
2222
event.line,
2323
event.col,

src/core/rules/doctype-html5.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@ import { Rule } from '../types'
44
export default {
55
id: 'doctype-html5',
66
description: 'Invalid doctype. Use: "<!DOCTYPE html>"',
7-
init(parser, reporter) {
7+
init(parser, reportMessageCallback) {
88
const onComment: Listener = (event) => {
99
if (
1010
event.long === false &&
1111
event.content.toLowerCase() !== 'doctype html'
1212
) {
13-
reporter.warn(
13+
reportMessageCallback(
1414
'Invalid doctype. Use: "<!DOCTYPE html>"',
1515
event.line,
1616
event.col,

src/core/rules/head-script-disabled.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { Rule } from '../types'
44
export default {
55
id: 'head-script-disabled',
66
description: 'The <script> tag cannot be used in a <head> tag.',
7-
init(parser, reporter) {
7+
init(parser, reportMessageCallback) {
88
const reScript = /^(text\/javascript|application\/javascript)$/i
99
let isInHead = false
1010

@@ -22,7 +22,7 @@ export default {
2222
tagName === 'script' &&
2323
(!type || reScript.test(type) === true)
2424
) {
25-
reporter.warn(
25+
reportMessageCallback(
2626
'The <script> tag cannot be used in a <head> tag.',
2727
event.line,
2828
event.col,

src/core/rules/href-abs-or-rel.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@ import { Rule } from '../types'
33
export default {
44
id: 'href-abs-or-rel',
55
description: 'An href attribute must be either absolute or relative.',
6-
init(parser, reporter, options) {
7-
const hrefMode = options === 'abs' ? 'absolute' : 'relative'
6+
init(
7+
parser,
8+
reportMessageCallback,
9+
options?: { mode: 'absolute' | 'relative' }
10+
) {
11+
const hrefMode = options?.mode ?? 'absolute'
812

913
parser.addListener('tagstart', (event) => {
1014
const attrs = event.attrs
@@ -20,7 +24,7 @@ export default {
2024
(hrefMode === 'relative' &&
2125
/^https?:\/\//.test(attr.value) === true)
2226
) {
23-
reporter.warn(
27+
reportMessageCallback(
2428
`The value of the href attribute [ ${attr.value} ] must be ${hrefMode}.`,
2529
event.line,
2630
col + attr.index,

0 commit comments

Comments
 (0)