Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new configuration structure #445

Merged
merged 9 commits into from
Jun 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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