Commit 5a397bd9 authored by David Sveningsson's avatar David Sveningsson

feat(rules): support multiple case styles

fixes #50
parent 24d8fad1
......@@ -25,4 +25,29 @@ Array [
]
`;
exports[`docs/rules/attr-case.md inline validation: multiple 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 4,
"context": undefined,
"line": 3,
"message": "Attribute \\"fooBar\\" should be lowercase or uppercase",
"offset": 33,
"ruleId": "attr-case",
"severity": 2,
"size": 6,
},
],
"source": "<p foobar></p>
<p FOOBAR></p>
<p fooBar></p>",
"warningCount": 0,
},
]
`;
exports[`docs/rules/attr-case.md inline validation: svg-viewbox 1`] = `Array []`;
......@@ -24,3 +24,28 @@ Array [
},
]
`;
exports[`docs/rules/element-case.md inline validation: multiple 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 2,
"context": undefined,
"line": 3,
"message": "Element \\"fooBar\\" should be lowercase or PascalCase",
"offset": 39,
"ruleId": "element-case",
"severity": 2,
"size": 6,
},
],
"source": "<foo-bar></foo-bar>
<FooBar></FooBar>
<fooBar></fooBar>",
"warningCount": 0,
},
]
`;
......@@ -3,6 +3,9 @@ import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<p ID="foo"></p>`;
markup["correct"] = `<p id="foo"></p>`;
markup["multiple"] = `<p foobar></p>
<p FOOBAR></p>
<p fooBar></p>`;
markup["svg-viewbox"] = `<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" />`;
describe("docs/rules/attr-case.md", () => {
......@@ -16,6 +19,11 @@ describe("docs/rules/attr-case.md", () => {
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: multiple", () => {
const htmlvalidate = new HtmlValidate({"rules":{"attr-case":["error",{"style":["lowercase","uppercase"]}]}});
const report = htmlvalidate.validateString(markup["multiple"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: svg-viewbox", () => {
const htmlvalidate = new HtmlValidate({"rules":{"attr-case":"error"}});
const report = htmlvalidate.validateString(markup["svg-viewbox"]);
......
......@@ -3,6 +3,9 @@ import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<DIV>...</DIV>`;
markup["correct"] = `<div>...</div>`;
markup["multiple"] = `<foo-bar></foo-bar>
<FooBar></FooBar>
<fooBar></fooBar>`;
describe("docs/rules/element-case.md", () => {
it("inline validation: incorrect", () => {
......@@ -15,4 +18,9 @@ describe("docs/rules/element-case.md", () => {
const report = htmlvalidate.validateString(markup["correct"]);
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"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -41,6 +41,17 @@ This rule takes an optional object:
- `pascalcase` requires all attribute names to be PascalCase.
- `uppercase` requires all attribute names to be UPPERCASE.
Multiple styles can be set as an array of strings.
With multiple styles the attribute must match at least one pattern to be considered valid.
For instance, when configured with `{"style": ["lowercase", "uppercase"]}` attributes can be either lowercase or uppercase:
<validate name="multiple" rules="attr-case" attr-case='{"style": ["lowercase", "uppercase"]}'>
<p foobar></p>
<p FOOBAR></p>
<p fooBar></p>
</validate>
### `ignoreForeign`
By default attributes on foreign elements (such as `<svg>` and `<math>`) are
......
......@@ -39,3 +39,14 @@ This rule takes an optional object:
- `lowercase` requires all element names to be lowercase.
- `pascalcase` requires all element names to be PascalCase.
- `uppercase` requires all element names to be UPPERCASE.
Multiple styles can be set as an array of strings.
With multiple styles the element name must match at least one pattern to be considered valid.
For instance, when configured with `{"style": ["lowercase", "pascalcase"]}` element names can be either lowercase or PascalCase:
<validate name="multiple" rules="element-case" element-case='{"style": ["lowercase", "pascalcase"]}'>
<foo-bar></foo-bar>
<FooBar></FooBar>
<fooBar></fooBar>
</validate>
......@@ -207,6 +207,21 @@ describe("rule attr-case", () => {
});
});
it("should handle multiple styles", () => {
expect.assertions(3);
htmlvalidate = new HtmlValidate({
rules: {
"attr-case": ["error", { style: ["lowercase", "camelcase"] }],
},
});
expect(htmlvalidate.validateString("<div foo-bar></div>")).toBeValid();
expect(htmlvalidate.validateString("<div fooBar></div>")).toBeValid();
expect(htmlvalidate.validateString("<div FooBar></div>")).toHaveError(
"attr-case",
'Attribute "FooBar" should be lowercase or camelCase'
);
});
it("should not report duplicate errors for dynamic attributes", () => {
htmlvalidate = new HtmlValidate({
rules: { "attr-case": "error" },
......@@ -232,7 +247,7 @@ describe("rule attr-case", () => {
rules: { "attr-case": ["error", { style: "foobar" }] },
});
expect(() => htmlvalidate.validateString("<foo></foo>")).toThrow(
`Invalid style "foobar" for "attr-case" rule`
`Invalid style "foobar" for attr-case rule`
);
});
......
......@@ -134,12 +134,27 @@ describe("rule element-case", () => {
});
});
it("should handle multiple styles", () => {
expect.assertions(3);
htmlvalidate = new HtmlValidate({
rules: {
"element-case": ["error", { style: ["lowercase", "pascalcase"] }],
},
});
expect(htmlvalidate.validateString("<foo-bar></foo-bar>")).toBeValid();
expect(htmlvalidate.validateString("<FooBar></FooBar>")).toBeValid();
expect(htmlvalidate.validateString("<fooBar></fooBar>")).toHaveError(
"element-case",
'Element "fooBar" should be lowercase or PascalCase'
);
});
it("should throw error if configured with invalid value", () => {
htmlvalidate = new HtmlValidate({
rules: { "element-case": ["error", { style: "foobar" }] },
});
expect(() => htmlvalidate.validateString("<foo></foo>")).toThrow(
`Invalid style "foobar" for "element-case" rule`
`Invalid style "foobar" for element-case rule`
);
});
......
......@@ -28,9 +28,47 @@ it.each`
expect(cs.match(text)).toBeFalsy();
});
it("should handle multiple patterns", () => {
expect.assertions(3);
const cs = new CaseStyle(["uppercase", "lowercase"], "test-case");
expect(cs.match("FOO")).toBeTruthy();
expect(cs.match("bar")).toBeTruthy();
expect(cs.match("FooBar")).toBeFalsy();
});
it("should throw exception for unknown styles", () => {
expect.assertions(1);
expect(() => {
return new CaseStyle("unknown-style", "test-case");
}).toThrow('Invalid style "unknown-style" for "test-case" rule');
}).toThrow('Invalid style "unknown-style" for test-case rule');
});
it("should throw exception if no styles are set", () => {
expect.assertions(1);
expect(() => {
return new CaseStyle([], "test-case");
}).toThrow("Missing style for test-case rule");
});
describe("name", () => {
it("single name should be presented as-is", () => {
expect.assertions(1);
const cs = new CaseStyle("uppercase", "test-case");
expect(cs.name).toEqual("uppercase");
});
it('two names should be joined by "or"', () => {
expect.assertions(1);
const cs = new CaseStyle(["uppercase", "lowercase"], "test-case");
expect(cs.name).toEqual("uppercase or lowercase");
});
it("more than two names should be joined by comma followed by or", () => {
expect.assertions(1);
const cs = new CaseStyle(
["lowercase", "pascalcase", "camelcase"],
"test-case"
);
expect(cs.name).toEqual("lowercase, PascalCase or camelCase");
});
});
import { ConfigError } from "../../config/error";
interface Style {
pattern: RegExp;
name: string;
}
/**
* Represents casing for a name, e.g. lowercase, uppercase, etc.
*/
export class CaseStyle {
public name: string;
private pattern: RegExp;
private styles: Style[];
/**
* @param style - Name of a valid case style.
*/
public constructor(style: string, ruleId: string) {
[this.pattern, this.name] = this.parseStyle(style, ruleId);
public constructor(style: string | string[], ruleId: string) {
if (!Array.isArray(style)) {
style = [style];
}
if (style.length === 0) {
throw new ConfigError(`Missing style for ${ruleId} rule`);
}
this.styles = this.parseStyle(style, ruleId);
}
/**
* Test if a text matches this case style.
*/
public match(text: string): boolean {
return !!text.match(this.pattern);
return this.styles.some(style => text.match(style.pattern));
}
private parseStyle(style: string, ruleId: string): [RegExp, string] {
switch (style.toLowerCase()) {
case "lowercase":
return [/^[a-z]*$/, "lowercase"];
case "uppercase":
return [/^[A-Z]*$/, "uppercase"];
case "pascalcase":
return [/^[A-Z][A-Za-z]*$/, "PascalCase"];
case "camelcase":
return [/^[a-z][A-Za-z]*$/, "camelCase"];
default:
throw new Error(`Invalid style "${style}" for "${ruleId}" rule`);
public get name(): string {
const names = this.styles.map(style => style.name);
switch (this.styles.length) {
case 1:
return names[0];
case 2:
return names.join(" or ");
default: {
const last = names.slice(-1);
const rest = names.slice(0, -1);
return `${rest.join(", ")} or ${last}`;
}
}
}
private parseStyle(style: string[], ruleId: string): Style[] {
return style.map(
(cur: string): Style => {
switch (cur.toLowerCase()) {
case "lowercase":
return { pattern: /^[a-z]*$/, name: "lowercase" };
case "uppercase":
return { pattern: /^[A-Z]*$/, name: "uppercase" };
case "pascalcase":
return { pattern: /^[A-Z][A-Za-z]*$/, name: "PascalCase" };
case "camelcase":
return { pattern: /^[a-z][A-Za-z]*$/, name: "camelCase" };
default:
throw new ConfigError(
`Invalid style "${style}" for ${ruleId} rule`
);
}
}
);
}
}
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