Commit a328b558 authored by David Sveningsson's avatar David Sveningsson

feat: new rule `attribute-empty-style`

parent 5b6991b6
Pipeline #123027896 passed with stages
in 11 minutes and 31 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/attribute-empty-style.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/attribute-empty-style.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 1,
"message": "Attribute \\"download\\" should omit value",
"offset": 3,
"ruleId": "attribute-empty-style",
"selector": "a",
"severity": 2,
"size": 8,
},
],
"source": "<a download=\\"\\"></a>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<a download=""></a>`;
markup["correct"] = `<a download></a>`;
describe("docs/rules/attribute-empty-style.md", () => {
it("inline validation: incorrect", () => {
const htmlvalidate = new HtmlValidate({"rules":{"attribute-empty-style":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
const htmlvalidate = new HtmlValidate({"rules":{"attribute-empty-style":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -7,7 +7,19 @@ summary: Require a specific style for boolean attributes
# Boolean attribute style (`attribute-boolean-style`)
Require a specific style when writing boolean attributes.
Boolean attributes are attributes without a value and the presence of the attribute is considered a boolean `true` and absense a boolean `false`.
The [HTML5 standard][whatwg] allows three styles to write boolean attributes:
- `<input required>` (omitting the value)
- `<input required="">` (empty string)
- `<input required="required">` (attribute name)
This rule requires a specific style when writing boolean attributes.
Default is to omit the value.
This rule does not have an effect on regular attributes with empty values, see {@link attribute-empty-style} instead.
[whatwg]: https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#boolean-attributes
## Rule details
......
---
docType: rule
name: attribute-empty-style
category: style
summary: Require a specific style for empty attributes
---
# Empty attribute style (`attribute-empty-style`)
Attributes without a value is implied to be an empty string by the [HTML5 standard][whatwg], e.g. `<a download>` and `<a download="">` are equal.
This rule requires a specific style when writing empty attributes.
Default is to omit the empty string `""`.
This rule does not have an effect on boolean attributes, see {@link attribute-boolean-style} instead.
[whatwg]: https://html.spec.whatwg.org/multipage/syntax.html#attributes-2
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="attribute-empty-style">
<a download=""></a>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="attribute-empty-style">
<a download></a>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"style": "omit"
}
```
### Style
- `omit` require empty attributes to omit value, e.g. `<a download></a>`
- `empty` require empty attributes to be empty string, e.g. `<a download=""></a>`
......@@ -153,8 +153,9 @@ describe("HTML elements", () => {
},
],
rules: {
/* allow any style of boolean attributes, some tests runs all of them */
/* allow any style of boolean/empty attributes, some tests runs all of them */
"attribute-boolean-style": "off",
"attribute-empty-style": "off",
/* messes with tests validating that elements with support implicit close
* does so */
......
......@@ -4,6 +4,7 @@ module.exports = {
"attr-quotes": "error",
"attribute-allowed-values": "error",
"attribute-boolean-style": "error",
"attribute-empty-style": "error",
"close-attr": "error",
"close-order": "error",
deprecated: "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule attribute-empty-style should contain documentation 1`] = `
Object {
"description": "Require a specific style for attributes with empty values.",
"url": "https://html-validate.org/rules/attribute-empty-style.html",
}
`;
......@@ -13,6 +13,14 @@ describe("rule attribute-boolean-style", () => {
expect(report).toBeValid();
});
it("should not report for empty attributes", () => {
htmlvalidate = new HtmlValidate({
rules: { "attribute-boolean-style": ["error", { style: "omit" }] },
});
const report = htmlvalidate.validateString('<a download=""></a>');
expect(report).toBeValid();
});
describe('configured with "omit"', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
......
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule attribute-empty-style", () => {
let htmlvalidate: HtmlValidate;
it("should not report unknown elements", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": ["error", { style: "omit" }] },
});
const report = htmlvalidate.validateString(
'<custom-element foobar=""></custom-element>'
);
expect(report).toBeValid();
});
it("should not report unknown attributes", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": ["error", { style: "omit" }] },
});
const report = htmlvalidate.validateString('<a foobar=""></a>');
expect(report).toBeValid();
});
describe('configured with "omit"', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": ["error", { style: "omit" }] },
});
});
it("should report error when value is empty string", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<a download=""></a>');
expect(report).toBeInvalid();
expect(report).toHaveError(
"attribute-empty-style",
'Attribute "download" should omit value'
);
});
it("should not report error when value is omitted", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<a download></a>");
expect(report).toBeValid();
});
it("should not report error for non-empty attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a download="file.txt"></a>');
expect(report).toBeValid();
});
it("should not report error for boolean attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input required="">');
expect(report).toBeValid();
});
it("should not report error when attribute is interpolated", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a download="{{ dynamic }}">',
null,
{ processAttribute }
);
expect(report).toBeValid();
});
it("should not report error when attribute is dynamic", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<input dynamic-required="dynamic">',
null,
{ processAttribute }
);
expect(report).toBeValid();
});
});
describe('configured with "empty"', () => {
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": ["error", { style: "empty" }] },
});
});
it("should report error when value is omitted", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<a download></a>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"attribute-empty-style",
'Attribute "download" value should be empty string'
);
});
it("should not report error when value is empty string", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a download=""></a>');
expect(report).toBeValid();
});
it("should not report error for non-empty attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<a download="file.txt"></a>');
expect(report).toBeValid();
});
it("should not report error for boolean attributes", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<input required="">');
expect(report).toBeValid();
});
it("should not report error when attribute is interpolated", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<a download="{{ dynamic }}">',
null,
{ processAttribute }
);
expect(report).toBeValid();
});
it("should not report error when attribute is dynamic", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<input dynamic-required="dynamic">',
null,
{ processAttribute }
);
expect(report).toBeValid();
});
});
it("should throw error if configured with invalid value", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": ["error", { style: "foobar" }] },
});
expect(() => htmlvalidate.validateString("<foo></foo>")).toThrow(
`Invalid style "foobar" for "attribute-empty-style" rule`
);
});
it("should contain documentation", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
rules: { "attribute-empty-style": "error" },
});
expect(
htmlvalidate.getRuleDocumentation("attribute-empty-style")
).toMatchSnapshot();
});
});
import { Attribute, HtmlElement } from "../dom";
import { DOMReadyEvent } from "../event";
import { PermittedAttribute } from "../meta/element";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface Options {
style?: string;
}
const defaults: Options = {
style: "omit",
};
type checkFunction = (attr: Attribute) => boolean;
class AttributeEmptyStyle extends Rule<void, Options> {
private hasInvalidStyle: checkFunction;
public constructor(options: Options) {
super(Object.assign({}, defaults, options));
this.hasInvalidStyle = parseStyle(this.options.style);
}
public documentation(): RuleDocumentation {
return {
description: "Require a specific style for attributes with empty values.",
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("dom:ready", (event: DOMReadyEvent) => {
const doc = event.document;
doc.visitDepthFirst((node: HtmlElement) => {
const meta = node.meta;
/* ignore rule if element has no meta or meta does not specify attribute
* allowed values */
if (!meta || !meta.attributes) return;
/* check all boolean attributes */
for (const attr of node.attributes) {
/* only handle attributes which allows empty values */
if (!allowsEmpty(attr, meta.attributes)) {
continue;
}
/* skip attribute if the attribute is set to non-empty value
* (attribute-allowed-values deals with non-empty values)*/
if (!isEmptyValue(attr)) {
continue;
}
/* skip attribute if the style is valid */
if (!this.hasInvalidStyle(attr)) {
continue;
}
/* report error */
this.report(
node,
reportMessage(attr, this.options.style),
attr.keyLocation
);
}
});
});
}
}
function allowsEmpty(attr: Attribute, rules: PermittedAttribute): boolean {
return rules[attr.key] && rules[attr.key].includes("");
}
function isEmptyValue(attr: Attribute): boolean {
/* dynamic values are ignored, assumed to contain a value */
if (attr.isDynamic) {
return false;
}
return attr.value === null || attr.value === "";
}
function parseStyle(style: string): checkFunction {
switch (style.toLowerCase()) {
case "omit":
return (attr: Attribute) => attr.value !== null;
case "empty":
return (attr: Attribute) => attr.value !== "";
default:
throw new Error(
`Invalid style "${style}" for "attribute-empty-style" rule`
);
}
}
function reportMessage(attr: Attribute, style: string): string {
const key = attr.key;
switch (style.toLowerCase()) {
case "omit":
return `Attribute "${key}" should omit value`;
case "empty":
return `Attribute "${key}" value should be empty string`;
}
/* istanbul ignore next: the above switch should cover all cases */
return "";
}
module.exports = AttributeEmptyStyle;
......@@ -22,6 +22,7 @@
</button>
<!-- should allow preload attribute -->
<audio preload></audio>
<audio preload=""></audio>
<audio preload="none"></audio>
<audio preload="metadata"></audio>
......
<p contenteditable></p>
<p contenteditable=""></p>
<p contenteditable="true"></p>
<p contenteditable="false"></p>
......
......@@ -22,6 +22,7 @@
</button>
<!-- should allow preload attribute -->
<video preload></video>
<video preload=""></video>
<video preload="none"></video>
<video preload="metadata"></video>
......
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