...
 
Commits (46)
# html-validate changelog
# [3.4.0](https://gitlab.com/html-validate/html-validate/compare/v3.3.0...v3.4.0) (2020-10-08)
### Bug Fixes
- **deps:** update dependency acorn-walk to v8 ([5a41662](https://gitlab.com/html-validate/html-validate/commit/5a41662b6800a8400d493364d35db385300801a9))
- **rules:** fix issue in `no-dup-id` where value is dynamic ([203debe](https://gitlab.com/html-validate/html-validate/commit/203debef4c942f2ef8ab98848453a7fc3c534066)), closes [#96](https://gitlab.com/html-validate/html-validate/issues/96)
### Features
- **api:** add additional prototypes to `validateString` ([69e8102](https://gitlab.com/html-validate/html-validate/commit/69e81024ed6a077e92d32f79791e6b47e0ad0364))
- **dom:** new api for caching data on `DOMNode` ([13d99e4](https://gitlab.com/html-validate/html-validate/commit/13d99e4973a84109c9069fbe1718a33a302325d1))
- **rules:** implement caching in some helper methods ([5746d6c](https://gitlab.com/html-validate/html-validate/commit/5746d6cf37c6ca82bb5d3543f67b33341db0fdc5))
# [3.3.0](https://gitlab.com/html-validate/html-validate/compare/v3.2.0...v3.3.0) (2020-09-08)
### Bug Fixes
......
---
docType: content
title: DOM cache API
---
# DOM cache API
The DOM cache API can be used to save computations on DOM nodes.
Values are cached per node and must not be used before DOM is fully constructed.
## Cache key
Each cache must use a unique key.
The cache is stored per `DOMNode` so the key must only be unique per type of computation not per instance.
The cache key is typically `Symbol` named after the function performing the calculation but any unique string can be used.
```ts
const CACHE_KEY = Symbol(myFunction.name);
```
## Type safety (typescript)
Type safety can be archived by augmenting the `DOMNodeCache` interface:
```ts
declare module "html-validate" {
export interface DOMNodeCache {
[CACHE_KEY]?: number;
}
}
```
This ensures the typescript type system will correctly infer the proper datatypes when storing and fetching values.
Type declaration is optional but strongly recommended.
Without type declaration the value is implied to be `any` and the user must manually ensure it is of the correct type.
## Example
```ts
const CACHE_KEY = Symbol(myFunction.name);
declare module "html-validate" {
export interface DOMNodeCache {
[CACHE_KEY]?: number;
}
}
export function myFunction(node: HtmlElement): number {
if (node.cacheExists(CACHE_KEY)) {
return node.cacheGet(CACHE_KEY);
}
const value = expensiveComputation(node);
return node.cacheSet(CACHE_KEY, value);
}
```
## API
### `DOMNode.cacheGet<K>(key: K): DOMNodeCache[K] | undefined`
Fetch cached value from a `DOMNode`.
Returns `undefined` if no cached value is present.
### `DOMNode.cacheSet<K>(key: K, value: DOMNodeCache[K]): DOMNodeCache[K]`
Store value on `DOMNode`.
### `DOMNode.cacheRemove<K>(key: K): boolean`
Remove value from `DOMNode`.
Returns `true` if a value was present and `false` if there was none.
### `DOMNode.cacheExists<K>(key: K): boolean`
Check if a value is cached in `DOMNode`.
......@@ -120,9 +120,7 @@ Hooks should only be added in the last transformer, if chaining is used the hook
Transformers can add hooks for additional processing by setting `source.hooks`:
```typescript
function processAttribute(
attr: AttributeData
): IterableIterator<AttributeData> {
function processAttribute(attr: AttributeData): IterableIterator<AttributeData> {
/* handle attribute */
}
......
......@@ -168,10 +168,7 @@ test("should not frobnicate a flux", () => {
const htmlvalidate = new HtmlValidate(config);
const report = htmlvalidate.validateString("...");
expect(report).toBeInvalid();
expect(report).toHaveError(
"my-rule",
"the tux should not be frobnicated by a flux"
);
expect(report).toHaveError("my-rule", "the tux should not be frobnicated by a flux");
});
```
......
......@@ -150,6 +150,10 @@ class MyRule extends Rule<void, RuleOptions> {
}
```
## Cache
Expensive operations on `DOMNode` can be cached using the {@link dev/cache cache API}.
## API
### `options: RuleOptions`
......
......@@ -10,9 +10,7 @@ module.exports = {
const hash = crypto.createHash("md5").update(data).digest("hex");
return `${asset}?${hash}`;
} else {
console.log(
`${filename} does not exist when trying to calculate asset hash.`
);
console.log(`${filename} does not exist when trying to calculate asset hash.`);
return asset;
}
},
......
......@@ -10,12 +10,7 @@ module.exports = function renderMarkdown(trimIndentation) {
// markdown code render function
renderer.code = function (code, string, language) {
const trimmedCode = trimIndentation(code);
let renderedCode = marked.Renderer.prototype.code.call(
this,
trimmedCode,
string,
language
);
let renderedCode = marked.Renderer.prototype.code.call(this, trimmedCode, string, language);
// Bug in marked - forgets to add a final newline sometimes
if (!/\n$/.test(renderedCode)) {
......@@ -23,16 +18,13 @@ module.exports = function renderMarkdown(trimIndentation) {
}
// Add hljs class
renderedCode = renderedCode.replace(
/<code(?: class="(.*?)")?>/,
(_, classes) => {
if (classes) {
return `<code class="hljs ${classes}">`;
} else {
return `<code class="hljs">`;
}
renderedCode = renderedCode.replace(/<code(?: class="(.*?)")?>/, (_, classes) => {
if (classes) {
return `<code class="hljs ${classes}">`;
} else {
return `<code class="hljs">`;
}
);
});
return renderedCode;
};
......
......@@ -68,17 +68,13 @@ module.exports = new Package("html-validate-docs", [
/* add custom nunjuck filters */
.config(function (templateEngine) {
templateEngine.filters = templateEngine.filters.concat(
require("./filters")
);
templateEngine.filters = templateEngine.filters.concat(require("./filters"));
})
/* add the local template folder first in the search path so it overrides
* dgeni-packages bundled templates */
.config(function (templateFinder) {
templateFinder.templateFolders.unshift(
path.resolve(packagePath, "templates")
);
templateFinder.templateFolders.unshift(path.resolve(packagePath, "templates"));
})
.config(function (computePathsProcessor, computeIdsProcessor) {
......@@ -133,11 +129,7 @@ module.exports = new Package("html-validate-docs", [
docTypes: ["changelog"],
getPath: function (doc) {
const dirname = path.dirname(doc.fileInfo.relativePath);
return path.join(
dirname,
doc.fileInfo.baseName.toLowerCase(),
"index.html"
);
return path.join(dirname, doc.fileInfo.baseName.toLowerCase(), "index.html");
},
outputPathTemplate: "${path.toLowerCase()}",
});
......@@ -157,9 +149,7 @@ module.exports = new Package("html-validate-docs", [
checkAnchorLinksProcessor.ignoredLinks.push(/^\/changelog$/);
checkAnchorLinksProcessor.checkDoc = (doc) => {
return (
doc.path &&
doc.outputPath &&
[".html", ".json"].includes(path.extname(doc.outputPath))
doc.path && doc.outputPath && [".html", ".json"].includes(path.extname(doc.outputPath))
);
};
});
......@@ -32,8 +32,7 @@ module.exports = new Package("inline-validate", [])
computePathsProcessor.pathTemplates.push({
docTypes: ["validate-spec"],
getPath: function () {},
outputPathTemplate:
"../${fileInfo.path}/__tests__/${fileInfo.file}.spec.ts",
outputPathTemplate: "../${fileInfo.path}/__tests__/${fileInfo.file}.spec.ts",
});
computePathsProcessor.pathTemplates.push({
docTypes: ["inlineValidation"],
......@@ -41,12 +40,7 @@ module.exports = new Package("inline-validate", [])
getOutputPath: function () {},
});
computeIdsProcessor.idTemplates.push({
docTypes: [
"validate-config",
"validate-markup",
"validate-spec",
"inlineValidation",
],
docTypes: ["validate-config", "validate-markup", "validate-spec", "inlineValidation"],
getAliases: function (doc) {
return [doc.id];
},
......
module.exports = function inlineValidationInlineTagDef(
validateMap,
createDocMessage
) {
module.exports = function inlineValidationInlineTagDef(validateMap, createDocMessage) {
return {
name: "inlineValidation",
handler,
......@@ -11,10 +8,7 @@ module.exports = function inlineValidationInlineTagDef(
const validation = validateMap.get(description);
if (!validation) {
throw new Error(
createDocMessage(
`No inline validation exists with id "${description}".`,
doc
)
createDocMessage(`No inline validation exists with id "${description}".`, doc)
);
}
if (!validation.inlineValidationDoc) {
......
......@@ -34,9 +34,7 @@ module.exports = function generateValidationsSpecProcessor(log, validateMap) {
path: path.dirname(fileInfo.projectRelativePath),
file: path.basename(fileInfo.projectRelativePath),
fullpath: fileInfo.projectRelativePath,
docRoot: path
.dirname(fileInfo.projectRelativePath)
.replace(/[^/]+/g, ".."),
docRoot: path.dirname(fileInfo.projectRelativePath).replace(/[^/]+/g, ".."),
},
validations,
template: "spec-jest.ts.njk",
......
......@@ -22,44 +22,41 @@ module.exports = function parseValidatesProcessor(
return;
}
doc.content = doc.content.replace(
VALIDATE_REGEX,
function processValidate(match, attributeText, validateMarkup) {
const attr = extractAttributes(attributeText);
if (!attr.name) {
throw new Error(
createDocMessage("Inline validation is missing name", doc)
);
}
doc.content = doc.content.replace(VALIDATE_REGEX, function processValidate(
match,
attributeText,
validateMarkup
) {
const attr = extractAttributes(attributeText);
if (!attr.name) {
throw new Error(createDocMessage("Inline validation is missing name", doc));
}
const name = attr.name;
const rules = attr.rules ? attr.rules.split(/ +/) : undefined;
const showResults = attr.results ? Boolean(attr.results) : "auto";
const elements = readElements(doc.fileInfo, attr.elements);
const id = uniqueName(validateMap, `markup-${attr.name}`);
const markup = trimIndentation(validateMarkup);
const config = generateConfig(rules, elements, attr);
const name = attr.name;
const rules = attr.rules ? attr.rules.split(/ +/) : undefined;
const showResults = attr.results ? Boolean(attr.results) : "auto";
const elements = readElements(doc.fileInfo, attr.elements);
const id = uniqueName(validateMap, `markup-${attr.name}`);
const markup = trimIndentation(validateMarkup);
const config = generateConfig(rules, elements, attr);
const validate = {
config,
name,
markup,
showResults,
id,
doc,
};
const validate = {
config,
name,
markup,
showResults,
id,
doc,
};
// store the validate information for later
log.debug("Storing inline validation", id);
validateMap.set(id, validate);
// store the validate information for later
log.debug("Storing inline validation", id);
validateMap.set(id, validate);
return `{@inlineValidation ${id}}`;
}
);
return `{@inlineValidation ${id}}`;
});
} catch (error) {
throw new Error(
createDocMessage("Failed to parse inline validation", doc, error)
);
throw new Error(createDocMessage("Failed to parse inline validation", doc, error));
}
});
}
......
......@@ -41,9 +41,7 @@ module.exports = function rulesProcessor(renderDocsProcessor) {
/* compule rule source paths */
ruleDocs.forEach((doc) => {
const docPath = doc.fileInfo.projectRelativePath;
doc.ruleSourcePath = docPath
.replace("docs", "src")
.replace(/\.md$/, ".ts");
doc.ruleSourcePath = docPath.replace("docs", "src").replace(/\.md$/, ".ts");
});
/* generate title */
......
......@@ -7,11 +7,7 @@ module.exports = new Package("schema", [])
.processor(require("./processors/copy-schema-processor"))
.factory(require("./services/copy-schema"))
.config(function (
computeIdsProcessor,
computePathsProcessor,
templateFinder
) {
.config(function (computeIdsProcessor, computePathsProcessor, templateFinder) {
templateFinder.templateFolders.push(path.resolve(packagePath, "templates"));
computeIdsProcessor.idTemplates.push({
docTypes: ["schema"],
......
module.exports = [
require("./block"),
require("./category"),
require("./summary"),
];
module.exports = [require("./block"), require("./category"), require("./summary")];
......@@ -112,10 +112,7 @@ Assert that a specific error is present in an HTML-Validate report.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toHaveError(
"close-order",
"Mismatched close-tag, expected '</p>' but found '</i>'"
);
expect(report).toHaveError("close-order", "Mismatched close-tag, expected '</p>' but found '</i>'");
```
### `toHaveErrors(errors: Array<[string, string] | object>)`
......
......@@ -23,9 +23,7 @@ beforeEach(() => {
});
it("should give error when using <div> as content", () => {
const report = htmlvalidate.validateString(
"<my-component><div>lorem ipsum</div></my-component>"
);
const report = htmlvalidate.validateString("<my-component><div>lorem ipsum</div></my-component>");
expect(report.valid).toBeFalsy();
expect(report.errorCount).toEqual(1);
expect(report.results[0].messages[0]).toMatchInlineSnapshot(`
......@@ -49,9 +47,7 @@ When using Jest in particular there are helper functions to make it even easier:
require("html-validate/build/matchers");
it("should give error when using <div> as content", () => {
const report = htmlvalidate.validateString(
"<my-component><div>lorem ipsum</div></my-component>"
);
const report = htmlvalidate.validateString("<my-component><div>lorem ipsum</div></my-component>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
......
......@@ -314,9 +314,7 @@
"dt": {
"implicitClosed": ["dd", "dt"],
"permittedContent": ["@flow"],
"permittedDescendants": [
{ "exclude": ["header", "footer", "@sectioning", "@heading"] }
],
"permittedDescendants": [{ "exclude": ["header", "footer", "@sectioning", "@heading"] }],
"requiredAncestors": ["dl > dt", "dl > div > dt"]
},
......@@ -551,16 +549,7 @@
"capture": [],
"checked": [],
"disabled": [],
"inputmode": [
"none",
"text",
"decimal",
"numeric",
"tel",
"search",
"email",
"url"
],
"inputmode": ["none", "text", "decimal", "numeric", "tel", "search", "email", "url"],
"multiple": [],
"readonly": [],
"required": [],
......@@ -967,14 +956,7 @@
"required": [],
"size": ["/\\d+/"]
},
"permittedContent": [
"@script",
"datasrc",
"datafld",
"dataformatas",
"option",
"optgroup"
]
"permittedContent": ["@script", "datasrc", "datafld", "dataformatas", "option", "optgroup"]
},
"slot": {
......@@ -1067,27 +1049,13 @@
"rules",
"width"
],
"permittedContent": [
"@script",
"caption?",
"colgroup",
"tbody",
"tfoot?",
"thead?",
"tr"
],
"permittedContent": ["@script", "caption?", "colgroup", "tbody", "tfoot?", "thead?", "tr"],
"permittedOrder": ["caption", "colgroup", "thead", "tbody", "tr", "tfoot"]
},
"tbody": {
"implicitClosed": ["tbody", "tfoot"],
"deprecatedAttributes": [
"align",
"background",
"char",
"charoff",
"valign"
],
"deprecatedAttributes": ["align", "background", "char", "charoff", "valign"],
"permittedContent": ["@script", "tr"]
},
......@@ -1145,13 +1113,7 @@
"tfoot": {
"implicitClosed": ["tbody"],
"deprecatedAttributes": [
"align",
"background",
"char",
"charoff",
"valign"
],
"deprecatedAttributes": ["align", "background", "char", "charoff", "valign"],
"permittedContent": ["@script", "tr"]
},
......@@ -1177,20 +1139,12 @@
],
"requiredAttributes": ["scope"],
"permittedContent": ["@flow"],
"permittedDescendants": [
{ "exclude": ["header", "footer", "@sectioning", "@heading"] }
]
"permittedDescendants": [{ "exclude": ["header", "footer", "@sectioning", "@heading"] }]
},
"thead": {
"implicitClosed": ["tbody", "tfoot"],
"deprecatedAttributes": [
"align",
"background",
"char",
"charoff",
"valign"
],
"deprecatedAttributes": ["align", "background", "char", "charoff", "valign"],
"permittedContent": ["@script", "tr"]
},
......@@ -1207,14 +1161,7 @@
"tr": {
"implicitClosed": ["tr"],
"deprecatedAttributes": [
"align",
"background",
"bgcolor",
"char",
"charoff",
"valign"
],
"deprecatedAttributes": ["align", "background", "bgcolor", "char", "charoff", "valign"],
"permittedContent": ["@script", "td", "th"]
},
......
......@@ -195,8 +195,7 @@ describe("HTML elements", () => {
});
for (const tagName of tagNames) {
const filename = (variant: string): string =>
`${fileDirectory}/${tagName}-${variant}.html`;
const filename = (variant: string): string => `${fileDirectory}/${tagName}-${variant}.html`;
describe(`<${tagName}>`, () => {
it("valid markup", () => {
......
This diff is collapsed.
{
"name": "html-validate",
"version": "3.3.0",
"version": "3.4.0",
"description": "html linter",
"keywords": [
"html",
......@@ -80,7 +80,7 @@
"dependencies": {
"@babel/code-frame": "^7.0.0",
"@sidvind/better-ajv-errors": "^0.6.9",
"acorn-walk": "^7.0.0",
"acorn-walk": "^8.0.0",
"ajv": "^6.10.0",
"chalk": "^4.0.0",
"deepmerge": "^4.0.0",
......@@ -94,20 +94,20 @@
"devDependencies": {
"@babel/core": "7.11.6",
"@babel/preset-env": "7.11.5",
"@commitlint/cli": "9.1.2",
"@commitlint/cli": "11.0.0",
"@html-validate/commitlint-config": "1.0.3",
"@html-validate/eslint-config": "1.5.17",
"@html-validate/jest-config": "1.0.24",
"@html-validate/prettier-config": "1.0.1",
"@html-validate/semantic-release-config": "1.0.36",
"@types/babel__code-frame": "7.0.1",
"@html-validate/eslint-config": "1.5.21",
"@html-validate/jest-config": "1.0.28",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.0.40",
"@types/babel__code-frame": "7.0.2",
"@types/estree": "0.0.45",
"@types/glob": "7.1.3",
"@types/inquirer": "7.3.1",
"@types/jest": "26.0.13",
"@types/jest": "26.0.14",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.0",
"@types/node": "11.15.21",
"@types/node": "11.15.29",
"autoprefixer": "9.8.6",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
......@@ -129,24 +129,24 @@
"grunt-contrib-copy": "1.0.0",
"grunt-postcss": "0.9.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.1.2",
"highlight.js": "10.2.1",
"husky": "4.3.0",
"jest": "26.4.2",
"jest-diff": "26.4.2",
"jest": "26.5.2",
"jest-diff": "26.5.2",
"jquery": "3.5.1",
"lint-staged": "10.3.0",
"lint-staged": "10.4.0",
"load-grunt-tasks": "5.1.0",
"marked": "1.1.1",
"marked": "1.2.0",
"minimatch": "3.0.4",
"prettier": "2.1.1",
"pretty-format": "26.4.2",
"sass": "1.26.10",
"semantic-release": "17.1.1",
"prettier": "2.1.2",
"pretty-format": "26.5.2",
"sass": "1.26.11",
"semantic-release": "17.1.2",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "26.3.0",
"typescript": "4.0.2"
"ts-jest": "26.4.1",
"typescript": "4.0.3"
},
"jest": {
"preset": "@html-validate/jest-config",
......
......@@ -33,9 +33,7 @@ function syncMock(pattern: string, options: Options = {}): string[] {
let src = mockFiles;
if (dir) {
src = src
.filter((cur) => cur.startsWith(dir))
.map((cur) => cur.slice(dir.length));
src = src.filter((cur) => cur.startsWith(dir)).map((cur) => cur.slice(dir.length));
}
return src.filter((cur) => minimatch(cur, pattern));
......
......@@ -28,10 +28,7 @@ export class CLI {
this.config = this.getConfig();
}
public expandFiles(
patterns: string[],
options: ExpandOptions = {}
): string[] {
public expandFiles(patterns: string[], options: ExpandOptions = {}): string[] {
return expandFiles(patterns, options);
}
......@@ -77,9 +74,7 @@ export class CLI {
config.rules = rules;
} catch (e) {
// istanbul ignore next
throw new UserError(
`Error while parsing --rule option "{${raw}}": ${e.message}.\n`
);
throw new UserError(`Error while parsing --rule option "{${raw}}": ${e.message}.\n`);
}
}
return config;
......
......@@ -37,10 +37,7 @@ function directoryPattern(extensions: string[]): string {
* Takes a number of file patterns (globs) and returns array of expanded
* filenames.
*/
export function expandFiles(
patterns: string[],
options: ExpandOptions
): string[] {
export function expandFiles(patterns: string[], options: ExpandOptions): string[] {
const cwd = options.cwd || process.cwd();
const extensions = options.extensions || DEFAULT_EXTENSIONS;
......
......@@ -109,8 +109,6 @@ describe("cli/formatters", () => {
it("should throw error when formatter is missing", () => {
expect.assertions(1);
expect(() => cli.getFormatter("missing")).toThrow(
'No formatter named "missing"'
);
expect(() => cli.getFormatter("missing")).toThrow('No formatter named "missing"');
});
});
......@@ -6,10 +6,7 @@ import { UserError } from "../error";
type WrappedFormatter = (results: Result[]) => string;
function wrap(
formatter: Formatter,
dst: string
): (results: Result[]) => string {
function wrap(formatter: Formatter, dst: string): (results: Result[]) => string {
return (results: Result[]) => {
const output = formatter(results);
if (dst) {
......
......@@ -82,9 +82,7 @@ function dump(files: string[], mode: Mode): string {
lines = files.map((filename: string) => htmlvalidate.dumpTree(filename));
break;
case Mode.DUMP_SOURCE:
lines = files.map((filename: string) =>
htmlvalidate.dumpSource(filename)
);
lines = files.map((filename: string) => htmlvalidate.dumpSource(filename));
break;
default:
throw new Error(`Unknown mode "${mode}"`);
......@@ -94,9 +92,7 @@ function dump(files: string[], mode: Mode): string {
}
function renameStdin(report: Report, filename: string): void {
const stdin = report.results.find(
(cur: Result) => cur.filePath === "/dev/stdin"
);
const stdin = report.results.find((cur: Result) => cur.filePath === "/dev/stdin");
if (stdin) {
stdin.filePath = filename;
}
......@@ -146,16 +142,7 @@ function handleUnknownError(err: Error): void {
}
const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), {
string: [
"c",
"config",
"ext",
"f",
"formatter",
"max-warnings",
"rule",
"stdin-filename",
],
string: ["c", "config", "ext", "f", "formatter", "max-warnings", "rule", "stdin-filename"],
boolean: [
"init",
"dump-events",
......@@ -250,9 +237,7 @@ const htmlvalidate = cli.getValidator();
/* sanity check: ensure maxWarnings has a valid value */
if (isNaN(maxWarnings)) {
console.log(
`Invalid value "${argv["max-warnings"]}" given to --max-warnings`
);
console.log(`Invalid value "${argv["max-warnings"]}" given to --max-warnings`);
process.exit(1);
}
......@@ -279,9 +264,7 @@ try {
process.stdout.write(formatter(result));
if (maxWarnings >= 0 && result.warningCount > maxWarnings) {
console.log(
`\nhtml-validate found too many warnings (maxiumum: ${maxWarnings}).`
);
console.log(`\nhtml-validate found too many warnings (maxiumum: ${maxWarnings}).`);
result.valid = false;
}
......
......@@ -67,8 +67,7 @@ export async function init(cwd: string): Promise<InitResult> {
type: "confirm",
default: false,
when: exists,
message:
"A .htmlvalidate.json file already exists, do you want to overwrite it?",
message: "A .htmlvalidate.json file already exists, do you want to overwrite it?",
},
{
name: "frameworks",
......
type RuleSeverity = "off" | "warn" | "error" | number;
export type RuleConfig = Record<
string,
RuleSeverity | [RuleSeverity] | [RuleSeverity, any]
>;
export type RuleConfig = Record<string, RuleSeverity | [RuleSeverity] | [RuleSeverity, any]>;
export interface TransformMap {
[key: string]: string;
......
......@@ -59,11 +59,9 @@ describe("ConfigLoader", () => {
it("should load configuration", () => {
expect.assertions(1);
jest
.spyOn(fs, "existsSync")
.mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/to/.htmlvalidate.json");
});
jest.spyOn(fs, "existsSync").mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/to/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect.objectContaining({
......@@ -77,11 +75,9 @@ describe("ConfigLoader", () => {
it("should load configuration from parent directory", () => {
expect.assertions(1);
jest
.spyOn(fs, "existsSync")
.mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/.htmlvalidate.json");
});
jest.spyOn(fs, "existsSync").mockImplementation((filename: fs.PathLike) => {
return filename === path.resolve("/path/.htmlvalidate.json");
});
const config = loader.fromTarget("/path/to/target.html");
expect(config.get()).toEqual(
expect.objectContaining({
......@@ -211,11 +207,7 @@ describe("ConfigLoader", () => {
/* extract only relevant rules from configuration to avoid bloat when new
* rules are added to recommended config */
function filter(src: Config): ConfigData {
const whitelisted = [
"no-self-closing",
"deprecated",
"element-permitted-content",
];
const whitelisted = ["no-self-closing", "deprecated", "element-permitted-content"];
const data = src.get();
data.rules = Object.keys(data.rules)
.filter((key) => whitelisted.includes(key))
......@@ -227,15 +219,13 @@ describe("ConfigLoader", () => {
}
const files = glob.sync("test-files/config/**/*.html");
files.forEach((filename: string) => {
it(filename, () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate();
const config = htmlvalidate.getConfigFor(filename);
const report = htmlvalidate.validateFile(filename);
expect(filter(config)).toMatchSnapshot();
expect(report.results).toMatchSnapshot();
});
it.each(files)("%s", (filename: string) => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate();
const config = htmlvalidate.getConfigFor(filename);
const report = htmlvalidate.validateFile(filename);
expect(filter(config)).toMatchSnapshot();
expect(report.results).toMatchSnapshot();
});
});
});
......@@ -128,9 +128,7 @@ describe("config", () => {
expect.assertions(2);
const config = Config.fromObject({ rules: { foo: "error" } });
expect(config.get().rules).toEqual({ foo: "error" });
expect(Array.from(config.getRules().entries())).toEqual([
["foo", [Severity.ERROR, {}]],
]);
expect(Array.from(config.getRules().entries())).toEqual([["foo", [Severity.ERROR, {}]]]);
});
it("should parse severity from string", () => {
......@@ -316,9 +314,7 @@ describe("config", () => {
describe("expandRelative()", () => {
it("should expand ./foo", () => {
expect.assertions(1);
expect(Config.expandRelative("./foo", "/path")).toEqual(
path.join(path.sep, "path", "foo")
);
expect(Config.expandRelative("./foo", "/path")).toEqual(path.join(path.sep, "path", "foo"));
});
it("should expand ../foo", () => {
......@@ -685,17 +681,14 @@ describe("config", () => {
},
});
config.init();
expect(() =>
config.transformSource(source)
).toThrowErrorMatchingSnapshot();
expect(() => config.transformSource(source)).toThrowErrorMatchingSnapshot();
});
it("should throw sane error when transformer fails to load", () => {
expect.assertions(1);
const config = Config.fromObject({
transform: {
"^.*\\.foo$":
"missing-transformer" /* mocked transformer, see top of file */,
"^.*\\.foo$": "missing-transformer" /* mocked transformer, see top of file */,
},
});
expect(() => config.init()).toThrowErrorMatchingSnapshot();
......@@ -759,8 +752,7 @@ describe("config", () => {
},
});
config.init();
expect(config.transformFilename("test-files/parser/simple.html"))
.toMatchInlineSnapshot(`
expect(config.transformFilename("test-files/parser/simple.html")).toMatchInlineSnapshot(`
Array [
Object {
"column": 1,
......@@ -804,9 +796,7 @@ describe("config", () => {
it("should handle being called multiple times", () => {
expect.assertions(1);
const config = Config.fromObject({});
const spy = jest
.spyOn(config as any, "precompileTransformers")
.mockReturnValue([]);
const spy = jest.spyOn(config as any, "precompileTransformers").mockReturnValue([]);
config.init();
config.init();
expect(spy).toHaveBeenCalledTimes(1);
......@@ -829,9 +819,7 @@ describe("config", () => {
plugins: ["mock-plugin"],
});
config.init();
expect(config.getPlugins()).toEqual([
expect.objectContaining({ name: "mock-plugin" }),
]);
expect(config.getPlugins()).toEqual([expect.objectContaining({ name: "mock-plugin" })]);
});
});
......
......@@ -69,10 +69,7 @@ function loadFromFile(filename: string): ConfigData {
/* load using require as it can process both js and json */
json = require(filename); // eslint-disable-line import/no-dynamic-require
} catch (err) {
throw new ConfigError(
`Failed to read configuration from "${filename}"`,
err
);
throw new ConfigError(`Failed to read configuration from "${filename}"`, err);
}
/* expand any relative paths */
......@@ -116,10 +113,7 @@ export class Config {
/**
* Create configuration from object.
*/
public static fromObject(
options: ConfigData,
filename: string | null = null
): Config {
public static fromObject(options: ConfigData, filename: string | null = null): Config {
Config.validate(options, filename);
return new Config(options);
}
......@@ -204,9 +198,7 @@ export class Config {
}
/* precompile transform patterns */
this.transformers = this.precompileTransformers(
this.config.transform || {}
);
this.transformers = this.precompileTransformers(this.config.transform || {});
this.initialized = true;
}
......@@ -287,10 +279,7 @@ export class Config {
// eslint-disable-next-line security/detect-non-literal-require, import/no-dynamic-require
metaTable.loadFromObject(require(entry));
} catch (err) {
throw new ConfigError(
`Failed to load elements from "${entry}": ${err.message}`,
err
);
throw new ConfigError(`Failed to load elements from "${entry}": ${err.message}`, err);
}
}
......@@ -356,18 +345,12 @@ export class Config {
return plugins.map((moduleName: string) => {
try {
// eslint-disable-next-line security/detect-non-literal-require, import/no-dynamic-require
const plugin = require(moduleName.replace(
"<rootDir>",
this.rootDir
)) as LoadedPlugin;
const plugin = require(moduleName.replace("<rootDir>", this.rootDir)) as LoadedPlugin;
plugin.name = plugin.name || moduleName;
plugin.originalName = moduleName;
return plugin;
} catch (err) {
throw new ConfigError(
`Failed to load plugin "${moduleName}": ${err}`,
err
);
throw new ConfigError(`Failed to load plugin "${moduleName}": ${err}`, err);
}
});
}
......@@ -437,21 +420,15 @@ export class Config {
};
if (transformer) {
try {
return Array.from(
transformer.fn.call(context, source),
(cur: Source) => {
/* keep track of which transformers that has been run on this source
* by appending this entry to the transformedBy array */
cur.transformedBy = cur.transformedBy || [];
cur.transformedBy.push(transformer.name);
return cur;
}
);
return Array.from(transformer.fn.call(context, source), (cur: Source) => {