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.