Commits (35)
# html-validate changelog
## [5.2.0](https://gitlab.com/html-validate/html-validate/compare/v5.1.1...v5.2.0) (2021-07-23)
### Features
- support specifying a custom loader when using library ([0e509a3](https://gitlab.com/html-validate/html-validate/commit/0e509a3d7b8931acfc2bb2452ff81ecb0877aaa8))
- **config:** add `StaticConfigData` for simple static config ([ae40706](https://gitlab.com/html-validate/html-validate/commit/ae40706f6ab545b23ae8ceb008122d23264423b9))
### Bug Fixes
- **elements:** disallow whitespace in `id` ([df2906b](https://gitlab.com/html-validate/html-validate/commit/df2906bd2a19fcc7c7a6c020af5b3550cbcc5158))
- **meta:** regex matching attribute allowed values matches entire string ([ffa0d12](https://gitlab.com/html-validate/html-validate/commit/ffa0d122d9941128de4ebd433a1508854ac6b9b8))
- **rules:** handle id with whitespace in `no-redundant-for` ([a79f266](https://gitlab.com/html-validate/html-validate/commit/a79f2669c88a135bab987ea5c64ddd6f47fd3736)), closes [#128](https://gitlab.com/html-validate/html-validate/issues/128)
### [5.1.1](https://gitlab.com/html-validate/html-validate/compare/v5.1.0...v5.1.1) (2021-07-11)
### Bug Fixes
......
......@@ -15,13 +15,34 @@ While primarly developed as a NodeJS CLI/backend tool it is possible to run `htm
Improvements are welcome!
## Example
## Base implementation
There is an example project [try-online-repo] running at [try-online-url] showing that it can be done and the workarounds required.
This article assume you are trying to get something similar to this code to run in the browser.
```ts
import { HtmlValidate } from "html-validate";
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString(markup, "my-file.html");
```
### Example
There is an example project [try-online][try-online-repo] running at [online.html-validate.org][try-online-url] showing that it can be done and the workarounds required.
[try-online-repo]: https://gitlab.com/html-validate/try-online
[try-online-url]: https://online.html-validate.org/
## Browser bundle
The first step is to make sure the correct bundle is used.
The library contains both a full build and a browser build, if your bundler fails to pick up the right one you need to be explicit:
```diff
-import { HtmlValidate } from "html-validate";
+import { HtmlValidate } from "html-validate/es/browser"; // replace es with cjs for commonjs
```
## Configuration loading
By default `html-validate` will traverse the filesystem looking for configuration files (e.g. `.htmlvalidate.json`).
......@@ -33,38 +54,44 @@ This will manifest itself with errors such as:
- `Cannot read property 'existsSync' of undefined`
- `fs_1.default.existsSync is not a function`
### Workaround 1: prevent loader from trying to access filesystem
By far the easiest method is to pass a config to the [[HtmlValidate]] constructor with the `root` property to `true`:
To get around this the [[StaticConfigLoader]] (or a custom loader) can be used:
```ts
import { HtmlValidate } from "html-validate";
```diff
-import { HtmlValidate } from "html-validate"
+import { StaticConfigLoader, HtmlValidate } from "html-validate/es/browser";
const htmlvalidate = new HtmlValidate({
root: true,
extends: ["html-validate:recommended"],
});
-const htmlvalidate = new HtmlValidate();
+const loader = new StaticConfigLoader();
+const htmlvalidate = new HtmlValidate(loader);
const report = htmlvalidate.validateString(markup, "my-file.html");
```
Do note that no default configuration will be loaded either so you must explicitly enable rules or extend a preset.
The [[StaticConfigLoader]] will only load the configuration passed to the constructor or to `validateString(..)`.
By default it uses the `html-validate:recommended` preset but can be overridden by passing a different to the constructor:
### Workaround 2:
```diff
-const loader = new StaticConfigLoader();
+const loader = new StaticConfigLoader({
+ extends: ["html-validate:standard"],
+ elements: ["html5"],
+});
const htmlvalidate = new HtmlValidate(loader);
```
If you are emulating or providing virtual access to a filesystem you can ensure the `fs` module is implemented.
There is no exhaustive list of functions which must be added.
### Previous workaround
If you are using webpack you can use `resolve.alias` to implement this:
The previous workaround was to pass a configuration to the [[HtmlValidate]] constructor with the `root` property set to `true` but this is no longer recommended for this purpose:
```js
module.exports = {
resolve: {
alias: {
fs$: path.resolve(__dirname, "src/my-fs.js"),
},
},
};
```diff
-const htmlvalidate = new HtmlValidate();
+const htmlvalidate = new HtmlValidate({
+ root: true,
+ extends: ["html-validate:recommended"],
+});
```
Note that no default configuration will be loaded either so you must explicitly enable rules or extend a preset.
## Bundled files
The `html-validate` NPM package contains a few data files such as `elements/html.json`.
......@@ -80,17 +107,17 @@ This will manifest itself with errors such as:
This is typically archived by passing an object instead of a string when configuring `html-validate`:
```diff
import { HtmlValidate } from "html-validate";
import { StaticConfigLoader, HtmlValidate } from "html-validate/es/browser";
+// check your loader! it must return a plain object (not `default: { ... }`, a path/url, etc)
+// check your webpack loader! it must return a plain object (not `default: { ... }`, a path/url, etc)
+import html5 from "html-validate/elements/html5.json";
const htmlvalidate = new HtmlValidate({
root: true,
const loader = new StaticConfigLoader({
extends: ["html-validate:recommended"],
- elements: ["html5"],
+ elements: [html5]
});
+ elements: [html5],
});
const htmlvalidate = new HtmlValidate(loader);
```
## Webpack
......
......@@ -5,6 +5,19 @@ title: Using API
# Using API
## Bundles
The `html-validate` package contains four bundles:
- CommonJS full (`dist/cjs/main.js`)
- CommonJS browser (`dist/cjs/browser.js`)
- ESM full (`dist/es/main.js`)
- ESM browser (`dist/es/browser.js`)
The default full bundle includes everything (`CLI` classes etc) while the browser bundles are a bit more stripped and includes only code that runs in a browser<sup>1</sup>.
1. Running in a browser is not fully supported yet as there are still calls to NodeJS `fs` and dynamic `require`'s inside the library, see {@link dev/running-in-browser running in a browser} for details.
## Validating files
```typescript
......@@ -25,15 +38,31 @@ the CLI tool (in fact, the CLI tool uses this very API).
A configuration object may optionally be passed to the `HtmlValidate` constructor:
```typescript
const htmlvalidate = new HtmlValidate({
extends: ["html-validate:recommended"],
});
```diff
-const htmlvalidate = new HtmlValidate();
+const htmlvalidate = new HtmlValidate({
+ extends: ["html-validate:recommended"],
+});
```
If set, it will be used as configuration unless a configuration could be read from `.htmlvalidate.json` files.
Set `root: true` to prevent configuration files to be searched.
It is also possible to pass a [configuration loader](#configuration-loaders) to fully customize how the configuration loading is handled:
```diff
-import { HtmlValidate } from "html-validate";
+import { StaticConfigLoader, HtmlValidate } from "html-validate";
-const htmlvalidate = new HtmlValidate();
+const loader = new StaticConfigLoader();
+const htmlvalidate = new HtmlValidate(loader);
```
### `validateFile(filename: string)`
Reads a file and transforms the file according to the configured transformers.
## Validating strings and other sources
In addition to `validateFile` there is also `validateString` and
......@@ -62,6 +91,20 @@ const report = htmlvalidate.validateSource({
console.log(report.results);
```
### `validateString(markup: string, [filename: string], [config: ConfigData], [hooks: SourceHooks])`
Validates the given markup.
- `filename` - If a filename is passed it is used to load configuration and is used as the `filePath` property when generating the report.
- `config` - If configuration is passed it is merged with global config and config loaded from the filename.
- `hooks` - Normally reserved for transforms hooks can be used to alter DOM tree during parsing.
### `validateSource(source: Source, [config: ConfigData])`
Validates the given markup (passed in the `Source` object).
- `config` - If configuration is passed it is merged with global config and config loaded from the filename.
## Handling multiple files
To validate multiple files you need to call `validateFile` for each one,
......@@ -122,6 +165,96 @@ const stylish = require("eslint/lib/formatters/stylish");
console.log(stylish(report.results));
```
## Configuration loaders
By default `HtmlValidate` traverses the file system looking for configuration files such as `.htmlvalidate.json`.
If this behaviour is not desired a custom loader can be used instead:
```ts
import { StaticConfigLoader, HtmlValidate } from "html-validate";
const loader = new StaticConfigLoader();
const htmlvalidate = new HtmlValidate(loader);
```
A fully custom loader can be impemented by inheriting from `ConfigLoader`:
```ts
class MyCustomLoader extends ConfigLoader {
public override getConfigFor(handle: string, configOverride?: ConfigData): Config {
/* return config for given handle (e.g. filename passed to validateFile) */
const override = this.loadFromObject(configOverride || {});
const merged = this.globalConfig.merge(override);
merged.init();
return merged;
}
public override flushCache(handle?: string): void {
/* do nothing for this example */
}
protected defaultConfig(): Config {
/* return default configuration, used when no config is passed to constructor */
return this.loadFromObject({
extends: ["html-validate:recommended"],
elements: ["html5"],
});
}
}
```
The custom loader is used the same as builtin loaders:
```diff
-const loader = new StaticConfigLoader();
+const loader = new MyCustomLoader();
const htmlvalidate = new HtmlValidate(loader);
```
When markup is validated the library will call the loader to fetch configuration, e.g:
```ts
htmlvalidate.validateFile("foo.html");
htmlvalidate.validateString("..", "my-fancy-handle");
```
This will generate calls to `getConfigFor("foo.html")` and `getConfigFor("my-fancy-handle")` respectively.
While `validateFile` requires the file to be readable, the second argument to `validateString` can be any handle the API user wants as long as the loader can understand it.
### `FileSystemConfigLoader([config: ConfigData])` (default)
Default loader which traverses filesystem looking for `.htmlvalidate.json` configuration files, starting at the directory of the target filename.
The result from the configuration files are merged both with a global configuration and optionally explicit overrides from the calls to `validateFile`, `validateString` and `validateSource`.
### `StaticConfigLoader([config: ConfigData])`
Loads configuration only from the configuration passed to the constructor or explicit overrides to `validateString(..)`.
```ts
const loader = StaticConfigLoader({
/* global configuration */
});
```
The global configuration is used by default when using `validateFile`, `validateString` and `validateSource` without any arguments:
```ts
htmlvalidate.validateFile("myfile.html");
htmlvalidate.validateString("..");
htmlvalidate.validateSource({
data: "..",
});
```
Each call may also pass a configuration override (merged with global):
```ts
htmlvalidate.validateString("..", {
/* config override */
});
```
## Configuration cache
`HtmlValidate` is mostly stateless, it only acts on the input source and its
......
......@@ -114,7 +114,7 @@ Array [
"column": 22,
"context": Object {
"allowed": Array [
/\\\\d\\+/,
/\\^\\\\d\\+\\$/,
],
"attribute": "ducks",
"element": "my-component",
......
......@@ -12,7 +12,7 @@ Array [
"column": 4,
"context": Object {
"allowed": Array [
/\\.\\*/,
/\\^\\.\\*\\$/,
],
"attribute": "href",
"element": "a",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/usage/index.md inline validation: disable-block-button-type 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 17,
"context": Object {
"allowed": Array [
"submit",
"reset",
"button",
],
"attribute": "type",
"element": "button",
"value": "foo",
},
"line": 2,
"message": "Attribute \\"type\\" has invalid value \\"foo\\"",
"offset": 22,
"ruleId": "attribute-allowed-values",
"ruleUrl": "https://html-validate.org/rules/attribute-allowed-values.html",
"selector": "div > button:nth-child(1)",
"severity": 2,
"size": 3,
},
Object {
"column": 15,
"context": Object {
"allowed": Array [
"submit",
"reset",
"button",
],
"attribute": "type",
"element": "button",
"value": "spam",
},
"line": 7,
"message": "Attribute \\"type\\" has invalid value \\"spam\\"",
"offset": 281,
"ruleId": "attribute-allowed-values",
"ruleUrl": "https://html-validate.org/rules/attribute-allowed-values.html",
"selector": "button",
"severity": 2,
"size": 4,
},
],
"source": "<div>
<button type=\\"foo\\">Invalid button</button>
<!-- [html-validate-disable-block attribute-allowed-values: will be disabled until the parent div is closed] -->
<button type=\\"bar\\">Invalid but ignored</button>
<button type=\\"baz\\">Still ignored</button>
</div>
<button type=\\"spam\\">Another invalid</button>",
"warningCount": 0,
},
]
`;
exports[`docs/usage/index.md inline validation: disable-next-deprecated 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 2,
"context": Object {
"documentation": "\`<blink>\` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard",
"tagName": "blink",
},
"line": 3,
"message": "<blink> is deprecated",
"offset": 143,
"ruleId": "deprecated",
"ruleUrl": "https://html-validate.org/rules/deprecated.html",
"selector": "blink:nth-child(2)",
"severity": 2,
"size": 5,
},
],
"source": "<!-- [html-validate-disable-next deprecated: the next occurrence will not trigger an error] -->
<blink>This will not trigger an error</blink>
<blink>But this line will</blink>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["disable-block-button-type"] = `<div>
<button type="foo">Invalid button</button>
<!-- [html-validate-disable-block attribute-allowed-values: will be disabled until the parent div is closed] -->
<button type="bar">Invalid but ignored</button>
<button type="baz">Still ignored</button>
</div>
<button type="spam">Another invalid</button>`;
markup["disable-next-deprecated"] = `<!-- [html-validate-disable-next deprecated: the next occurrence will not trigger an error] -->
<blink>This will not trigger an error</blink>
<blink>But this line will</blink>`;
describe("docs/usage/index.md", () => {
it("inline validation: disable-block-button-type", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["disable-block-button-type"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: disable-next-deprecated", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["disable-next-deprecated"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -25,7 +25,7 @@ Create `.htmlvalidate.json`:
Run with:
node_modules/.bin/html-validate yourfile.html
npm exec html-validate yourfile.html
## Configuration
......@@ -211,21 +211,20 @@ Disable a rule for the rest of the file or until re-enabled using `enable` direc
### `disable-block`
<!-- [html-validate-disable-block void] -->
<!-- [html-validate-disable-block attribute-allowed-values] -->
Disables a rule for a block of elements. All siblings and descendants following
the directive will not trigger any errors.
Disables a rule for a block of elements.
All siblings and descendants following the directive will not trigger any errors.
```html
<i />error
<div>
<!-- [html-validate-disable-block void: will be disabled until the parent div is closed] -->
<i />no error
<p><i />no error</p>
<i />no error
</div>
<i />error
```
<validate name="disable-block-button-type">
<div>
<button type="foo">Invalid button</button>
<!-- [html-validate-disable-block attribute-allowed-values: will be disabled until the parent div is closed] -->
<button type="bar">Invalid but ignored</button>
<button type="baz">Still ignored</button>
</div>
<button type="spam">Another invalid</button>
</validate>
### `disable-next`
......@@ -233,13 +232,11 @@ the directive will not trigger any errors.
Disables the rule for the next element.
<!-- prettier-ignore-start -->
```html
<!-- [html-validate-disable-next deprecated: the next occurrence will not trigger an error] -->
<blink>This will not trigger an error</blink>
<blink>But this line will</blink>
```
<!-- prettier-ignore-end -->
<validate name="disable-next-deprecated">
<!-- [html-validate-disable-next deprecated: the next occurrence will not trigger an error] -->
<blink>This will not trigger an error</blink>
<blink>But this line will</blink>
</validate>
## Ignoring files
......
This diff is collapsed.
......@@ -6,6 +6,7 @@
"contenteditable": ["", "true", "false"],
"dir": ["ltr", "rtl", "auto"],
"draggable": ["true", "false"],
"id": ["/\\S+/"],
"hidden": [],
"tabindex": ["/-?\\d+/"]
},
......
This diff is collapsed.
{
"name": "html-validate",
"version": "5.1.1",
"version": "5.2.0",
"description": "Offline html5 validator",
"keywords": [
"html",
......@@ -147,11 +147,11 @@
"semver": "^7.0.0"
},
"devDependencies": {
"@babel/core": "7.14.6",
"@babel/preset-env": "7.14.7",
"@babel/core": "7.14.8",
"@babel/preset-env": "7.14.8",
"@commitlint/cli": "12.1.4",
"@html-validate/commitlint-config": "2.0.0",
"@html-validate/eslint-config": "4.4.3",
"@html-validate/eslint-config": "4.4.4",
"@html-validate/eslint-config-jest": "4.4.2",
"@html-validate/eslint-config-typescript": "4.4.0",
"@html-validate/jest-config": "2.2.0",
......@@ -159,8 +159,8 @@
"@html-validate/semantic-release-config": "2.0.1",
"@lodder/grunt-postcss": "3.0.1",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-replace": "2.4.2",
"@rollup/plugin-typescript": "8.2.1",
"@rollup/plugin-replace": "3.0.0",
"@rollup/plugin-typescript": "8.2.3",
"@rollup/plugin-virtual": "2.0.3",
"@types/babar": "0.2.1",
"@types/babel__code-frame": "7.0.3",
......@@ -173,16 +173,16 @@
"@types/node": "11.15.54",
"@types/prompts": "2.0.14",
"@types/semver": "7.3.7",
"autoprefixer": "10.2.6",
"autoprefixer": "10.3.1",
"babar": "0.2.0",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
"cssnano": "5.0.6",
"cssnano": "5.0.7",
"dgeni": "0.4.14",
"dgeni-front-matter": "3.0.0",
"dgeni-packages": "0.29.1",
"eslint": "7.30.0",
"eslint": "7.31.0",
"font-awesome": "4.7.0",
"front-matter": "4.0.2",
"grunt": "1.4.1",
......@@ -196,21 +196,21 @@
"jest": "27.0.6",
"jest-diff": "27.0.6",
"jquery": "3.6.0",
"lint-staged": "11.0.0",
"lint-staged": "11.1.0",
"load-grunt-tasks": "5.1.0",
"marked": "2.1.3",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.4.0",
"postcss": "8.3.5",
"postcss": "8.3.6",
"prettier": "2.3.2",
"rollup": "2.53.0",
"rollup": "2.53.3",
"rollup-plugin-copy": "3.4.0",
"rollup-plugin-dts": "3.0.2",
"sass": "1.35.2",
"semantic-release": "17.4.4",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"ts-jest": "27.0.3",
"ts-jest": "27.0.4",
"typescript": "4.3.5"
},
"peerDependencies": {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable-block.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable-multiple.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/directive/disable-next.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/elements/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/js-config/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/off/error/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/off/file.html 1`] = `undefined`;
exports[`HtmlValidate configuration smoketest test-files/config/warn/file.html 1`] = `undefined`;
......@@ -126,3 +126,5 @@ Array [
},
]
`;
exports[`regression tests test-files/issues/issue128-id-whitespace.html 1`] = `Array []`;
......@@ -3,6 +3,7 @@
export { default as HtmlValidate } from "./htmlvalidate";
export { AttributeData } from "./parser";
export { Config, ConfigData, ConfigError, ConfigLoader, Severity, configPresets } from "./config";
export { StaticConfigLoader } from "./config/loaders/static";
export { DynamicValue, HtmlElement, NodeClosed, TextNode } from "./dom";
export { EventDump, TokenDump } from "./engine";
export { UserError, SchemaValidationError } from "./error";
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ConfigLoader smoketest test-files/config/directive/disable.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable.html 2`] = `
Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 8,
"message": "<i> must not be self-closed",
"offset": 212,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(4)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable no-self-closing] -->
<i/>After disable, should not trigger
<i/>After disable, should not trigger
<!-- [html-validate-enable no-self-closing] -->
<i/>Before after, should trigger
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-block.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-block.html 2`] = `
Array [
Object {
"errorCount": 4,
"filePath": "test-files/config/directive/disable-block.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 4,
"context": "i",
"line": 3,
"message": "<i> must not be self-closed",
"offset": 43,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "div > i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 11,
"message": "<i> must not be self-closed",
"offset": 335,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 16,
"message": "<i> must not be self-closed",
"offset": 439,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(5)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Outside block, should trigger
<div>
<i/>Inside block, before directive, should trigger
<!-- [html-validate-disable-block no-self-closing] -->
<i/>Inside block, after directive, shout not trigger
<div>
<i/>Inside block, after directive, shout not trigger
</div>
<i/>Inside block, after directive, shout not trigger
</div>
<i/>Outside block, should trigger
<div>
<!-- [html-validate-disable-block no-self-closing] -->
</div>
<i/>Outside block, should trigger
<!-- [html-validate-disable-block no-self-closing] -->
Should handle when root element is parent but no children
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-multiple.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-multiple.html 2`] = `
Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable-multiple.html",
"messages": Array [
Object {
"column": 2,
"context": Object {
"documentation": "\`<blink>\` has no direct replacement and blinking text is frowned upon by accessibility standards.",
"source": "non-standard",
"tagName": "blink",
},
"line": 1,
"message": "<blink> is deprecated",
"offset": 1,
"ruleId": "deprecated",
"ruleUrl": "https://html-validate.org/rules/deprecated.html",
"selector": "blink",
"severity": 2,
"size": 5,
},
Object {
"column": 7,
"context": "blink",
"line": 1,
"message": "<blink> must not be self-closed",
"offset": 6,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "blink",
"severity": 2,
"size": 2,
},
],
"source": "<blink/>
<div>
<!-- [html-validate-disable-block no-self-closing, deprecated] -->
<blink/>
</div>
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-next.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "off",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/directive/disable-next.html 2`] = `
Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/directive/disable-next.html",
"messages": Array [
Object {
"column": 3,
"context": "i",
"line": 1,
"message": "<i> must not be self-closed",
"offset": 2,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i",
"severity": 2,
"size": 2,
},
Object {
"column": 3,
"context": "i",
"line": 5,
"message": "<i> must not be self-closed",
"offset": 136,
"ruleId": "no-self-closing",
"ruleUrl": "https://html-validate.org/rules/no-self-closing.html",
"selector": "i:nth-child(3)",
"severity": 2,
"size": 2,
},
],
"source": "<i/>Before disable, should trigger
<!-- [html-validate-disable-next no-self-closing] -->
<i/>First after disable, should not trigger
<i/>Second after disable, should trigger
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/elements/file.html 1`] = `
Object {
"elements": Array [
"html5",
"<rootDir>/test-files/config/elements/elements.json",
],
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/elements/file.html 2`] = `
Array [
Object {
"errorCount": 2,
"filePath": "test-files/config/elements/file.html",
"messages": Array [
Object {
"column": 3,
"context": Object {
"tagName": "deprecated",
},
"line": 2,
"message": "<deprecated> is deprecated",
"offset": 8,
"ruleId": "deprecated",
"ruleUrl": "https://html-validate.org/rules/deprecated.html",
"selector": "div > deprecated",
"severity": 2,
"size": 10,
},
Object {
"column": 4,
"context": undefined,
"line": 5,
"message": "Element <li> is not permitted as content in <ul>",
"offset": 67,
"ruleId": "element-permitted-content",
"ruleUrl": "https://html-validate.org/rules/element-permitted-content.html",
"selector": "div > ul > li",
"severity": 2,
"size": 2,
},
],
"source": "<div>
<deprecated>new (deprecated) element</deprecated>
<ul>
<li>overridden so it is disallowed</li>
<p>overridden so it is allowed</p>
</ul>
</div>
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/file.html 2`] = `
Array [
Object {
"errorCount": 1,
"filePath": "test-files/config/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 49,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 2,
"size": 3,
},
],
"source": "<!-- default configuration yields error -->
<br></br>
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/js-config/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "off",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/js-config/file.html 2`] = `Array []`;
exports[`ConfigLoader smoketest test-files/config/off/error/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "error",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/off/error/file.html 2`] = `
Array [
Object {
"errorCount": 1,
"filePath": "test-files/config/off/error/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 42,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 2,
"size": 3,
},
],
"source": "<!-- reconfigured to yield error -->
<br></br>
",
"warningCount": 0,
},
]
`;
exports[`ConfigLoader smoketest test-files/config/off/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "off",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/off/file.html 2`] = `Array []`;
exports[`ConfigLoader smoketest test-files/config/warn/file.html 1`] = `
Object {
"extends": Array [
"html-validate:recommended",
],
"plugins": Array [],
"root": true,
"rules": Object {
"deprecated": "error",
"element-permitted-content": "error",
"no-self-closing": "error",
"void-content": "warn",
},
"transform": Object {},
}
`;
exports[`ConfigLoader smoketest test-files/config/warn/file.html 2`] = `
Array [
Object {
"errorCount": 0,
"filePath": "test-files/config/warn/file.html",
"messages": Array [
Object {
"column": 6,
"context": "br",
"line": 2,
"message": "End tag for <br> must be omitted",
"offset": 44,
"ruleId": "void-content",
"ruleUrl": "https://html-validate.org/rules/void-content.html",
"selector": null,
"severity": 1,
"size": 3,
},
],
"source": "<!-- configured to yield a warning -->
<br></br>
",
"warningCount": 1,
},
]
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`config transformSource() should throw error when trying to load garbage as transformer 1`] = `"Failed to load transformer \\"mock-garbage\\": Module is not a valid transformer."`;
exports[`config transformers should throw error when trying to load garbage as transformer 1`] = `"Failed to load transformer \\"mock-garbage\\": Module is not a valid transformer."`;
exports[`config transformSource() should throw error when trying to load named transform from plugin without any 1`] = `"Failed to load transformer \\"mock-plugin-notransform:named\\": Plugin does not expose any transformer"`;
exports[`config transformers should throw error when trying to load named transform from plugin without any 1`] = `"Failed to load transformer \\"mock-plugin-notransform:named\\": Plugin does not expose any transformer"`;
exports[`config transformSource() should throw error when trying to load unnamed transform from plugin without any 1`] = `"Failed to load transformer \\"mock-plugin-notransform\\": Plugin does not expose any transformer"`;
exports[`config transformers should throw error when trying to load unnamed transform from plugin without any 1`] = `"Failed to load transformer \\"mock-plugin-notransform\\": Plugin does not expose any transformer"`;
exports[`config transformSource() should throw helpful error when trying to load unregistered plugin as transformer 1`] = `"Failed to load transformer \\"mock-plugin-unregistered\\": Module is not a valid transformer. This looks like a plugin, did you forget to load the plugin first?"`;
exports[`config transformers should throw helpful error when trying to load unregistered plugin as transformer 1`] = `"Failed to load transformer \\"mock-plugin-unregistered\\": Module is not a valid transformer. This looks like a plugin, did you forget to load the plugin first?"`;
exports[`config transformSource() should throw sane error when transformer fails 1`] = `"When transforming \\"/path/to/test.foo\\": Failed to frobnicate a baz"`;
exports[`config transformSource() should throw sane error when transformer fails to load 1`] = `"Failed to load transformer \\"missing-transformer\\""`;
exports[`config transformers should throw sane error when transformer fails to load 1`] = `"Failed to load transformer \\"missing-transformer\\""`;
import fs from "fs";
import path from "path";
import { Config } from "./config";
import { ConfigData } from "./config-data";
/**
* @internal
*/
interface ConfigClass {
export interface ConfigFactory {
defaultConfig(): Config;
empty(): Config;
fromObject(options: ConfigData, filename?: string | null): Config;
fromFile(filename: string): Config;
}
/**
* Configuration loader.
* Configuration loader interface.
*
* Handles configuration lookup and cache results. When performing lookups
* parent directories is searched as well and the result is merged together.
* A configuration loader takes a handle (typically a filename) and returns a
* configuration for it.
*
* @public
*/
export class ConfigLoader {
protected cache: Map<string, Config | null>;
protected configClass: ConfigClass;
export abstract class ConfigLoader {
protected readonly configFactory: ConfigFactory;
protected readonly globalConfig: Config;
/**
* @param configClass - Override class to construct.
*/
public constructor(configClass: ConfigClass) {
this.cache = new Map<string, Config | null>();
this.configClass = configClass;
public constructor(config?: ConfigData, configFactory: ConfigFactory = Config) {
const defaults = configFactory.empty();
this.configFactory = configFactory;
this.globalConfig = defaults.merge(config ? this.loadFromObject(config) : this.defaultConfig());
}
/**
* Flush configuration cache.
* Get configuration for given handle.
*
* @param filename - If given only the cache for that file is flushed.
* Handle is typically a filename but it is up to the loader to interpret the
* handle as something useful.
*
* If [[configOverride]] is set it is merged with the final result.
*
* @param handle - Unique handle to get configuration for.
* @param configOverride - Optional configuration to merge final results with.
*/
public flush(filename?: string): void {
if (filename) {
this.cache.delete(filename);
} else {
this.cache.clear();
}
}
public abstract getConfigFor(handle: string, configOverride?: ConfigData): Config;
/**
* Get configuration for file.
* Flush configuration cache.
*
* Searches parent directories for configuration and merges the result.
* Flushes all cached entries unless a specific handle is given.
*
* @param filename - Filename to get configuration for.
* @param handle - If given only the cache for given handle will be flushed.
*/
// eslint-disable-next-line complexity, sonarjs/cognitive-complexity
public fromTarget(filename: string): Config | null {
if (filename === "inline") {
return null;
}
public abstract flushCache(handle?: string): void;
if (this.cache.has(filename)) {
return this.cache.get(filename) ?? null;
}
let found = false;
let current = path.resolve(path.dirname(filename));
let config = this.configClass.empty();
// eslint-disable-next-line no-constant-condition
while (true) {
for (const potentialExtension of ["json", "cjs", "js"]) {
const filePath = path.join(current, `.htmlvalidate.${potentialExtension}`);
if (fs.existsSync(filePath)) {
const local = this.configClass.fromFile(filePath);
found = true;
config = local.merge(config);
}
}
/* stop if a configuration with "root" is set to true */
if (config.isRootFound()) {
break;
}
/* get the parent directory */
const child = current;
current = path.dirname(current);
/**
* Default configuration used when no explicit configuration is passed to constructor.
*/
protected abstract defaultConfig(): Config;
/* stop if this is the root directory */
if (current === child) {
break;
}
}
protected empty(): Config {
return this.configFactory.empty();
}
/* no config was found by loader, return null and let caller decide what to do */
if (!found) {
this.cache.set(filename, null);
return null;
}
protected loadFromObject(options: ConfigData, filename?: string | null): Config {
return this.configFactory.fromObject(options, filename);
}
this.cache.set(filename, config);
return config;
protected loadFromFile(filename: string): Config {
return this.configFactory.fromFile(filename);
}
}
......@@ -3,7 +3,8 @@ import path from "path";
import { Source } from "../context";
import { SchemaValidationError } from "../error";
import { UserError } from "../error/user-error";
import { Transformer, TRANSFORMER_API } from "../transform";
import { TRANSFORMER_API } from "../transform";
import { Plugin } from "../plugin";
import { Config } from "./config";
import { ConfigError } from "./error";
import { Severity } from "./severity";
......@@ -388,160 +389,133 @@ describe("config", () => {
});
});
describe("transformSource()", () => {
let source: Source;
beforeEach(() => {
source = {
filename: "/path/to/test.foo",
data: "original data",
line: 2,
column: 3,
offset: 4,
};
describe("transformers", () => {
it("should load transformer from package", () => {
expect.assertions(1);
const config = Config.fromObject({
transform: {
"\\.foo$": "mock-transform",
},
});
config.init();
expect(config.resolveData().transformers).toEqual([
{ pattern: /\.foo$/, name: "mock-transform", fn: require("mock-transform") },
]);
});
it("should match filename against transformer", () => {
it("should load transformer from path with <rootDir>", () => {
expect.assertions(1);
const config = Config.fromObject({
transform: {
"^.*\\.foo$": "mock-transform",
"\\.foo$": "<rootDir>/src/transform/__mocks__/mock-transform",
},
});
config.init();
expect(config.transformSource(source)).toMatchInlineSnapshot(`
Array [
Object {
"column": 1,
"data": "transformed source (was: original data)",
"filename": "/path/to/test.foo",
"line": 1,
"offset": 0,<