Commit 288cf867 authored by David Sveningsson's avatar David Sveningsson

feat(rule): validate matching case for start and end tags

parent 5a397bd9
Pipeline #106703008 passed with stages
in 9 minutes and 5 seconds
......@@ -25,6 +25,29 @@ Array [
]
`;
exports[`docs/rules/element-case.md inline validation: matching 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 13,
"context": undefined,
"line": 1,
"message": "Start and end tag must not differ in casing",
"offset": 12,
"ruleId": "element-case",
"severity": 2,
"size": 7,
},
],
"source": "<FooBar>...</Foobar>",
"warningCount": 0,
},
]
`;
exports[`docs/rules/element-case.md inline validation: multiple 1`] = `
Array [
Object {
......
......@@ -3,6 +3,7 @@ import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<DIV>...</DIV>`;
markup["correct"] = `<div>...</div>`;
markup["matching"] = `<FooBar>...</Foobar>`;
markup["multiple"] = `<foo-bar></foo-bar>
<FooBar></FooBar>
<fooBar></fooBar>`;
......@@ -18,6 +19,11 @@ describe("docs/rules/element-case.md", () => {
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: matching", () => {
const htmlvalidate = new HtmlValidate({"rules":{"element-case":["error",{"style":"pascalcase"}]}});
const report = htmlvalidate.validateString(markup["matching"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: multiple", () => {
const htmlvalidate = new HtmlValidate({"rules":{"element-case":["error",{"style":["lowercase","pascalcase"]}]}});
const report = htmlvalidate.validateString(markup["multiple"]);
......
......@@ -23,6 +23,14 @@ Examples of **correct** code for this rule:
<div>...</div>
</validate>
### Matching case
When using styles such as `pascalcase` the start and end tag must have matching case:
<validate name="matching" rules="element-case" element-case='{"style": "pascalcase"}'>
<FooBar>...</Foobar>
</validate>
## Options
This rule takes an optional object:
......
......@@ -158,6 +158,27 @@ describe("rule element-case", () => {
);
});
it("should report error if start and close tag have different case", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
rules: { "element-case": ["error", { style: "camelcase" }] },
});
const report = htmlvalidate.validateString("<foo-Bar></foo-bar>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-case",
"Start and end tag must not differ in casing"
);
});
it("should not report error when elements are closed out-of-order", () => {
expect.assertions(4);
expect(htmlvalidate.validateString("<p></i>")).toBeValid();
expect(htmlvalidate.validateString("<p>")).toBeValid();
expect(htmlvalidate.validateString("</i>")).toBeValid();
expect(htmlvalidate.validateString("<input></input>")).toBeValid();
});
it("should contain documentation", () => {
htmlvalidate = new HtmlValidate({
rules: { "element-case": "error" },
......
import { sliceLocation } from "../context";
import { TagOpenEvent } from "../event";
import { Location, sliceLocation } from "../context";
import { HtmlElement } from "../dom";
import { TagCloseEvent, TagOpenEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
import { CaseStyle } from "./helper/case-style";
......@@ -24,16 +25,47 @@ class ElementCase extends Rule {
public setup(): void {
this.on("tag:open", (event: TagOpenEvent) => {
const letters = event.target.tagName.replace(/[^a-z]+/gi, "");
if (!this.style.match(letters)) {
const location = sliceLocation(event.location, 1);
this.report(
event.target,
`Element "${event.target.tagName}" should be ${this.style.name}`,
location
);
}
const { target, location } = event;
this.validateCase(target, location);
});
this.on("tag:close", (event: TagCloseEvent) => {
const { target, previous } = event;
this.validateMatchingCase(previous, target);
});
}
private validateCase(target: HtmlElement, targetLocation: Location): void {
const letters = target.tagName.replace(/[^a-z]+/gi, "");
if (!this.style.match(letters)) {
const location = sliceLocation(targetLocation, 1);
this.report(
target,
`Element "${target.tagName}" should be ${this.style.name}`,
location
);
}
}
private validateMatchingCase(start: HtmlElement, end: HtmlElement): void {
/* handle when elements have have missing start or end tag */
if (!start || !end || !start.tagName || !end.tagName) {
return;
}
/* only check case if the names are a lowercase match to each other or it
* will yield false positives when elements are closed in wrong order or
* otherwise mismatched */
if (start.tagName.toLowerCase() !== end.tagName.toLowerCase()) {
return;
}
if (start.tagName !== end.tagName) {
this.report(
start,
"Start and end tag must not differ in casing",
end.location
);
}
}
}
......
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