Commit f30de03e authored by David Sveningsson's avatar David Sveningsson

feat(rules): new rule `void-style`

refs #58
parent c93c63b1
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/void-style.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/void-style.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 7,
"context": Object {
"style": 1,
"tagName": "input",
},
"line": 1,
"message": "Expected omitted end tag <input> instead of self-closing element <input/>",
"offset": 6,
"ruleId": "void-style",
"selector": "input",
"severity": 2,
"size": 2,
},
],
"source": "<input/>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<input/>`;
markup["correct"] = `<input>`;
describe("docs/rules/void-style.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"void-style":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"void-style":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: void-style
category: style
summary: Require a specific style for closing void elements
---
# Require a specific style for closing void elements (`void-style`)
HTML [void elements](https://www.w3.org/TR/html5/syntax.html#void-elements) are elements which cannot have content.
Void elements are implicitly closed (`<img>`) but may optionally be XML-style self-closed (`<img/>`).
This rules enforces usage of one of the two styles.
Default is to omit self-closing tag.
Non-void elements are ignored by this rule.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="void-style">
<input/>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="void-style">
<input>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"style": "omit",
}
```
### Style
- `omit` requires end tag to be omitted and disallows self-closing
elements (default).
- `selfclosing` requests self-closing all void element.
......@@ -37,6 +37,7 @@ module.exports = {
"unrecognized-char-ref": "error",
void: "error",
"void-content": "error",
"void-style": "error",
"wcag/h30": "error",
"wcag/h32": "error",
"wcag/h36": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule void-style should have contextual documentation 1`] = `
Object {
"description": "The current configuration requires void elements to omit end tag, use <foo> instead.",
"url": "https://html-validate.org/rules/void-style.html",
}
`;
exports[`rule void-style should have contextual documentation 2`] = `
Object {
"description": "The current configuration requires void elements to be self-closed, use <bar/> instead.",
"url": "https://html-validate.org/rules/void-style.html",
}
`;
exports[`rule void-style should have documentation 1`] = `
Object {
"description": "The current configuration requires a specific style for ending void elements.",
"url": "https://html-validate.org/rules/void-style.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
describe("rule void-style", () => {
let htmlvalidate: HtmlValidate;
describe("default", () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": "error" },
});
});
it("should not report when void element omitted end tag", () => {
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should report when void element is self-closed", () => {
const report = htmlvalidate.validateString("<input/>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"void-style",
"Expected omitted end tag <input> instead of self-closing element <input/>"
);
});
it("should not report when non-void element has end tag", () => {
const report = htmlvalidate.validateString("<div></div>");
expect(report).toBeValid();
});
it("should not report when xml namespaces is used", () => {
const report = htmlvalidate.validateString("<xi:include/>");
expect(report).toBeValid();
});
});
describe('configured with style="omit"', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": ["error", { style: "omit" }] },
});
});
it("should not report when void element omits end tag", () => {
const report = htmlvalidate.validateString("<input>");
expect(report).toBeValid();
});
it("should report when void element is self-closed", () => {
const report = htmlvalidate.validateString("<input/>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"void-style",
"Expected omitted end tag <input> instead of self-closing element <input/>"
);
});
});
describe('configured with style="selfclose"', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": ["error", { style: "selfclose" }] },
});
});
it("should report when void element omits end tag", () => {
const report = htmlvalidate.validateString("<input>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"void-style",
"Expected self-closing element <input/> instead of omitted end-tag <input>"
);
});
it("should not report when void element is self-closed", () => {
const report = htmlvalidate.validateString("<input/>");
expect(report).toBeValid();
});
});
it("should throw error if configured with invalid value", () => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": ["error", { style: "foobar" }] },
});
expect(() => htmlvalidate.validateString("<input></input>")).toThrow(
`Invalid style "foobar" for "void-style" rule`
);
});
it("should have documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": "error" },
});
expect(htmlvalidate.getRuleDocumentation("void-style")).toMatchSnapshot();
});
it("should have contextual documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "void-style": "error" },
});
const context1 = {
style: 1,
tagName: "foo",
};
const context2 = {
style: 2,
tagName: "bar",
};
expect(
htmlvalidate.getRuleDocumentation("void-style", null, context1)
).toMatchSnapshot();
expect(
htmlvalidate.getRuleDocumentation("void-style", null, context2)
).toMatchSnapshot();
});
});
import { HtmlElement, NodeClosed } from "../dom";
import { TagCloseEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
const defaults = {
style: "omit",
};
enum Style {
AlwaysOmit = 1,
AlwaysSelfclose = 2,
}
interface RuleContext {
style: Style;
tagName: string;
}
interface RuleOptions {
style: string;
}
class VoidStyle extends Rule<RuleContext, RuleOptions> {
private style: Style;
public constructor(options: RuleOptions) {
super(Object.assign({}, defaults, options));
this.style = parseStyle(this.options.style);
}
public documentation(context: RuleContext): RuleDocumentation {
const doc: RuleDocumentation = {
description:
"The current configuration requires a specific style for ending void elements.",
url: ruleDocumentationUrl(__filename),
};
if (context) {
const [desc, end] = styleDescription(context.style);
doc.description = `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`;
}
return doc;
}
public setup(): void {
this.on("tag:close", (event: TagCloseEvent) => {
const active = event.previous; // The current active element (that is, the current element on the stack)
if (active && active.meta) {
this.validateActive(active);
}
});
}
private validateActive(node: HtmlElement): void {
/* ignore non-void elements, they must be closed with regular end tag */
if (!node.voidElement) {
return;
}
if (this.shouldBeOmitted(node)) {
this.report(
node,
`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
);
}
if (this.shouldBeSelfClosed(node)) {
this.report(
node,
`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
);
}
}
public report(node: HtmlElement, message: string): void {
const context: RuleContext = {
style: this.style,
tagName: node.tagName,
};
super.report(node, message, null, context);
}
private shouldBeOmitted(node: HtmlElement): boolean {
return (
this.style === Style.AlwaysOmit &&
node.closed === NodeClosed.VoidSelfClosed
);
}
private shouldBeSelfClosed(node: HtmlElement): boolean {
return (
this.style === Style.AlwaysSelfclose &&
node.closed === NodeClosed.VoidOmitted
);
}
}
function parseStyle(name: string): Style {
switch (name) {
case "omit":
return Style.AlwaysOmit;
case "selfclose":
case "selfclosing":
return Style.AlwaysSelfclose;
default:
throw new Error(`Invalid style "${name}" for "void-style" rule`);
}
}
function styleDescription(style: Style): [string, string] {
switch (style) {
case Style.AlwaysOmit:
return ["omit end tag", ""];
case Style.AlwaysSelfclose:
return ["be self-closed", "/"];
// istanbul ignore next: will only happen if new styles are added, otherwise this isn't reached
default:
throw new Error(`Unknown style`);
}
}
module.exports = VoidStyle;
{
"rules": {
"void": ["error", { "style": "selfclose" }]
"void": "off",
"void-style": ["error", { "style": "selfclose" }]
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment