Commit eb984615 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): new rule `attr-delimiter`

fixes #126
parent 0979798a
Pipeline #334938345 passed with stages
in 11 minutes and 17 seconds
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/attr-delimiter.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/attr-delimiter.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "inline",
"messages": Array [
Object {
"column": 12,
"context": undefined,
"line": 1,
"message": "Attribute value must not be delimited by whitespace",
"offset": 11,
"ruleId": "attr-delimiter",
"ruleUrl": "https://html-validate.org/rules/attr-delimiter.html",
"selector": null,
"severity": 2,
"size": 2,
},
Object {
"column": 12,
"context": undefined,
"line": 2,
"message": "Attribute value must not be delimited by whitespace",
"offset": 36,
"ruleId": "attr-delimiter",
"ruleUrl": "https://html-validate.org/rules/attr-delimiter.html",
"selector": null,
"severity": 2,
"size": 2,
},
],
"source": "<input name= \\"my-field\\">
<input name =\\"my-field\\">",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<input name= "my-field">
<input name ="my-field">`;
markup["correct"] = `<input name="my-field">`;
describe("docs/rules/attr-delimiter.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"attr-delimiter":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"attr-delimiter":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: attr-delimiter
summary: Disallow whitespace between attribute key and value
---
# Disallow whitespace between attribute key and value (`attr-delimiter`)
While technically allowed by the HTML5 specification this rule disallows the usage of whitespace separating the attribute key and value, i.e. before or after the `=` character.
Usage of whitespace in this context is typically a sign of typo or unintended behaviour.
For instance, consider the following markup:
<!-- prettier-ignore -->
```html
<input name= disabled>
```
As the HTML5 specification allows whitespace after `=` this is to be interpreted as `<input name="disabled">` which is has a completely different meaning than the developer probably intended.
This could have been generated from a templating langage where the value supposted to be bound to `name` was not set:
<!-- prettier-ignore -->
```html
<input name=<%= fieldName %> disabled>
```
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="attr-delimiter">
<input name= "my-field">
<input name ="my-field">
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="attr-delimiter">
<input name="my-field">
</validate>
......@@ -4,6 +4,7 @@ const config: ConfigData = {
rules: {
"aria-label-misuse": "error",
"attr-case": "error",
"attr-delimiter": "error",
"attr-quotes": "error",
"attr-spacing": "error",
"attribute-allowed-values": "error",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule attr-delimiter should contain documentation 1`] = `
Object {
"description": "Attribute value should be separated by ",
"url": "https://html-validate.org/rules/attr-delimiter.html",
}
`;
import HtmlValidate from "../htmlvalidate";
import "../matchers";
describe("rule attr-delimiter", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "attr-delimiter": ["error"] },
});
});
it("should not report when attributes have proper delimiter", () => {
expect.assertions(1);
const markup = `<i foo="1" bar='2'></i>`;
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should handle boolean attributes", () => {
expect.assertions(1);
const markup = "<i foo bar></i>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should report error when equal sign has leading whitespace", () => {
expect.assertions(2);
const markup = '<i foo ="1"></i>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-delimiter",
"Attribute value must not be delimited by whitespace"
);
});
it("should report error when equal sign has trailing whitespace", () => {
expect.assertions(2);
const markup = '<i foo= "1"></i>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-delimiter",
"Attribute value must not be delimited by whitespace"
);
});
it("should handle single quote", () => {
expect.assertions(2);
const markup = "<i foo = '1'></i>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-delimiter",
"Attribute value must not be delimited by whitespace"
);
});
it("should handle unquoted value", () => {
expect.assertions(2);
const markup = "<i foo = bar></i>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"attr-delimiter",
"Attribute value must not be delimited by whitespace"
);
});
it("should handle spaces in value", () => {
expect.assertions(1);
const markup = '<i foo=" foo bar "></i>';
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should contain documentation", () => {
expect.assertions(1);
expect(htmlvalidate.getRuleDocumentation("attr-delimiter")).toMatchSnapshot();
});
});
import { sliceLocation } from "../context";
import { TokenEvent } from "../event";
import { TokenType } from "../lexer";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
const whitespace = /(\s+)/;
function isRelevant(event: TokenEvent): boolean {
return event.type === TokenType.ATTR_VALUE;
}
export default class AttrDelimiter extends Rule {
public documentation(): RuleDocumentation {
return {
description: `Attribute value should be separated by `,
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("token", isRelevant, (event: TokenEvent) => {
const delimiter = event.data[1];
const match = whitespace.exec(delimiter);
if (match) {
const location = sliceLocation(event.location, 0, delimiter.length);
this.report(null, "Attribute value must not be delimited by whitespace", location);
}
});
}
}
......@@ -2,6 +2,7 @@ import { RuleConstructor } from "../rule";
import AllowedLinks from "./allowed-links";
import AriaLabelMisuse from "./aria-label-misuse";
import AttrCase from "./attr-case";
import AttrDelimiter from "./attr-delimiter";
import AttrPattern from "./attr-pattern";
import AttrQuotes from "./attr-quotes";
import AttrSpacing from "./attr-spacing";
......@@ -68,6 +69,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"allowed-links": AllowedLinks,
"aria-label-misuse": AriaLabelMisuse,
"attr-case": AttrCase,
"attr-delimiter": AttrDelimiter,
"attr-pattern": AttrPattern,
"attr-quotes": AttrQuotes,
"attr-spacing": AttrSpacing,
......
Supports Markdown
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