Commit 75aa5f0f authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(rules): new option `allowedProperties` to `no-inline-style` (defaults to `display`)

parent 96f9f9ff
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-inline-style.md inline validation: allowed-properties 1`] = `Array []`;
exports[`docs/rules/no-inline-style.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-inline-style.md inline validation: incorrect 1`] = `
......
......@@ -3,6 +3,7 @@ import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<p style="color: red"></p>`;
markup["correct"] = `<p class="error"></p>`;
markup["allowed-properties"] = `<p style="display: none"></p>`;
describe("docs/rules/no-inline-style.md", () => {
it("inline validation: incorrect", () => {
......@@ -17,4 +18,10 @@ describe("docs/rules/no-inline-style.md", () => {
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: allowed-properties", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"no-inline-style":"error"}});
const report = htmlvalidate.validateString(markup["allowed-properties"]);
expect(report.results).toMatchSnapshot();
});
});
......@@ -7,8 +7,8 @@ summary: Disallow inline style
# Disallow inline style (`no-inline-style`)
Inline style is a sign of unstructured CSS. Use class or ID with a separate
stylesheet.
Inline style is a sign of unstructured CSS.
Use class or ID with a separate stylesheet.
## Rule details
......@@ -28,10 +28,11 @@ Examples of **correct** code for this rule:
This rule takes an optional object:
```javascript
```json
{
"include": []
"exclude": []
"include": [],
"exclude": [],
"allowedProperties": ["display"]
}
```
......@@ -51,3 +52,14 @@ If set only attributes listed in this array generates errors.
### `exclude`
If set attributes listed in this array is ignored.
### `allowedProperties`
List of CSS properties to ignore.
If the `style` attribute contains only the properties listed no error will be yielded.
By default `display` is allowed.
<validate name="allowed-properties" rules="no-inline-style">
<p style="display: none"></p>
</validate>
......@@ -2,7 +2,13 @@
exports[`rule no-inline-style should contain documentation 1`] = `
Object {
"description": "Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.",
"description": "Inline style is not allowed.
Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.
Under the current configuration the following CSS properties are allowed:
- \`display\`",
"url": "https://html-validate.org/rules/no-inline-style.html",
}
`;
......
......@@ -13,9 +13,15 @@ describe("rule no-inline-style", () => {
});
});
it("should not report when style attribute sets display property", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<p style="display: none"></p>');
expect(report).toBeValid();
});
it("should report when style attribute is used", () => {
expect.assertions(2);
const report = htmlvalidate.validateString('<p style=""></p>');
const report = htmlvalidate.validateString('<p style="color: red; background: green"></p>');
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
......@@ -78,6 +84,83 @@ describe("rule no-inline-style", () => {
});
});
describe("allowedProperties", () => {
it("should not report when style attribute contains only allowed properties", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: ["color", "background"] }] },
});
const report = htmlvalidate.validateString('<p style="color: red; background: green;"></p>');
expect(report).toBeValid();
});
it("should report when one or more properties are now allowed", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: ["color"] }] },
});
const report = htmlvalidate.validateString('<p style="color: red; background: green;"></p>');
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
it("should handle when allowed properties is empty", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: [] }] },
});
const report = htmlvalidate.validateString('<p style="color: red; background: green;"></p>');
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
it("should handle missing value", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: ["color"] }] },
});
const report = htmlvalidate.validateString("<p style></p>");
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
it("should handle empty value", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: ["color"] }] },
});
const report = htmlvalidate.validateString('<p style=""></p>');
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
it("should handle malformed declaration", () => {
expect.assertions(2);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: [] }] },
});
const report = htmlvalidate.validateString('<p style="color"></p>');
expect(report).toBeInvalid();
expect(report).toHaveError("no-inline-style", "Inline style is not allowed");
});
it("should handle trailing semicolon", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
root: true,
rules: { "no-inline-style": ["error", { allowedProperties: ["color"] }] },
});
const report = htmlvalidate.validateString('<p style="color: red;"></p>');
expect(report).toBeValid();
});
});
it("smoketest", () => {
expect.assertions(1);
htmlvalidate = new HtmlValidate({
......
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl, SchemaObject } from "../rule";
interface CSSDeclaration {
property: string;
value: string;
}
export interface RuleOptions {
include: string[] | null;
exclude: string[] | null;
allowedProperties: string[];
}
const defaults: RuleOptions = {
include: null,
exclude: null,
allowedProperties: ["display"],
};
function getCSSDeclarations(value: string): CSSDeclaration[] {
return value
.split(";")
.filter(Boolean)
.map(
(it): CSSDeclaration => {
const [property, value] = it.split(":", 2);
return { property: property.trim(), value: value.trim() };
}
);
}
export default class NoInlineStyle extends Rule<void, RuleOptions> {
public constructor(options: Partial<RuleOptions>) {
super({ ...defaults, ...options });
......@@ -44,23 +64,44 @@ export default class NoInlineStyle extends Rule<void, RuleOptions> {
},
],
},
allowedProperties: {
items: {
type: "string",
},
type: "array",
},
};
}
public documentation(): RuleDocumentation {
const text = [
"Inline style is not allowed.\n",
"Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.\n",
];
if (this.options.allowedProperties.length > 0) {
text.push("Under the current configuration the following CSS properties are allowed:\n");
text.push(this.options.allowedProperties.map((it) => `- \`${it}\``).join("\n"));
}
return {
description:
"Inline style is a sign of unstructured CSS. Use class or ID with a separate stylesheet.",
description: text.join("\n"),
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
if (this.isRelevant(event)) {
this.on(
"attr",
(event: AttributeEvent) => this.isRelevant(event),
(event: AttributeEvent) => {
const { value } = event;
if (this.allPropertiesAllowed(value)) {
return;
}
this.report(event.target, "Inline style is not allowed");
}
});
);
}
private isRelevant(event: AttributeEvent): boolean {
......@@ -83,4 +124,25 @@ export default class NoInlineStyle extends Rule<void, RuleOptions> {
return true;
}
private allPropertiesAllowed(value: string | DynamicValue | null): boolean {
if (typeof value !== "string") {
return false;
}
const allowProperties = this.options.allowedProperties;
/* quick path: no properties are allowed, no need to check each one individually */
if (allowProperties.length === 0) {
return false;
}
const declarations = getCSSDeclarations(value);
return (
declarations.length > 0 &&
declarations.every((it) => {
return allowProperties.includes(it.property);
})
);
}
}
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