Skip to content

Commit

Permalink
feat: new configuration structure (#445)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Shinigami committed Jun 30, 2020
1 parent a61befc commit ed7c797
Show file tree
Hide file tree
Showing 67 changed files with 310 additions and 238 deletions.
50 changes: 30 additions & 20 deletions src/core/core.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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) {
Expand All @@ -37,18 +37,17 @@ class HTMLHintCore {
/^\s*<!--\s*htmlhint\s+([^\r\n]+?)\s*-->/i,
(all, strRuleset: string) => {
// For example:
// all is '<!-- htmlhint alt-require:true-->'
// strRuleset is 'alt-require:true'
// all is '<!-- htmlhint alt-require:warn-->'
// 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 ''
}
Expand All @@ -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
)
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/core/reporter.ts
Original file line number Diff line number Diff line change
@@ -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[]
Expand Down
6 changes: 3 additions & 3 deletions src/core/rules/alt-require.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ export default {
id: 'alt-require',
description:
'The alt attribute of an <img> 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)
const col = event.col + tagName.length + 1
let selector

if (tagName === 'img' && !('alt' in mapAttrs)) {
reporter.warn(
reportMessageCallback(
'An alt attribute must be present on <img> elements.',
event.line,
col,
Expand All @@ -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,
Expand Down
12 changes: 8 additions & 4 deletions src/core/rules/attr-lowercase.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Rule } from '../types'
import { Rule, RuleConfig } from '../types'

/**
* testAgainstStringOrRegExp
Expand Down Expand Up @@ -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<string | RegExp> }
) {
const exceptions = options?.exceptions ?? []

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-no-duplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
6 changes: 3 additions & 3 deletions src/core/rules/attr-no-unnecessary-whitespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-sorted.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -45,7 +45,7 @@ export default {
})

if (originalAttrs !== JSON.stringify(listOfAttributes)) {
reporter.error(
reportMessageCallback(
`Inaccurate order ${originalAttrs} should be in hierarchy ${JSON.stringify(
listOfAttributes
)} `,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-unsafe-chars.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-value-double-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-value-not-empty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/attr-value-single-quotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand Down
10 changes: 4 additions & 6 deletions src/core/rules/attr-whitespace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | boolean> = Array.isArray(options)
? options
: []
init(parser, reportMessageCallback, options?: { exceptions: string[] }) {
const exceptions: string[] = options?.exceptions ?? []

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
Expand All @@ -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,
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/doctype-first.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' ||
Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/doctype-html5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import { Rule } from '../types'
export default {
id: 'doctype-html5',
description: 'Invalid doctype. Use: "<!DOCTYPE html>"',
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: "<!DOCTYPE html>"',
event.line,
event.col,
Expand Down
4 changes: 2 additions & 2 deletions src/core/rules/head-script-disabled.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Rule } from '../types'
export default {
id: 'head-script-disabled',
description: 'The <script> tag cannot be used in a <head> tag.',
init(parser, reporter) {
init(parser, reportMessageCallback) {
const reScript = /^(text\/javascript|application\/javascript)$/i
let isInHead = false

Expand All @@ -22,7 +22,7 @@ export default {
tagName === 'script' &&
(!type || reScript.test(type) === true)
) {
reporter.warn(
reportMessageCallback(
'The <script> tag cannot be used in a <head> tag.',
event.line,
event.col,
Expand Down
10 changes: 7 additions & 3 deletions src/core/rules/href-abs-or-rel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,12 @@ import { Rule } from '../types'
export default {
id: 'href-abs-or-rel',
description: 'An href attribute must be either absolute or relative.',
init(parser, reporter, options) {
const hrefMode = options === 'abs' ? 'absolute' : 'relative'
init(
parser,
reportMessageCallback,
options?: { mode: 'absolute' | 'relative' }
) {
const hrefMode = options?.mode ?? 'absolute'

parser.addListener('tagstart', (event) => {
const attrs = event.attrs
Expand All @@ -20,7 +24,7 @@ export default {
(hrefMode === 'relative' &&
/^https?:\/\//.test(attr.value) === true)
) {
reporter.warn(
reportMessageCallback(
`The value of the href attribute [ ${attr.value} ] must be ${hrefMode}.`,
event.line,
col + attr.index,
Expand Down
Loading

0 comments on commit ed7c797

Please sign in to comment.