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: add config attribute extends #489

Open
wants to merge 9 commits into
base: beta
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ lib
# Test releated files
test/unit/coverage
test/e2e/reports
report.html

# Website/Docs releated files
/website/node_modules
Expand Down
4 changes: 2 additions & 2 deletions src/cli/formatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { EventEmitter } from 'events'
import { sync as globSync } from 'glob'
import { parse, resolve } from 'path'
import type { HTMLHint as IHTMLHint } from '../core/core'
import type { Hint, Ruleset } from '../core/types'
import type { Configuration, Hint } from '../core/types'

let HTMLHint: typeof IHTMLHint
let options: { nocolor?: boolean }
Expand Down Expand Up @@ -45,7 +45,7 @@ export interface FormatterFileEvent {
}

export interface FormatterConfigEvent {
ruleset: Ruleset
config?: Configuration
configPath?: string
}

Expand Down
53 changes: 29 additions & 24 deletions src/cli/htmlhint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { dirname, resolve, sep } from 'path'
import * as request from 'request'
import * as stripJsonComments from 'strip-json-comments'
import type { HTMLHint as IHTMLHint } from '../core/core'
import type { Hint, Ruleset } from '../core/types'
import type { Configuration, Hint } from '../core/types'
import { Formatter } from './formatter'

const HTMLHint: typeof IHTMLHint = require('../htmlhint.js').HTMLHint
Expand Down Expand Up @@ -96,9 +96,11 @@ if (format) {
formatter.setFormat(format)
}

// TODO: parse and validate `program.rules`

