Commits (70)
coverage/
dist/
node_modules/
public/assets/
......@@ -13,8 +13,8 @@
"overrides": [
{
"files": "**/*.ts",
"extends": ["@html-validate/eslint-config/typescript"],
"files": "*.ts",
"extends": ["@html-validate/typescript"],
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-use-before-define": "off",
......@@ -23,7 +23,8 @@
},
{
"files": "*.spec.[jt]s",
"extends": ["@html-validate/eslint-config/jest"],
"excludedFiles": "cypress/**",
"extends": ["@html-validate/jest"],
"rules": {
"sonarjs/no-duplicate-string": "off",
"sonarjs/no-identical-functions": "off",
......
# html-validate changelog
## [4.2.0](https://gitlab.com/html-validate/html-validate/compare/v4.1.0...v4.2.0) (2021-01-15)
### Features
- **dom:** disable cache until node is fully constructed ([5e35c49](https://gitlab.com/html-validate/html-validate/commit/5e35c498f790be65928989a327c40772b3fb7184))
- **htmlvalidate:** add `getConfigurationSchema()` to get effective configuration schema ([1dd81d9](https://gitlab.com/html-validate/html-validate/commit/1dd81d993508720b13b8c094867bd780da002b84))
- **htmlvalidate:** add `getElementsSchema()` to get effective elements schema ([4baac36](https://gitlab.com/html-validate/html-validate/commit/4baac36ecb608dd2ef83bbf3c284d08ed05d1087)), closes [#78](https://gitlab.com/html-validate/html-validate/issues/78)
- **rule:** support filter callback for rule events ([f3f949c](https://gitlab.com/html-validate/html-validate/commit/f3f949cd5f2cdef526bc1c60d9176f4ae57890ee))
- **rules:** add `allowMultipleH1` option to `heading-level` ([a33071d](https://gitlab.com/html-validate/html-validate/commit/a33071d12807770a9484c5d713b7037c354d8fe1))
### Bug Fixes
- enable `strictNullCheck` ([64b5af2](https://gitlab.com/html-validate/html-validate/commit/64b5af25723e6441a133a0a561a941d3f8a2daa0)), closes [#76](https://gitlab.com/html-validate/html-validate/issues/76)
- **event:** `location` property can be `null` for some events ([fbbc87c](https://gitlab.com/html-validate/html-validate/commit/fbbc87cf5d62d2a102d86cb8165e9d3dac630474))
- **event:** pass `null` when attribute value is missing ([08c2876](https://gitlab.com/html-validate/html-validate/commit/08c2876dc8f4e01f4c4b0aa97de9672b43476ca3))
- **rules:** rule options uses `Partial<T>` ([221113b](https://gitlab.com/html-validate/html-validate/commit/221113b41adcd9fd8ab5bc10aa9a8d6723b40db6))
### Dependency upgrades
- **deps:** update dependency ajv to v7 ([4c04388](https://gitlab.com/html-validate/html-validate/commit/4c043884a74083274f729ed0d3d40406f9163799))
## [4.1.0](https://gitlab.com/html-validate/html-validate/compare/v4.0.2...v4.1.0) (2020-12-14)
### Features
......
......@@ -11,7 +11,7 @@ method:
```typescript
import { Rule, RuleDocumentation } from "html-validate";
class MyRule extends Rule {
export default class MyRule extends Rule {
documentation(): RuleDocumentation {
return {
description: "Lorem ipsum",
......@@ -30,8 +30,6 @@ class MyRule extends Rule {
});
}
}
module.exports = MyRule;
```
All (enabled) rules run the `setup()` callback before the source document is being parsed and is used to setup any event listeners relevant for this rule.
......@@ -134,9 +132,9 @@ const defaults: RuleOptions = {
};
class MyRule extends Rule<void, RuleOptions> {
constructor(options: RuleOptions) {
constructor(options: Partial<RuleOptions>) {
/* assign default values if not provided by user */
super(Object.assign({}, defaults, options));
super({ ...defaults, ...options });
}
setup(): void {
......@@ -164,10 +162,11 @@ Options are accessed using `this.options`.
When using typescript: pass the datatype as the second template argument when extending `Rule`.
Default is `void` (i.e. no options)
### `on(event: string, callback: (event: Event)): void`
### `on(event: string, [filter: (event: Event) => boolean], callback: (event: Event) => void): void`
Listen for events. See [events](/dev/events.html) for a full list of available events and data.
Listen for events. See [events](/dev/events.html) for a full list of available
events and data.
If `filter` is passed the callback is only called if the filter function evaluates to true.
### `report(node: DOMNode, message: string, location?: Location, context?: RuleContext): void`
......
......@@ -86,7 +86,7 @@ module.exports = function parseValidatesProcessor(
}
function generateConfig(rules, elements, attr) {
attr = Object.assign({}, attr); /* copy before modification */
attr = { ...attr }; /* copy before modification */
delete attr.elements;
delete attr.name;
delete attr.rules;
......
......@@ -71,9 +71,10 @@ plugins](dev/writing-plugins.html).**
First-class support for:
- {@link frameworks/angularjs AngularJS}
- {@link frameworks/vue Vue.js}
- {@link usage/protractor Protractor}
- JS: {@link frameworks/vue Vue.js} and {@link frameworks/angularjs AngularJS}
- Testing: {@link frameworks/jest Jest}
- IDE: {@link usage/vscode VS Code}
- E2E: {@link usage/cypress Cypress} and {@link usage/protractor Protractor}
@block Examples
......
......@@ -52,7 +52,7 @@ Array [
"ruleId": "parser-error",
"selector": null,
"severity": 2,
"size": 0,
"size": 1,
},
],
"source": "<p>Fred <3 Barney</p>",
......
......@@ -25,3 +25,17 @@ Examples of **correct** code for this rule:
<h1>Heading 1</h1>
<h2>Subheading</h2>
</validate>
## Options
This rule takes an optional object:
```json
{
"allowMultipleH1": false
}
```
### AllowMultipleH1
Set `allowMultipleH1` to `true` to allow multiple `<h1>` elements in a document.
This diff is collapsed.
{
"name": "html-validate",
"version": "4.1.0",
"version": "4.2.0",
"description": "html linter",
"keywords": [
"html",
......@@ -42,8 +42,8 @@
"commitlint": "commitlint",
"compatibility": "scripts/compatibility.sh",
"debug": "node --inspect ./node_modules/.bin/jest --runInBand --watch --no-coverage",
"eslint": "eslint --ext js,ts .",
"eslint:fix": "eslint --ext js,ts . --fix",
"eslint": "eslint .",
"eslint:fix": "eslint --fix .",
"htmlvalidate": "./bin/html-validate.js",
"prepare": "scripts/prepare.sh",
"prettier:check": "prettier --check .",
......@@ -92,7 +92,7 @@
"@html-validate/stylish": "1.0.0",
"@sidvind/better-ajv-errors": "^0.6.10",
"acorn-walk": "^8.0.0",
"ajv": "^6.12.6",
"ajv": "^7.0.0",
"chalk": "^4.0.0",
"deepmerge": "^4.2.2",
"espree": "^7.3.0",
......@@ -102,33 +102,35 @@
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.12.9",
"@babel/preset-env": "7.12.7",
"@babel/core": "7.12.10",
"@babel/preset-env": "7.12.11",
"@commitlint/cli": "11.0.0",
"@html-validate/commitlint-config": "1.1.1",
"@html-validate/eslint-config": "2.3.2",
"@html-validate/jest-config": "1.1.0",
"@html-validate/commitlint-config": "1.2.0",
"@html-validate/eslint-config": "3.1.0",
"@html-validate/eslint-config-jest": "3.0.0",
"@html-validate/eslint-config-typescript": "3.0.0",
"@html-validate/jest-config": "1.2.3",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.1.2",
"@html-validate/semantic-release-config": "1.2.4",
"@lodder/grunt-postcss": "3.0.0",
"@types/babel__code-frame": "7.0.2",
"@types/estree": "0.0.45",
"@types/estree": "0.0.46",
"@types/glob": "7.1.3",
"@types/inquirer": "7.3.1",
"@types/jest": "26.0.15",
"@types/jest": "26.0.20",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.38",
"@types/node": "11.15.43",
"@types/prompts": "2.0.9",
"autoprefixer": "10.0.2",
"autoprefixer": "10.2.1",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
"cssnano": "4.1.10",
"dgeni": "0.4.12",
"dgeni": "0.4.13",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint": "7.14.0",
"eslint": "7.17.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"font-awesome": "4.7.0",
......@@ -139,26 +141,26 @@
"grunt-contrib-connect": "3.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.4.0",
"husky": "4.3.0",
"highlight.js": "10.5.0",
"husky": "4.3.7",
"jest": "26.6.3",
"jest-diff": "26.6.2",
"jquery": "3.5.1",
"lint-staged": "10.5.2",
"lint-staged": "10.5.3",
"load-grunt-tasks": "5.1.0",
"marked": "1.2.5",
"marked": "1.2.7",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.1.10",
"prettier": "2.2.0",
"postcss": "8.2.4",
"prettier": "2.2.1",
"pretty-format": "26.6.2",
"sass": "1.29.0",
"semantic-release": "17.3.0",
"sass": "1.32.4",
"semantic-release": "17.3.2",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "26.4.4",
"typescript": "4.1.2"
"typescript": "4.1.3"
},
"engines": {
"node": ">= 10.0"
......
......@@ -7,7 +7,7 @@ interface Options {
cwd?: string;
}
let mockFiles: string[] = null;
let mockFiles: string[] | null = null;
function setMockFiles(files: string[]): void {
mockFiles = files.map((cur) => path.normalize(cur));
......
......@@ -52,10 +52,7 @@ export function expandFiles(patterns: string[], options: ExpandOptions): string[
/* if file is a directory recursively expand files from it */
const fullpath = path.join(cwd, filename);
if (isDirectory(fullpath)) {
const dir = expandFiles(
[directoryPattern(extensions)],
Object.assign({}, options, { cwd: fullpath })
);
const dir = expandFiles([directoryPattern(extensions)], { ...options, cwd: fullpath });
result = result.concat(dir.map((cur) => path.join(filename, cur)));
continue;
}
......
......@@ -55,6 +55,7 @@ const report: Report = {
filePath: "mock-file.html",
errorCount: 1,
warningCount: 0,
source: null,
},
],
errorCount: 1,
......
......@@ -15,7 +15,7 @@ function wrap(formatter: Formatter, dst: string): (results: Result[]) => string
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(dst, output, "utf-8");
return null;
return "";
} else {
return output;
}
......
type RuleSeverity = "off" | "warn" | "error" | number;
export type RuleSeverity = "off" | "warn" | "error" | number;
export type RuleConfig = Record<string, RuleSeverity | [RuleSeverity] | [RuleSeverity, any]>;
export type RuleOptions = string | number | Record<string, any>;
export type RuleConfig = Record<
string,
RuleSeverity | [RuleSeverity] | [RuleSeverity, RuleOptions]
>;
export interface TransformMap {
[key: string]: string;
......
......@@ -41,7 +41,7 @@ class MockConfig {
}
}
function getInteralCache(loader: ConfigLoader): Map<string, Config> {
function getInteralCache(loader: ConfigLoader): Map<string, Config | null> {
return (loader as any).cache;
}
......@@ -63,7 +63,7 @@ describe("ConfigLoader", () => {
return filename === path.resolve("/path/to/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect(config?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -79,7 +79,7 @@ describe("ConfigLoader", () => {
return filename === path.resolve("/path/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect(config?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -93,7 +93,7 @@ describe("ConfigLoader", () => {
expect.assertions(1);
jest.spyOn(fs, "existsSync").mockImplementation(() => true);
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect(config?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -109,7 +109,7 @@ describe("ConfigLoader", () => {
expect.assertions(1);
jest.spyOn(fs, "existsSync").mockImplementation(() => true);
const config = loader.fromTarget("/project/root/src/target.html");
expect(config.get()).toEqual(
expect(config?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -133,7 +133,7 @@ describe("ConfigLoader", () => {
expect(cache.has("/path/to/target.html")).toBeFalsy();
loader.fromTarget("/path/to/target.html");
expect(cache.has("/path/to/target.html")).toBeTruthy();
expect(cache.get("/path/to/target.html").get()).toEqual(
expect(cache.get("/path/to/target.html")?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -155,7 +155,7 @@ describe("ConfigLoader", () => {
throw new Error("expected cache to be used");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect(config?.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
......@@ -208,7 +208,7 @@ describe("ConfigLoader", () => {
* rules are added to recommended config */
function filter(src: Config): ConfigData {
const whitelisted = ["no-self-closing", "deprecated", "element-permitted-content"];
const data = src.get();
const data = { rules: {}, ...src.get() };
data.rules = Object.keys(data.rules)
.filter((key) => whitelisted.includes(key))
.reduce((dst, key) => {
......
......@@ -54,7 +54,7 @@ export class ConfigLoader {
}
if (this.cache.has(filename)) {
return this.cache.get(filename);
return this.cache.get(filename) ?? null;
}
let found = false;
......
......@@ -53,7 +53,7 @@ describe("config", () => {
it("should contain no rules by default", () => {
expect.assertions(1);
const config = Config.empty();
expect(Object.keys(config.get().rules)).toHaveLength(0);
expect(Object.keys(config.get().rules || {})).toHaveLength(0);
});
it("empty() should load empty config", () => {
......
......@@ -9,11 +9,12 @@ import { MetaCopyableProperty, MetaDataTable } from "../meta/element";
import { Plugin } from "../plugin";
import schema from "../schema/config.json";
import { TransformContext, Transformer, TRANSFORMER_API } from "../transform";
import { ConfigData, TransformMap } from "./config-data";
import { ConfigData, RuleOptions, TransformMap } from "./config-data";
import defaultConfig from "./default";
import { ConfigError } from "./error";
import { parseSeverity, Severity } from "./severity";
import Presets from "./presets";
import { ResolvedConfig } from "./resolved-config";
interface TransformerEntry {
pattern: RegExp;
......@@ -29,9 +30,9 @@ interface LoadedPlugin extends Plugin {
originalName: string;
}
let rootDirCache: string = null;
let rootDirCache: string | null = null;
const ajv = new Ajv({ jsonPointers: true });
const ajv = new Ajv({ strict: true, strictTuples: true, strictTypes: true });
ajv.addMetaSchema(require("ajv/lib/refs/json-schema-draft-06.json"));
const validator = ajv.compile(schema);
......@@ -41,7 +42,7 @@ function overwriteMerge<T>(a: T[], b: T[]): T[] {
}
function mergeInternal(base: ConfigData, rhs: ConfigData): ConfigData {
const dst = deepmerge(base, Object.assign({}, rhs, { rules: {} }));
const dst = deepmerge(base, { ...rhs, rules: {} });
/* rules need some special care, should overwrite arrays instead of
* concaternation, i.e. ["error", {...options}] should not be merged by
......@@ -52,8 +53,9 @@ function mergeInternal(base: ConfigData, rhs: ConfigData): ConfigData {
/* root property is merged with boolean "or" since it should always be truthy
* if any config has it set. */
if (base.root || rhs.root) {
dst.root = base.root || rhs.root;
const root = base.root || rhs.root;
if (root) {
dst.root = root;
}
return dst;
......@@ -93,9 +95,9 @@ export class Config {
private configurations: Map<string, ConfigData>;
private initialized: boolean;
protected metaTable: MetaTable;
protected metaTable: MetaTable | null;
protected plugins: LoadedPlugin[];
protected transformers: TransformerEntry[];
protected transformers: TransformerEntry[] = [];
protected rootDir: string;
/**
......@@ -137,7 +139,7 @@ export class Config {
*
* Throws SchemaValidationError if invalid.
*/
public static validate(options: ConfigData, filename?: string): void {
public static validate(options: ConfigData, filename: string | null = null): void {
const valid = validator(options);
if (!valid) {
throw new SchemaValidationError(
......@@ -145,7 +147,7 @@ export class Config {
`Invalid configuration`,
options,
schema,
validator.errors
validator.errors ?? []
);
}
}
......@@ -175,7 +177,7 @@ export class Config {
this.extendMeta(this.plugins);
/* process extended configs */
for (const extend of this.config.extends) {
for (const extend of this.config.extends ?? []) {
this.config = this.extendConfig(extend);
}
......@@ -207,7 +209,7 @@ export class Config {
* Returns true if this configuration is marked as "root".
*/
public isRootFound(): boolean {
return this.config.root;
return Boolean(this.config.root);
}
/**
......@@ -223,7 +225,7 @@ export class Config {
private extendConfig(entry: string): ConfigData {
let base: ConfigData;
if (this.configurations.has(entry)) {
base = this.configurations.get(entry);
base = this.configurations.get(entry) as ConfigData;
} else {
base = Config.fromFile(entry).config;
}
......@@ -303,9 +305,9 @@ export class Config {
* @hidden primary purpose is unittests
*/
public get(): ConfigData {
const config = Object.assign({}, this.config);
const config = { ...this.config };
if (config.elements) {
config.elements = config.elements.map((cur: string | MetaDataTable) => {
config.elements = config.elements.map((cur) => {
if (typeof cur === "string") {
return cur.replace(this.rootDir, "<rootDir>");
} else {
......@@ -319,9 +321,9 @@ export class Config {
/**
* Get all configured rules, their severity and options.
*/
public getRules(): Map<string, [Severity, any]> {
const rules = new Map<string, [Severity, any]>();
for (const [ruleId, data] of Object.entries(this.config.rules)) {
public getRules(): Map<string, [Severity, RuleOptions]> {
const rules = new Map<string, [Severity, RuleOptions]>();
for (const [ruleId, data] of Object.entries(this.config.rules ?? {})) {
let options = data;
if (!Array.isArray(options)) {
options = [options, {}];
......@@ -366,6 +368,8 @@ export class Config {
/* presets from plugins */
for (const plugin of plugins) {
for (const [name, config] of Object.entries(plugin.configs || {})) {
if (!config) continue;
/* add configuration with name provided by plugin */
configs.set(`${plugin.name}:${name}`, config);
......@@ -398,6 +402,14 @@ export class Config {
}
}
public resolve(): ResolvedConfig {
return new ResolvedConfig({
metaTable: this.getMetaTable(),
plugins: this.getPlugins(),
rules: this.getRules(),
});
}
/**
* Transform a source.
*
......@@ -465,7 +477,8 @@ export class Config {
}
private findTransformer(filename: string): TransformerEntry | null {
return this.transformers.find((entry: TransformerEntry) => entry.pattern.test(filename));
const match = this.transformers.find((entry: TransformerEntry) => entry.pattern.test(filename));
return match ?? null;
}
private precompileTransformers(transform: TransformMap): TransformerEntry[] {
......@@ -552,11 +565,12 @@ export class Config {
);
}
if (!plugin.transformer[key]) {
const transformer = plugin.transformer[key];
if (!transformer) {
throw new ConfigError(`Plugin "${pluginName}" does not expose a transformer named "${key}".`);
}
return plugin.transformer[key];
return transformer;
}
/**
......
export { Config } from "./config";
export { ConfigData, RuleConfig } from "./config-data";
export { ConfigData, RuleConfig, RuleOptions } from "./config-data";
export { ConfigLoader } from "./config-loader";
export { ConfigError } from "./error";
export { ResolvedConfig } from "./resolved-config";
export { Severity } from "./severity";