...
 
Commits (25)
......@@ -13,3 +13,4 @@
# nunjucks templates
/docs/dgeni/inline-validate/templates
/docs/dgeni/schema/templates
# html-validate changelog
# [2.9.0](https://gitlab.com/html-validate/html-validate/compare/v2.8.2...v2.9.0) (2020-01-17)
### Features
- **jest:** add `toHTMLValidate()` ([44388ea](https://gitlab.com/html-validate/html-validate/commit/44388ea0f759a33831967859386299d95b528c63))
- **rules:** check references from `aria-controls` ([9e9805d](https://gitlab.com/html-validate/html-validate/commit/9e9805dc0e89e92411f7845a4fedc7ade0ca8cdd))
## [2.8.2](https://gitlab.com/html-validate/html-validate/compare/v2.8.1...v2.8.2) (2020-01-09)
### Bug Fixes
......
......@@ -9,7 +9,7 @@ HTML-validate was created by David Sveningsson in early 2016 with a few goals in
- Enterprise and privacy friendly: no data should leave the machine.
- Pluggable and extendable: must be easy to extend with own domain-specific
functionallity and rules.
functionality and rules.
- Strict and non-forgiving: should never try to autocorrect or guess anything.
- First-class support for views, components and templates, including when using
javascript frameworks.
......
.alert.alert-info {
ul,
p {
margin-top: 0.5rem;
}
}
......@@ -7,11 +7,21 @@ $icon-font-path: "fonts/";
@import "anchorlink";
@import "frontpage";
@import "alert";
main {
padding-bottom: 3rem;
}
header {
.nav > li > a {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
padding-left: 10px;
padding-right: 10px;
}
}
}
footer {
border-top: 1px solid $navbar-default-border;
padding: 3rem 0;
......
......@@ -85,7 +85,7 @@ about to be closed.
}
```
Emitted when an elemnet is fully constructed (including its children). `target`
Emitted when an element is fully constructed (including its children). `target`
will be the the element.
## `attr`
......@@ -130,5 +130,5 @@ Emitted when inter-element, leading and trailing whitespace is parsed.
```
Emitted when a conditional comment `<![conditional]>` is parsed. The parser
ignores and condition and run all possbile branches but raises the event for any
ignores and condition and run all possible branches but raises the event for any
rules that wishes to do anything with it.
......@@ -145,4 +145,4 @@ both the key and value. If the attribute is processed with scripting
### `processElement`
Called after element is fully created but before children are parsed. Can be
used to manipluate elements (e.g. add dynamic text from frameworks).
used to manipulate elements (e.g. add dynamic text from frameworks).
......@@ -19,7 +19,7 @@ if (!report.valid) {
}
```
`validateFile` is a highlevel API which automatically locates configuration
`validateFile` is a high-level API which automatically locates configuration
files, load plugins, runs any transformations etc and is very similar to using
the CLI tool (in fact, the CLI tool uses this very API).
......@@ -143,7 +143,7 @@ htmlvalidate.flushConfigCache();
htmlvalidate.flushConfigCache("myfile.html");
```
## Unittesting
## Unit testing
If using jest to write tests there is a couple of helpers to assist writing
tests:
......
......@@ -118,7 +118,7 @@ e.g. rules that requires initialization.
If needed the callback may setup event listeners for [parser
events](/dev/events.html) (same as rules).
The callback may not manpiulate the source object.
The callback may not manipulate the source object.
## Configuration presets
......@@ -163,7 +163,7 @@ module.exports = {
};
```
This makes the rules accessable as usual when configuring in
This makes the rules accessible as usual when configuring in
`.htmlvalidate.json`:
```js
......
......@@ -30,6 +30,10 @@ module.exports = new Package("html-validate-docs", [
readFilesProcessor.fileReaders.push(changelogFileReader);
})
.config(function(getLinkInfo) {
getLinkInfo.relativeLinks = true;
})
.config(function(log, readFilesProcessor, writeFilesProcessor, copySchema) {
log.level = "info";
......@@ -48,7 +52,7 @@ module.exports = new Package("html-validate-docs", [
},
];
copySchema.outputFolder = "public/schemas";
copySchema.outputFolder = "schemas";
copySchema.files = ["src/schema/elements.json", "src/schema/config.json"];
writeFilesProcessor.outputFolder = "public";
......@@ -135,4 +139,11 @@ module.exports = new Package("html-validate-docs", [
.config(function(checkAnchorLinksProcessor) {
checkAnchorLinksProcessor.ignoredLinks.push(/^\/$/);
checkAnchorLinksProcessor.ignoredLinks.push(/^\/changelog$/);
checkAnchorLinksProcessor.checkDoc = doc => {
return (
doc.path &&
doc.outputPath &&
[".html", ".json"].includes(path.extname(doc.outputPath))
);
};
});
const path = require("canonical-path");
const packagePath = __dirname;
const Package = require("dgeni").Package;
module.exports = new Package("schema", [])
.processor(require("./processors/copy-schema-processor"))
.factory(require("./services/copy-schema"));
.factory(require("./services/copy-schema"))
.config(function(computeIdsProcessor, computePathsProcessor, templateFinder) {
templateFinder.templateFolders.push(path.resolve(packagePath, "templates"));
computeIdsProcessor.idTemplates.push({
docTypes: ["schema"],
getAliases: function(doc) {
return [doc.id, doc.name];
},
});
computePathsProcessor.pathTemplates.push({
docTypes: ["schema"],
outputPathTemplate: "${path}",
});
});
const path = require("path");
const fs = require("fs");
const mkdirp = require("mkdirp");
module.exports = function copySchemaProcessor(copySchema, readFilesProcessor) {
return {
......@@ -8,15 +7,20 @@ module.exports = function copySchemaProcessor(copySchema, readFilesProcessor) {
$process,
};
function $process() {
function $process(docs) {
const root = readFilesProcessor.basePath;
const outputFolder = path.join(root, copySchema.outputFolder);
mkdirp.sync(outputFolder);
const outputFolder = copySchema.outputFolder;
for (const src of copySchema.files) {
const name = path.basename(src);
fs.copyFileSync(path.join(root, src), path.join(outputFolder, name));
const { base, name } = path.parse(src);
docs.push({
id: `schema:${name}`,
name: `schemas/${name}`,
docType: "schema",
fileContents: fs.readFileSync(path.join(root, src), "utf-8"),
path: path.join(outputFolder, base),
template: "schema.json",
});
}
}
};
......@@ -31,23 +31,38 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="/">HTML-validate</a>
<a class="navbar-brand" href="/">HTML-validate <small class="visible-xs-inline visible-sm-inline">v{{ pkg.version }}</small></a>
</div>
<div class="collapse navbar-collapse" id="navbar">
<ul class="nav navbar-nav">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">User guide <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/usage">Gettings started</a></li>
<li><a href="/usage">Getting started</a></li>
<li><a href="/usage/cli.html">Using CLI</a></li>
<li><a href="/usage/elements.html">Elements</a></li>
<li><a href="/usage/transformers.html">Transfomers</a></li>
<li role="separator" class="divider"></li>
<li><a href="/usage/vscode.html">VS Code</a></li>
<li role="separator" class="divider"></li>
<li><a href="/frameworks/angularjs.html">AngularJS</a></li>
<li><a href="/usage/grunt.html">Grunt</a></li>
<li><a href="/frameworks/jest.html">Jest</a></li>
<li><a href="/usage/protractor.html">Protractor</a></li>
<li><a href="/frameworks/vue.html">Vue.js</a></li>
</ul>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Elements <span class="caret"></span></a>
<ul class="dropdown-menu">
<li>{@link guide/metadata/simple-component A simple component}</li>
<li>{@link guide/metadata/restrict-content Restricting element content}</li>
<li>{@link guide/metadata/restrict-attributes Restricting element attributes}</li>
<li>{@link guide/metadata/inheritance Inheritance}</li>
<li>{@link guide/metadata/best-practice Best practice}</li>
<li>{@link guide/metadata/writing-tests Writing tests}</li>
</ul>
</li>
<li><a href="/rules">Rules</a></li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">Developers guide <span class="caret"></span></a>
......@@ -65,7 +80,7 @@
<li><a href="/changelog">Changelog</a></li>
<li><a href="/about">About</a></li>
</ul>
<p class="navbar-text navbar-right">{{ pkg.name }}-{{ pkg.version }}</p>
<p class="navbar-text navbar-right hidden-xs hidden-sm">{{ pkg.name }}-{{ pkg.version }}</p>
</div>
</div>
</nav>
......
---
docType: content
title: AngularJS
---
# Usage with AngularJS
npm install html-validate-angular
[html-validate-angular](https://www.npmjs.com/package/html-validate-angular) is needed to transform `.html` and `.js` files using AngularJS.
Configure with:
```json
{
"elements": ["html5"],
"transform": {
"^.*\\.js$": "html-validate-angular/js",
"^.*\\.html$": "html-validate-angular/html"
}
}
```
---
docType: content
title: Usage with Jest
---
# Usage with Jest
`html-validate` comes with Jest support built-in.
In you test import `matchers`:
```js
import "html-validate/build/matchers";
```
This makes all the custom matchers available.
## API
### `toHTMLValidate(config?: ConfigData, filename?: string)`
Validates a string of HTML and passes the assertion if the markup is valid.
```js
expect("<p></p>").toHTMLValidate();
expect("<p></i>").not.toHTMLValidate();
```
You can also pass jsdom elements:
```js
const elem = document.createElement("div");
expect(elem).toHTMLValidate();
```
If needed a custom configuration can be passed:
```js
expect("<p></i>").toHTMLValidate({
rules: {
"close-order": "off",
},
});
```
By default configuration is also read from `.htmlvalidate.json` files where the test-case filename is used to match.
If you need to override this (perhaps because the test-case isn't in the same folder) you can pass in a custom filename as the third argument:
```js
expect("<p></i>").toHTMLValidate(null, "path/to/my-file.html");
```
Additionally, the `root` configuration property can be used to skip loading from `.htmlvalidate.json` but remember to actually include the rules you need:
```js
expect("<p></i>").toHTMLValidate({
extends: ["html-validate:recommended"],
root: true,
});
```
### `toBeValid()`
Assert that a HTML-Validate report is valid.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></p>");
expect(report).toBeValid();
```
### `toBeInvalid()`
Assert that a HTML-Validate report is invalid.
Inverse of `toBeValid()`.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toBeInvalid();
```
### `toHaveError(ruleId: string, message: string, context?: any)`
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>'"
);
```
### `toHaveErrors(errors: Array<[string, string] | object>)`
Similar to `toHaveError` but but asserts multiple errors.
The passed list must have the same length as the report.
Each error must either be `[ruleId, message]` or an object passed to `expect.objectContaining`.
```js
const htmlvalidate = new HtmlValidate();
const report = htmlvalidate.validateString("<p></i>");
expect(report).toHaveErrors([
["close-order", "Mismatched close-tag, expected '</p>' but found '</i>'"],
]);
```
or with object syntax:
```js
expect(report).toHaveErrors([
{
ruleId: "close-order",
message: "Mismatched close-tag, expected '</p>' but found '</i>'",
},
]);
```
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/guide/metadata/inheritance.md inline validation: inheritance 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 5,
"message": "Element <div> is not permitted as content in <my-component>",
"offset": 76,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 3,
},
],
"source": "<my-component>
<span>lorem ipsum</span>
</my-component>
<my-component>
<div>lorem ipsum</div>
</my-component>",
"warningCount": 0,
},
]
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/guide/metadata/restrict-attributes.md inline validation: boolean 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 15,
"context": undefined,
"line": 2,
"message": "Attribute \\"quacks\\" should omit value",
"offset": 54,
"ruleId": "attribute-boolean-style",
"severity": 2,
"size": 6,
},
Object {
"column": 23,
"context": Object {
"allowed": Array [],
"attribute": "quacks",
"element": "my-component",
"value": "duck",
},
"line": 2,
"message": "Attribute \\"quacks\\" has invalid value \\"duck\\"",
"offset": 62,
"ruleId": "attribute-allowed-values",
"severity": 2,
"size": 4,
},
],
"source": "<my-component quacks>...</my-component>
<my-component quacks=\\"duck\\">...</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-attributes.md inline validation: deprecated 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 15,
"context": undefined,
"line": 1,
"message": "Attribute \\"duck\\" is deprecated on <my-component> element",
"offset": 14,
"ruleId": "no-deprecated-attr",
"severity": 2,
"size": 4,
},
],
"source": "<my-component duck=\\"dewey\\">...</my-component>
<my-component>...</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-attributes.md inline validation: enum 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 21,
"context": Object {
"allowed": Array [
"huey",
"dewey",
"louie",
],
"attribute": "duck",
"element": "my-component",
"value": "flintheart",
},
"line": 2,
"message": "Attribute \\"duck\\" has invalid value \\"flintheart\\"",
"offset": 66,
"ruleId": "attribute-allowed-values",
"severity": 2,
"size": 10,
},
],
"source": "<my-component duck=\\"dewey\\">...</my-component>
<my-component duck=\\"flintheart\\">...</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-attributes.md inline validation: regexp 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 22,
"context": Object {
"allowed": Array [
/\\\\d\\+/,
],
"attribute": "ducks",
"element": "my-component",
"value": "huey",
},
"line": 2,
"message": "Attribute \\"ducks\\" has invalid value \\"huey\\"",
"offset": 64,
"ruleId": "attribute-allowed-values",
"severity": 2,
"size": 4,
},
],
"source": "<my-component ducks=\\"3\\">...</my-component>
<my-component ducks=\\"huey\\">...</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-attributes.md inline validation: required 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 2,
"context": Object {
"attribute": "duck",
"element": "my-component",
},
"line": 2,
"message": "<my-component> is missing required \\"duck\\" attribute",
"offset": 47,
"ruleId": "element-required-attributes",
"severity": 2,
"size": 12,
},
],
"source": "<my-component duck=\\"dewey\\">...</my-component>
<my-component>...</my-component>",
"warningCount": 0,
},
]
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/guide/metadata/restrict-content.md inline validation: descendants 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 6,
"context": undefined,
"line": 4,
"message": "Element <footer> is not permitted as descendant of <my-component>",
"offset": 63,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 6,
},
Object {
"column": 6,
"context": undefined,
"line": 7,
"message": "Element <my-component> is not permitted as descendant of <my-component>",
"offset": 137,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 12,
},
],
"source": "<my-component>
<!-- the div itself is allowed -->
<div>
<footer>
sectioning element can no longer be used
</footer>
<my-component>
nor can the component be nested
</my-component>
</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-content.md inline validation: exclude 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 4,
"message": "Element <h1> is not permitted as content in <my-component>",
"offset": 67,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 2,
},
],
"source": "<my-component>
<div>allowed</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/restrict-content.md inline validation: tags 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 2,
"message": "Element <button> is not permitted as content in <my-component>",
"offset": 18,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 6,
},
],
"source": "<my-component>
<button type=\\"button\\">click me!</button>
</my-component>",
"warningCount": 0,
},
]
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/guide/metadata/simple-component.md inline validation: basic-metadata 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 2,
"message": "Element <my-component> is not permitted as content in <div>",
"offset": 9,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 12,
},
],
"source": "<div>
<my-component>lorem ipsum</my-component>
</div>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/simple-component.md inline validation: flow--metadata-2 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 2,
"message": "Element <my-component> is not permitted as content in <span>",
"offset": 10,
"ruleId": "element-permitted-content",
"severity": 2,
"size": 12,
},
],
"source": "<span>
<my-component>lorem ipsum</my-component>
</span>",
"warningCount": 0,
},
]
`;
exports[`docs/guide/metadata/simple-component.md inline validation: flow-metadata-1 1`] = `Array []`;
exports[`docs/guide/metadata/simple-component.md inline validation: no-metadata-1 1`] = `Array []`;
exports[`docs/guide/metadata/simple-component.md inline validation: no-metadata-2 1`] = `Array []`;
exports[`docs/guide/metadata/simple-component.md inline validation: no-metadata-3 1`] = `Array []`;
exports[`docs/guide/metadata/simple-component.md inline validation: phrasing-metadata 1`] = `Array []`;
import HtmlValidate from "../../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["inheritance"] = `<my-component>
<span>lorem ipsum</span>
</my-component>
<my-component>
<div>lorem ipsum</div>
</my-component>`;
describe("docs/guide/metadata/inheritance.md", () => {
it("inline validation: inheritance", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"inherit":"label"}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["inheritance"]);
expect(report.results).toMatchSnapshot();
});
});
import HtmlValidate from "../../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["enum"] = `<my-component duck="dewey">...</my-component>
<my-component duck="flintheart">...</my-component>`;
markup["regexp"] = `<my-component ducks="3">...</my-component>
<my-component ducks="huey">...</my-component>`;
markup["boolean"] = `<my-component quacks>...</my-component>
<my-component quacks="duck">...</my-component>`;
markup["required"] = `<my-component duck="dewey">...</my-component>
<my-component>...</my-component>`;
markup["deprecated"] = `<my-component duck="dewey">...</my-component>
<my-component>...</my-component>`;
describe("docs/guide/metadata/restrict-attributes.md", () => {
it("inline validation: enum", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"attributes":{"duck":["huey","dewey","louie"]}}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["enum"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: regexp", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"attributes":{"ducks":["/\\d+/"]}}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["regexp"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: boolean", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"attributes":{"quacks":[]}}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["boolean"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: required", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"requiredAttributes":["duck"]}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["required"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: deprecated", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"deprecatedAttributes":["duck"]}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["deprecated"]);
expect(report.results).toMatchSnapshot();
});
});
import HtmlValidate from "../../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["tags"] = `<my-component>
<button type="button">click me!</button>
</my-component>`;
markup["exclude"] = `<my-component>
<div>allowed</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>`;
markup["descendants"] = `<my-component>
<!-- the div itself is allowed -->
<div>
<footer>
sectioning element can no longer be used
</footer>
<my-component>
nor can the component be nested
</my-component>
</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>`;
describe("docs/guide/metadata/restrict-content.md", () => {
it("inline validation: tags", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"permittedContent":["span","strong","em"]}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["tags"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: exclude", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"permittedContent":[{"exclude":"@heading"}]}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["exclude"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: descendants", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"footer":{"flow":true,"sectioning":true},"my-component":{"flow":true,"permittedDescendants":[{"exclude":["@sectioning","my-component"]}]}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["descendants"]);
expect(report.results).toMatchSnapshot();
});
});
import HtmlValidate from "../../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["no-metadata-1"] = `<!-- this is probably legal? -->
<div>
<my-component>lorem ipsum</my-component>
</div>
<!-- but should it work inside a span? -->
<span>
<my-component>lorem ipsum</my-component>
</span>`;
markup["no-metadata-2"] = `<!-- can it contain an interactive button? who knows? -->
<my-component>
<button type="button">click me!</button>
</my-component>
<!-- or is it allowed inside a button? -->
<button type="button">
<my-component>click me!</my-component>
</button>`;
markup["no-metadata-3"] = `<!-- lets nest the component for fun and profit! -->
<my-component>
<my-component>
<my-component>
Sup dawg I heard you like components so I put components inside your components.
</my-component>
</my-component>
</my-component>`;
markup["basic-metadata"] = `<div>
<my-component>lorem ipsum</my-component>
</div>`;
markup["flow-metadata-1"] = `<div>
<my-component>lorem ipsum</my-component>
</div>`;
markup["flow--metadata-2"] = `<span>
<my-component>lorem ipsum</my-component>
</span>`;
markup["phrasing-metadata"] = `<span>
<my-component>lorem ipsum</my-component>
</span>`;
describe("docs/guide/metadata/simple-component.md", () => {
it("inline validation: no-metadata-1", () => {
const htmlvalidate = new HtmlValidate({"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["no-metadata-1"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: no-metadata-2", () => {
const htmlvalidate = new HtmlValidate({"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["no-metadata-2"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: no-metadata-3", () => {
const htmlvalidate = new HtmlValidate({"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["no-metadata-3"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: basic-metadata", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["basic-metadata"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: flow-metadata-1", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["flow-metadata-1"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: flow--metadata-2", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["flow--metadata-2"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: phrasing-metadata", () => {
const htmlvalidate = new HtmlValidate({"elements":["html5",{"my-component":{"flow":true,"phrasing":true}}],"extends":["html-validate:recommended"]});
const report = htmlvalidate.validateString(markup["phrasing-metadata"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: content
title: Writing custom element metadata - Best practice
---
# Best practice
## For applications
### A1. Always include element metadata from libraries if present
Why: when using IDE plugins you get help using the library right in the editor.
Why: easier upgrade paths when the validation will help find bugs and deprecations.
Why: the library may include custom rules to prevent invalid usage.
### A2. Write element metadata for all custom components
Why: To get full benefits of the validator all custom elements must include metadata.
## For libraries
### L1. Bundle element metadata in the same package
Always bundle the element metadata in the same package as your library.
Why: this way the metadata is always in sync with the actual components and the end-user does not have to install a separate package.
How: include all element metadata in the same repository as your library and make sure the files are included when publishing package (e.g. ensure they are present in `package.json` `files` property)
### L2. Wrap in plugin with configuration preset
Always wrap the metadata in a plugin exposing a `recommended` configuration.
The end-user would then use the library with something similar to this without knowing much about where your files lives:
```json
{
"plugins": ["my-library/htmlvalidate"],
"extends": ["html-validate:recommended", "my-library:recommended"]
}
```
Why: it is easier for the end-user to use it as they don't need to keep track of how it is supposed to be used.
Why: files can be moved around easier as long as the plugin is intact.
Why: seamless for end-users if you later decide to add custom rules, recommended rules, etc.
How: create a plugin folder (preferably named `htmlvalidate`) in your repository with an `index.js` file implementing the `Plugin` API.
See documentation for {@link dev/writing-plugins writing plugins}.
### L3. Include metadata for all elements
No matter how insignificant always include element metadata for all components.
Why: without any metadata the element is silently ignored and can cause similar unnoticed errors for other elements.
Consider if A > B is disallowed but with an unknown C between A > C > B no errors will be yielded as it is not known if C can be content of A or if it can be the parent of B.
Why: it makes you think about intended semantics of your components.
Why: makes deprecations easier.
How: Use {@link rules/no-unknown-elements no-unknown-elements} rule to find elements you have not yet provided metadata for.
### L4. Use deprecations
If you ever find yourself removing an element or attribute mark it as deprecated.
Why: end-users get errors about using deprecated elements and attributes making it easier to migrate.
Why: a properly configured CI pipeline would fail the build.
How: deprecate elements using the `deprecated` property.
How: deprecate attributes using the `deprecatedAttributes` property.
### L5. Write unit-tests
Consider the metadata as part of your deliverable and thus you should write tests to ensure it always works as expected.
How: See {@link guide/metadata/writing-tests writing tests} for details how to write unit tests.
---
docType: content
title: Writing custom element metadata
---
# Writing custom element metadata
This is a guide to how to write metadata for custom elements and framework components.
In this series we learn how to write metadata for a fictive `<my-component>`.
- {@link guide/metadata/simple-component Part 1: A simple component}
- {@link guide/metadata/restrict-content Part 2: Restricting element content}
- {@link guide/metadata/restrict-attributes Part 3: Restricting element attributes}
- {@link guide/metadata/inheritance Part 4: Inheritance}
- {@link guide/metadata/best-practice Part 5: Best practice}
- {@link guide/metadata/writing-tests Part 6: Writing tests}
{
"my-component": {
"inherit": "label"
}
}
---
docType: content
title: Writing custom element metadata - Inheritance
---
# Writing custom element metadata: Inheritance
When writing custom elements it is common to wrap builtin elements and thus wanting to use the same rules.
One method is to simply copy it over to the component but then it won't be updated in case the original updates.
A better method is to use inheritance.
Lets assume our `<my-component>` is actually a wrapper for an input field with a label and the content is what is used as `<label>`.
Thus by inheriting from `<label>` we automatically get the same rules.
```json
{
"my-component": {
"inherit": "label"
}
}
```
<validate name="inheritance" elements="inheritance.json">
<my-component>
<span>lorem ipsum</span>
</my-component>
<my-component>
<div>lorem ipsum</div>
</my-component>
</validate>
When inheriting you can still override any properties from the inherited element.
<div class="alert alert-info">
<i class="fa fa-info-circle" aria-hidden="true"></i>
<strong>Note</strong>
<p>
Some elements has properties not suitable for inheriting.
For instance <code>&lt;input&gt;</code> is a <code>void</code> element but custom elements cannot be pure <code>void</code> thus if you inherit from it you must set <code>void</code> to <code>false</code>.
</p>
</div>
{
"my-component": {
"flow": true,
"attributes": {
"quacks": []
}
}
}
{
"my-component": {
"flow": true,
"deprecatedAttributes": ["duck"]
}
}
{
"my-component": {
"flow": true,
"attributes": {
"duck": ["huey", "dewey", "louie"]
}
}
}
{
"my-component": {
"flow": true,
"attributes": {
"ducks": ["/\\d+/"]
}
}
}
{
"my-component": {
"flow": true,
"requiredAttributes": ["duck"]
}
}
---
docType: content
title: Writing custom element metadata - Resticting element attributes
---
# Writing custom element metadata: Restricting element attributes
Similar to {@link guide/metadata/restrict-content restricting content} restricting attributes comes in a few different versions.
To define what values attribute accept the `attributes` property is used, to define what attributes are required use `requiredAttributes` and to mark an attribute as deprecated use `deprecatedAttributes`.
## Define attribute values
Assuming our `<my-component>` element has a `duck` attribute which can take the value `huey`, `dewey` or `louie` we can use the `attributes` property to define an enumerated list of allowed values:
```json
{
"my-component": {
"flow": true,
"attributes": {
"duck": ["huey", "dewey", "louie"]
}
}
}
```
<validate name="enum" elements="restrict-attributes-enum.json">
<my-component duck="dewey">...</my-component>
<my-component duck="flintheart">...</my-component>
</validate>
We can also specify regular expressions by surrounding the string with `/` (remember to escape special characters properly):
```json
{
"my-component": {
"flow": true,
"attributes": {
"ducks": ["/\\d+/"]
}
}
}
```
<validate name="regexp" elements="restrict-attributes-regexp.json">
<my-component ducks="3">...</my-component>
<my-component ducks="huey">...</my-component>
</validate>
<div class="alert alert-info">
<i class="fa fa-info-circle" aria-hidden="true"></i>
<strong>Tips</strong>
<ul>
<li>Regular expressions and enumeration can be used at the same time.</li>
<li>The empty string <code>""</code> is also valid and means that the value must be empty, e.g. <code>ducks=""</code></li>
</ul>
</div>
To force a boolean value similar to `disabled`, `selected` etc use an empty array `[]`:
```json
{
"my-component": {
"flow": true,
"attributes": {
"quacks": []
}
}
}
```
<validate name="boolean" elements="restrict-attributes-boolean.json">
<my-component quacks>...</my-component>
<my-component quacks="duck">...</my-component>
</validate>
## Define required attributes
To define a list of required attributes use `requiredAttributes`:
```json
{
"my-component": {
"flow": true,
"requiredAttributes": ["duck"]
}
}
```
<validate name="required" elements="restrict-attributes-required.json">
<my-component duck="dewey">...</my-component>
<my-component>...</my-component>
</validate>
## Deprecating attributes
Similar to required attribute we can use `deprecatedAttributes` to deprecate attributes:
```json
{
"my-component": {
"flow": true,
"deprecatedAttributes": ["duck"]
}
}
```
<validate name="deprecated" elements="restrict-attributes-deprecated.json">
<my-component duck="dewey">...</my-component>
<my-component>...</my-component>
</validate>
{
"footer": {
"flow": true,
"sectioning": true
},
"my-component": {
"flow": true,
"permittedDescendants": [{ "exclude": ["@sectioning", "my-component"] }]
}
}
{
"my-component": {
"flow": true,
"permittedContent": [{ "exclude": "@heading" }]
}
}
{
"my-component": {
"flow": true,
"permittedContent": ["span", "strong", "em"]
}
}
---
docType: content
title: Writing custom element metadata - Resticting element content
---
# Writing custom element metadata: Restricting element content
Looking back at our initial example we saw that the element accepted a `<button>` as content.
If we want to allow only phrasing content (`<span>`, `<strong>`, etc) inside we can use the `permittedContent` property to limit.
`permittedContent` is a list of allowed tags or content categories.
```json
{
"my-component": {
"flow": true,
"permittedContent": ["span", "strong", "em"]
}
}
```
<validate name="tags" elements="restrict-content-tags.json">
<my-component>
<button type="button">click me!</button>
</my-component>
</validate>
As it quickly get tedious to list all tag names we can refer to content categories directly:
```json
{
"my-component": {
"flow": true,
"permittedContent": ["@phrasing"]
}
}
```
The list can also be turned to a blacklist by using the `exclude` keyword:
```json
{
"my-component": {
"flow": true,
"permittedContent": [{ "exclude": "@heading" }]
}
}
```
<validate name="exclude" elements="restrict-content-exclude.json">
<my-component>
<div>allowed</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>
</validate>
<div class="alert alert-info">
<i class="fa fa-info-circle" aria-hidden="true"></i>
<strong>Tips</strong>
<p><code>exclude</code> is also useful to prevent interactive elements from disallowing other interactive elements by excluding <code>@interactive</code></p>
</div>
## Descendants
`permittedContent` validates direct children to the element but not deeper descendants.
The related `permittedDescendants` property checks all descendants.
Most of the time you should prefer `permittedContent` over `permittedDescendants` as each children should have its own metadata describing what should and should not be allowed and by allowing the element itself you should accept any descendants it may pull.
However, it can be used in circumstances where this is not possible.
The most common case is to prevent nesting of the component or limit usage of certain content categories such as sectioning or headings:
```json
{
"my-component": {
"flow": true,
"permittedDescendants": [{ "exclude": ["my-component", "@sectioning"] }]
}
}
```
<validate name="descendants" elements="restrict-content-descendants.json">
<my-component>
<!-- the div itself is allowed -->
<div>
<footer>
sectioning element can no longer be used
</footer>
<my-component>
nor can the component be nested
</my-component>
</div>
<span>also allowed</span>
<h1>not allowed</h1>
</my-component>
</validate>
<div class="alert alert-info">
<i class="fa fa-info-circle" aria-hidden="true"></i>
<strong>Rule of thumb</strong>
<ul>
<li>Use <code>permittedContent</code> to limit the elements you want to allow as content.</li>
<li>Use <code>permittedDescendants</code> to prevent nestling.</li>
<li>If the component wraps a <code>&lt;a&gt;</code> or <code>&lt;button&gt;</code> element use <code>permittedDescendants</code> exclude other interactive elements.</li>
</ul>
</div>
Other properties to limit content also exits, check the [element metadata reference](/usage/elements.html) for details.
### Case study: `<fieldset>`
```json
{
"fieldset": {
"flow": true,
"permittedContent": ["@flow", "legend?"],
"permittedOrder": ["legend", "@flow"],
"requiredContent": ["legend"]
}
}
```
Like we seen before the `permittedContent` property is used to restrict to only accept flow content and the `<legend>` element.
Note the usage of a trailing `?` after legend, this limits the allowed occurrences to 0 or 1 (2 or more is disallowed).
Next it uses `permittedOrder` to declare that `<legend>` must come before any flow content.
`permittedOrder` must not list all the possible elements from `permittedContent` but for the items listed the order must be adhered to.
Unlisted elements can be used in any order.
Lastly it uses `requiredContent` to declare that a `<legend>` element must be present.
To sum up, the `<fieldset>` elements puts the following restrictions in place:
- It must contain a single `<legend>` element.
- The `<legend>` element must come before any other content.
{
"my-component": {
"flow": true
}
}
{
"my-component": {
"flow": true,
"phrasing": true
}
}
---
docType: content
title: Writing custom element metadata - A simple component
---
# Writing custom element metadata: A simple component
HTML-Validate is fully data-driven and has no hardcoded rules for which elements exists and how they should work.
This data is called _element metadata_ and the validator bundles metadata for all HTML5 elements but makes no distinction between the bundled and user-provided metadata.
The bundled metadata is available in [html5.json](https://gitlab.com/html-validate/html-validate/blob/master/elements/html5.json).
See also:
- {@link schema:elements JSON Schema}.
- {@link usage/elements Full metadata documentation}.
## The component
Lets assume we have a custom element called `<my-component>`.
If this element has no metadata anything goes for this element and the validator cannot help much.
Lets start off with some examples:
<validate name="no-metadata-1" results="true">
<!-- this is probably legal? -->
<div>
<my-component>lorem ipsum</my-component>
</div>
<!-- but should it work inside a span? -->
<span>
<my-component>lorem ipsum</my-component>
</span>
</validate>
Depending on what the element consists of it might not be appropriate to use inside a `<span>` but there is not yet any metadata available to tell if `<my-component>` is allowed to be used in that context.
<validate name="no-metadata-2" results="true">
<!-- can it contain an interactive button? who knows? -->
<my-component>
<button type="button">click me!</button>
</my-component>
<!-- or is it allowed inside a button? -->
<button type="button">
<my-component>click me!</my-component>
</button>
</validate>