hintTargets(arrTargets, {
rulesdir: program.rulesdir,
ruleset: program.rules,
config: program.rules ? { rules: program.rules } : undefined,
formatter: formatter,
ignore: program.ignore,
})
Expand All @@ -121,7 +123,7 @@ function hintTargets(
arrTargets: string[],
options: {
formatter: Formatter
ruleset?: Ruleset
config?: Configuration
rulesdir?: string
ignore?: string
}
Expand Down Expand Up @@ -213,7 +215,7 @@ function hintAllFiles(
options: {
ignore?: string
formatter: Formatter
ruleset?: Ruleset
config?: Configuration
},
onFinised: (result: {
targetFileCount: number
Expand Down Expand Up @@ -241,22 +243,22 @@ function hintAllFiles(
time: number
}> = []

// init ruleset
let ruleset = options.ruleset
if (ruleset === undefined) {
ruleset = getConfig(program.config, globInfo.base, formatter)
// init config
let config = options.config
if (config === undefined) {
config = getConfig(program.config, globInfo.base, formatter)
}

// hint queue
const hintQueue = asyncQueue<string>((filepath, next) => {
const startTime = new Date().getTime()

if (filepath === 'stdin') {
hintStdin(ruleset, hintNext)
hintStdin(config, hintNext)
} else if (/^https?:\/\//.test(filepath)) {
hintUrl(filepath, ruleset, hintNext)
hintUrl(filepath, config, hintNext)
} else {
const messages = hintFile(filepath, ruleset)
const messages = hintFile(filepath, config)
hintNext(messages)
}

Expand Down Expand Up @@ -370,14 +372,15 @@ function getConfig(
configPath: string | undefined,
base: string,
formatter: Formatter
) {
): Configuration | undefined {
if (configPath === undefined && existsSync(base)) {
// find default config file in parent directory
if (statSync(base).isDirectory() === false) {
base = dirname(base)
}

while (base) {
// TODO: load via cosmiconfig (https://github.com/htmlhint/HTMLHint/issues/126)
const tmpConfigFile = resolve(base, '.htmlhintrc')

if (existsSync(tmpConfigFile)) {
Expand All @@ -395,20 +398,22 @@ function getConfig(

// TODO: can configPath be undefined here?
if (configPath !== undefined && existsSync(configPath)) {
const config = readFileSync(configPath, 'utf-8')
let ruleset: Ruleset = {}
const configContent = readFileSync(configPath, 'utf-8')
let config: Configuration | undefined

try {
ruleset = JSON.parse(stripJsonComments(config))
config = JSON.parse(stripJsonComments(configContent))
formatter.emit('config', {
ruleset: ruleset,
configPath: configPath,
configPath,
config,
})
} catch (e) {
// ignore
}

return ruleset
// TODO: validate config

return config
}
}

Expand Down Expand Up @@ -456,7 +461,7 @@ function walkPath(
}

// hint file
function hintFile(filepath: string, ruleset?: Ruleset) {
function hintFile(filepath: string, config?: Configuration) {
let content = ''

try {
Expand All @@ -465,12 +470,12 @@ function hintFile(filepath: string, ruleset?: Ruleset) {
// ignore
}

return HTMLHint.verify(content, { rules: ruleset })
return HTMLHint.verify(content, config)
}

// hint stdin
function hintStdin(
ruleset: Ruleset | undefined,
config: Configuration | undefined,
callback: (messages: Hint[]) => void
) {
process.stdin.setEncoding('utf8')
Expand All @@ -483,20 +488,20 @@ function hintStdin(

process.stdin.on('end', () => {
const content = buffers.join('')
const messages = HTMLHint.verify(content, { rules: ruleset })
const messages = HTMLHint.verify(content, config)
callback(messages)
})
}

// hint url
function hintUrl(
url: string,
ruleset: Ruleset | undefined,
config: Configuration | undefined,
callback: (messages: Hint[]) => void
) {
request.get(url, (error, response, body) => {
if (!error && response.statusCode == 200) {
const messages = HTMLHint.verify(body, { rules: ruleset })
const messages = HTMLHint.verify(body, config)
callback(messages)
} else {
callback([])
Expand Down
39 changes: 39 additions & 0 deletions src/core/configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Configuration } from './types'

export function isConfiguration(value: unknown): value is Configuration {
// Config must be an object
if (typeof value !== 'object') {
return false
}

// Config must not be null
if (value === null) {
return false
}

// This helps to get better support for TypeScript
const config = value as Record<string, unknown>

// If extends is defined, it must be an array
if (config.extends !== undefined) {
if (!Array.isArray(config.extends)) {
return false
}

// Any value within extens must be a string
for (const extension of config.extends) {
if (typeof extension !== 'string') {
return false
}
}
}

// If extends is defined, it must be an object
if (config.rules !== undefined) {
if (typeof config.rules !== 'object' && config.rules !== null) {
return false
}
}

return true
}
69 changes: 58 additions & 11 deletions src/core/core.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isConfiguration } from './configuration'
import HTMLParser from './htmlparser'
import Reporter, { ReportMessageCallback } from './reporter'
import * as HTMLRules from './rules'
Expand All @@ -15,32 +16,78 @@ export interface FormatOptions {
indent?: number
}

class HTMLHintCore {
public rules: { [id: string]: Rule } = {}
public readonly defaultRuleset: Ruleset = {
'tagname-lowercase': 'error',
const HTMLHINT_RECOMMENDED = 'htmlhint:recommended'
const HTMLHINT_LEGACY = 'htmlhint:legacy'

const DEFAULT_RULESETS: Record<string, Ruleset> = {
[HTMLHINT_RECOMMENDED]: {
'alt-require': 'warn',
'attr-lowercase': 'warn',
'attr-no-duplication': 'error',
'attr-no-unnecessary-whitespace': 'warn',
'attr-unsafe-chars': 'warn',
'attr-value-double-quotes': 'warn',
'id-class-ad-disabled': 'warn',
'id-unique': 'error',
'space-tab-mixed-disabled': 'warn',
'spec-char-escape': 'warn',
'src-not-empty': 'error',
'tag-pair': 'warn',
'tagname-lowercase': 'warn',
'tagname-specialchars': 'error',
'title-require': 'warn',
},
[HTMLHINT_LEGACY]: {
'attr-lowercase': 'error',
'attr-no-duplication': 'error',
'attr-value-double-quotes': 'error',
'doctype-first': 'error',
'tag-pair': 'error',
'spec-char-escape': 'error',
'id-unique': 'error',
'spec-char-escape': 'error',
'src-not-empty': 'error',
'attr-no-duplication': 'error',
'tag-pair': 'error',
'tagname-lowercase': 'error',
'title-require': 'error',
}
},
}

class HTMLHintCore {
public rules: { [id: string]: Rule } = {}

public addRule(rule: Rule) {
this.rules[rule.id] = rule
}

public verify(
html: string,
config: Configuration = { rules: this.defaultRuleset }
config: Configuration = { extends: [HTMLHINT_RECOMMENDED] }
) {
let ruleset = config.rules ?? this.defaultRuleset
if (!isConfiguration(config)) {
throw new Error('The HTMLHint configuration is invalid')
}

let ruleset: Ruleset = {}

// If an empty configuration is passed, use the recommended ruleset
if (config.extends === undefined && config.rules === undefined) {
config.extends = [HTMLHINT_RECOMMENDED]
}

// Iterate through extensions and merge rulesets into ruleset
for (const extend of config.extends ?? []) {
if (typeof extend === 'string') {
const extendRuleset = DEFAULT_RULESETS[extend] ?? {}
ruleset = { ...ruleset, ...extendRuleset }
}
}

// Apply self-configured rules
ruleset = { ...ruleset, ...(config.rules ?? {}) }

// If no rules have been configured, return immediately
if (Object.keys(ruleset).length === 0) {
ruleset = this.defaultRuleset
// console.log('Please configure some HTMLHint rules')
return []
}

// parse inline ruleset
Expand Down
1 change: 1 addition & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { HTMLParser } from './core'
import { ReportMessageCallback } from './reporter'

export interface Configuration {
extends?: string[]
rules?: Ruleset
}

Expand Down
2 changes: 1 addition & 1 deletion test/cli/formatters/checkstyle.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const path = require('path')

describe('CLI', () => {
describe('Formatter: checkstyle', () => {
it('should have stdout output with formatter checkstyle', (done) => {
it('Should have stdout output with formatter checkstyle', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'checkstyle.xml'), 'utf8')
.replace('{{path}}', path.resolve(__dirname, 'example.html'))
Expand Down
36 changes: 18 additions & 18 deletions test/cli/formatters/checkstyle.xml
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<checkstyle version="4.3">
<file name="{{path}}">
<error line="8" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="9" column="7" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="9" column="14" severity="error" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="7" severity="warning" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="8" column="14" severity="warning" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="9" column="14" severity="error" message="Duplicate of attribute name [ bad ] was found." source="htmlhint.attr-no-duplication"/>
<error line="10" column="22" severity="error" message="Tag must be paired, no start tag: [ &lt;/input&gt; ]" source="htmlhint.tag-pair"/>
<error line="11" column="3" severity="error" message="Special characters must be escaped : [ &lt; ]." source="htmlhint.spec-char-escape"/>
<error line="11" column="18" severity="error" message="Special characters must be escaped : [ &gt; ]." source="htmlhint.spec-char-escape"/>
<error line="13" column="11" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="14" column="9" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="15" column="7" severity="error" message="Tag must be paired, no start tag: [ &lt;/hello&gt; ]" source="htmlhint.tag-pair"/>
<error line="16" column="5" severity="error" message="Tag must be paired, no start tag: [ &lt;/test&gt; ]" source="htmlhint.tag-pair"/>
<error line="17" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="21" column="15" severity="error" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="24" severity="error" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="32" severity="error" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="25" column="3" severity="error" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="26" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="27" column="1" severity="error" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="9" column="7" severity="warning" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="9" column="14" severity="warning" message="The value of attribute [ bad ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="10" column="22" severity="warning" message="Tag must be paired, no start tag: [ &lt;/input&gt; ]" source="htmlhint.tag-pair"/>
<error line="11" column="3" severity="warning" message="Special characters must be escaped : [ &lt; ]." source="htmlhint.spec-char-escape"/>
<error line="11" column="18" severity="warning" message="Special characters must be escaped : [ &gt; ]." source="htmlhint.spec-char-escape"/>
<error line="13" column="11" severity="warning" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="14" column="9" severity="warning" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="15" column="7" severity="warning" message="Tag must be paired, no start tag: [ &lt;/hello&gt; ]" source="htmlhint.tag-pair"/>
<error line="16" column="5" severity="warning" message="Tag must be paired, no start tag: [ &lt;/test&gt; ]" source="htmlhint.tag-pair"/>
<error line="17" column="3" severity="warning" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="21" column="15" severity="warning" message="The value of attribute [ class ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="24" severity="warning" message="The value of attribute [ what ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="21" column="32" severity="warning" message="The value of attribute [ something ] must be in double quotes." source="htmlhint.attr-value-double-quotes"/>
<error line="25" column="3" severity="warning" message="Tag must be paired, no start tag: [ &lt;/div&gt; ]" source="htmlhint.tag-pair"/>
<error line="26" column="1" severity="warning" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
<error line="27" column="1" severity="warning" message="Tag must be paired, no start tag: [ &lt;/bad&gt; ]" source="htmlhint.tag-pair"/>
</file>
</checkstyle>
2 changes: 1 addition & 1 deletion test/cli/formatters/compact.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const path = require('path')

describe('CLI', () => {
describe('Formatter: compact', () => {
it('should have stdout output with formatter compact', (done) => {
it('Should have stdout output with formatter compact', (done) => {
const expected = fs
.readFileSync(path.resolve(__dirname, 'compact.txt'), 'utf8')
.replace(
Expand Down
Loading