Commits (38)
......@@ -68,7 +68,7 @@ Jest:
reports:
junit: temp/jest.xml
script:
- npm test -- src elements tests
- npm test -- src/ elements/ tests/
Prettier:
stage: test
......
# html-validate changelog
# [2.22.0](https://gitlab.com/html-validate/html-validate/compare/v2.21.0...v2.22.0) (2020-05-15)
### Bug Fixes
- **elements:** add `<details>` and `<summary>` elements ([47ba673](https://gitlab.com/html-validate/html-validate/commit/47ba6739951a37bdb285400d392ff27ec57ff89e)), closes [#89](https://gitlab.com/html-validate/html-validate/issues/89)
- `<legend>` should allow heading elements ([73e150f](https://gitlab.com/html-validate/html-validate/commit/73e150f13a8b797458dac4fcbe3a22997422f4d9))
- **deps:** update dependency json-merge-patch to v1 ([e9f83d2](https://gitlab.com/html-validate/html-validate/commit/e9f83d2047aed16e81fe006795c9b30111478534))
### Features
- **rules:** new rule `no-autoplay` ([9ed5474](https://gitlab.com/html-validate/html-validate/commit/9ed5474493eedebd2db5c673060538d244b69f63)), closes [#84](https://gitlab.com/html-validate/html-validate/issues/84)
# [2.21.0](https://gitlab.com/html-validate/html-validate/compare/v2.20.1...v2.21.0) (2020-04-26)
### Bug Fixes
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`docs/rules/no-autoplay.md inline validation: correct 1`] = `Array []`;
exports[`docs/rules/no-autoplay.md inline validation: incorrect 1`] = `
Array [
Object {
"errorCount": 1,
"filePath": "inline",
"messages": Array [
Object {
"column": 8,
"context": Object {
"tagName": "video",
},
"line": 1,
"message": "The autoplay attribute is not allowed on <video>",
"offset": 7,
"ruleId": "no-autoplay",
"selector": "video",
"severity": 2,
"size": 8,
},
],
"source": "<video autoplay></video>",
"warningCount": 0,
},
]
`;
import HtmlValidate from "../../../src/htmlvalidate";
const markup: { [key: string]: string } = {};
markup["incorrect"] = `<video autoplay></video>`;
markup["correct"] = `<video></video>`;
describe("docs/rules/no-autoplay.md", () => {
it("inline validation: incorrect", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"no-autoplay":"error"}});
const report = htmlvalidate.validateString(markup["incorrect"]);
expect(report.results).toMatchSnapshot();
});
it("inline validation: correct", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({"rules":{"no-autoplay":"error"}});
const report = htmlvalidate.validateString(markup["correct"]);
expect(report.results).toMatchSnapshot();
});
});
---
docType: rule
name: no-autoplay
category: a17y
summary: Disallow autoplaying media elements
---
# Disallow autoplaying media elements (`no-autoplay`)
Autoplaying content can be disruptive for users and has accessibility issues.
This rule disallows `<audio>` and `<video>` with autoplay enabled.
Unless the user is expecting media to play automatically it is better to let the user control playback.
The media might be too loud or the user might be in a location where audio is discouraged.
Users with assistive technology might find it hard to pause as they must first navigate to the controls.
Media can be distracting for users with cognitive or concentration issues and if the video contains flashing or blinking sequences it can cause epilepsy.
There are also issues where some browsers use heurestics to prevent autoplaying so results may vary when used.
## Rule details
Examples of **incorrect** code for this rule:
<validate name="incorrect" rules="no-autoplay">
<video autoplay></video>
</validate>
Examples of **correct** code for this rule:
<validate name="correct" rules="no-autoplay">
<video></video>
</validate>
## Options
This rule takes an optional object:
```javascript
{
"include": ["audio", "video"],
"exclude": [],
}
```
### `include`
If set only elements listed in this array generates errors.
### `exclude`
If set elements listed in this array is ignored.
......@@ -1432,6 +1432,75 @@ Array [
exports[`HTML elements <del> valid markup 1`] = `Array []`;
exports[`HTML elements <details> invalid markup 1`] = `
Array [
Object {
"errorCount": 3,
"filePath": "test-files/elements/details-invalid.html",
"messages": Array [
Object {
"column": 3,
"context": undefined,
"line": 4,
"message": "Element <summary> must be used before <div> in this context",
"offset": 62,
"ruleId": "element-permitted-order",
"selector": "details:nth-child(1) > summary",
"severity": 2,
"size": 7,
},
Object {
"column": 2,
"context": Object {
"missing": "summary",
"node": "details",
},
"line": 8,
"message": "<details> element must have <summary> as content",
"offset": 123,
"ruleId": "element-required-content",
"selector": "details:nth-child(2)",
"severity": 2,
"size": 7,
},
Object {
"column": 16,
"context": Object {
"allowed": Array [],
"attribute": "open",
"element": "details",
"value": "foobar",
},
"line": 11,
"message": "Attribute \\"open\\" has invalid value \\"foobar\\"",
"offset": 193,
"ruleId": "attribute-allowed-values",
"selector": "details:nth-child(3)",
"severity": 2,
"size": 6,
},
],
"source": "<!-- summary must go before flow -->
<details>
<div></div>
<summary></summary>
</details>
<!-- summary is required -->
<details></details>
<!-- open attribute is boolean -->
<details open=\\"foobar\\">
<summary></summary>
</details>
",
"warningCount": 0,
},
]
`;
exports[`HTML elements <details> valid markup 1`] = `Array []`;
exports[`HTML elements <dfn> invalid markup 1`] = `
Array [
Object {
......@@ -5368,6 +5437,54 @@ Array [
exports[`HTML elements <sub> valid markup 1`] = `Array []`;
exports[`HTML elements <summary> invalid markup 1`] = `
Array [
Object {
"errorCount": 2,
"filePath": "test-files/elements/summary-invalid.html",
"messages": Array [
Object {
"column": 3,
"context": undefined,
"line": 3,
"message": "Element <summary> is not permitted as content in <div>",
"offset": 58,
"ruleId": "element-permitted-content",
"selector": "div > summary",
"severity": 2,
"size": 7,
},
Object {
"column": 4,
"context": undefined,
"line": 9,
"message": "Element <p> is not permitted as content in <summary>",
"offset": 159,
"ruleId": "element-permitted-content",
"selector": "details > summary > p",
"severity": 2,
"size": 1,
},
],
"source": "<!-- should not allow under arbitrary content -->
<div>
<summary>lorem ipsum</summary>
</div>
<!-- should not allow flow content -->
<details>
<summary>
<p>lorem ipsum</p>
</summary>
</details>
",
"warningCount": 0,
},
]
`;
exports[`HTML elements <summary> valid markup 1`] = `Array []`;
exports[`HTML elements <sup> invalid markup 1`] = `
Array [
Object {
......
......@@ -272,6 +272,18 @@
"transparent": true
},
"details": {
"flow": true,
"sectioning": true,
"interactive": true,
"attributes": {
"open": []
},
"permittedContent": ["summary", "@flow"],
"permittedOrder": ["summary", "@flow"],
"requiredContent": ["summary"]
},
"dfn": {
"flow": true,
"phrasing": true,
......@@ -614,7 +626,7 @@
"legend": {
"deprecatedAttributes": ["align", "datasrc", "datafld", "dataformatas"],
"permittedContent": ["@phrasing"]
"permittedContent": ["@phrasing", "@heading"]
},
"li": {
......@@ -1014,6 +1026,10 @@
"permittedContent": ["@phrasing"]
},
"summary": {
"permittedContent": ["@phrasing", "@heading"]
},
"sup": {
"flow": true,
"phrasing": true,
......
......@@ -35,6 +35,7 @@ const tagNames = [
"datalist",
"dd",
"del",
"details",
"dfn",
"dir",
"div",
......@@ -119,6 +120,7 @@ const tagNames = [
"strong",
"style",
"sub",
"summary",
"sup",
"svg",
"table",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "html-validate",
"version": "2.21.0",
"version": "2.22.0",
"description": "html linter",
"keywords": [
"html",
......@@ -88,23 +88,23 @@
"espree": "^6.0.0",
"glob": "^7.1.3",
"inquirer": "^7.0.0",
"json-merge-patch": "^0.2.3",
"json-merge-patch": "^1.0.0",
"minimist": "^1.2.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.5",
"@babel/core": "7.9.6",
"@babel/preset-env": "7.9.6",
"@commitlint/cli": "8.3.5",
"@html-validate/commitlint-config": "1.0.3",
"@html-validate/eslint-config": "1.3.1",
"@html-validate/jest-config": "1.0.4",
"@html-validate/eslint-config": "1.5.0",
"@html-validate/jest-config": "1.0.7",
"@html-validate/prettier-config": "1.0.1",
"@html-validate/semantic-release-config": "1.0.17",
"@html-validate/semantic-release-config": "1.0.20",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.44",
"@types/glob": "7.1.1",
"@types/inquirer": "6.5.0",
"@types/jest": "25.2.1",
"@types/jest": "25.2.2",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.15.12",
......@@ -121,7 +121,7 @@
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"font-awesome": "4.7.0",
"front-matter": "3.1.0",
"front-matter": "3.2.1",
"grunt": "1.1.0",
"grunt-browserify": "5.3.0",
"grunt-cli": "1.3.2",
......@@ -129,14 +129,14 @@
"grunt-contrib-copy": "1.0.0",
"grunt-postcss": "0.9.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.0.0",
"highlight.js": "10.0.3",
"husky": "4.2.5",
"jest": "25.4.0",
"jest-diff": "25.4.0",
"jquery": "3.5.0",
"lint-staged": "10.1.7",
"jest": "25.5.4",
"jest-diff": "25.5.0",
"jquery": "3.5.1",
"lint-staged": "10.2.2",
"load-grunt-tasks": "5.1.0",
"marked": "0.8.2",
"marked": "1.0.0",
"minimatch": "3.0.4",
"prettier": "2.0.5",
"sass": "1.26.5",
......@@ -144,8 +144,8 @@
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "25.4.0",
"typescript": "3.8.3"
"ts-jest": "25.5.1",
"typescript": "3.9.2"
},
"jest": {
"preset": "@html-validate/jest-config",
......
......@@ -21,6 +21,7 @@ module.exports = {
"empty-title": "error",
"long-title": "error",
"meta-refresh": "error",
"no-autoplay": ["error", { include: ["audio", "video"] }],
"no-conditional-comment": "error",
"no-deprecated-attr": "error",
"no-dup-attr": "error",
......
......@@ -12,7 +12,7 @@ class ConfigMock extends Config {
describe("Meta validator", () => {
describe("validatePermitted()", () => {
it("should handle undefined", () => {
it("should handle null", () => {
expect.assertions(1);
const table = new MetaTable();
table.loadFromObject({
......@@ -20,7 +20,7 @@ describe("Meta validator", () => {
});
const parser = new Parser(new ConfigMock(table));
const [foo] = parser.parseHtml("<foo/>").root.childElements;
expect(Validator.validatePermitted(foo, undefined)).toBeTruthy();
expect(Validator.validatePermitted(foo, null)).toBeTruthy();
});
it("should validate tagName", () => {
......@@ -374,7 +374,7 @@ describe("Meta validator", () => {
});
describe("validateOccurrences()", () => {
it("should handle undefined", () => {
it("should handle null", () => {
expect.assertions(1);
const table = new MetaTable();
table.loadFromObject({
......@@ -382,7 +382,7 @@ describe("Meta validator", () => {
});
const parser = new Parser(new ConfigMock(table));
const [foo] = parser.parseHtml("<foo/>").root.childElements;
expect(Validator.validateOccurrences(foo, undefined, 1)).toBeTruthy();
expect(Validator.validateOccurrences(foo, null, 1)).toBeTruthy();
});
it("should support missing qualifier", () => {
......@@ -441,10 +441,10 @@ describe("Meta validator", () => {
cb = jest.fn();
});
it("should handle undefined rules", () => {
it("should handle null rules", () => {
expect.assertions(2);
const children = parser.parseHtml("<foo/>").root.childElements;
expect(Validator.validateOrder(children, undefined, cb)).toBeTruthy();
expect(Validator.validateOrder(children, null, cb)).toBeTruthy();
expect(cb).not.toHaveBeenCalled();
});
......@@ -498,7 +498,7 @@ describe("Meta validator", () => {
it("should match if no rule is present", () => {
expect.assertions(2);
const node = root.querySelector("dd");
expect(Validator.validateAncestors(node, undefined)).toBeTruthy();
expect(Validator.validateAncestors(node, null)).toBeTruthy();
expect(Validator.validateAncestors(node, [])).toBeTruthy();
});
......@@ -541,7 +541,7 @@ describe("Meta validator", () => {
it("should match if no rule is present", () => {
expect.assertions(2);
const node = parser.parseHtml("<div></div>").querySelector("div");
expect(Validator.validateRequiredContent(node, undefined)).toEqual([]);
expect(Validator.validateRequiredContent(node, null)).toEqual([]);
expect(Validator.validateRequiredContent(node, [])).toEqual([]);
});
......
......@@ -24,7 +24,7 @@ export class Validator {
*/
public static validatePermitted(
node: HtmlElement,
rules: Permitted
rules: Permitted | null
): boolean {
if (!rules) {
return true;
......@@ -48,7 +48,7 @@ export class Validator {
*/
public static validateOccurrences(
node: HtmlElement,
rules: Permitted,
rules: Permitted | null,
numSiblings: number
): boolean {
if (!rules) {
......@@ -82,7 +82,7 @@ export class Validator {
*/
public static validateOrder(
children: HtmlElement[],
rules: PermittedOrder,
rules: PermittedOrder | null,
cb: (node: HtmlElement, prev: HtmlElement) => void
): boolean {
if (!rules) {
......@@ -127,7 +127,7 @@ export class Validator {
*/
public static validateAncestors(
node: HtmlElement,
rules: RequiredAncestors
rules: RequiredAncestors | null
): boolean {
if (!rules || rules.length === 0) {
return true;
......@@ -146,7 +146,7 @@ export class Validator {
*/
public static validateRequiredContent(
node: HtmlElement,
rules: RequiredContent
rules: RequiredContent | null
): string[] {
if (!rules || rules.length === 0) {
return [];
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`rule no-autoplay should contain contextual documentation 1`] = `
Object {
"description": "The autoplay attribute is not allowed on <video>.
Autoplaying content can be disruptive for users and has accessibilty concerns.
Prefer to let the user control playback.",
"url": "https://html-validate.org/rules/no-autoplay.html",
}
`;
exports[`rule no-autoplay should contain documentation 1`] = `
Object {
"description": "The autoplay attribute is not allowed.
Autoplaying content can be disruptive for users and has accessibilty concerns.
Prefer to let the user control playback.",
"url": "https://html-validate.org/rules/no-autoplay.html",
}
`;
......@@ -25,6 +25,7 @@ import InputMissingLabel from "./input-missing-label";
import LongTitle from "./long-title";
import MetaRefresh from "./meta-refresh";
import MissingDoctype from "./missing-doctype";
import NoAutoplay from "./no-autoplay";
import NoConditionalComment from "./no-conditional-comment";
import NoDeprecatedAttr from "./no-deprecated-attr";
import NoDupAttr from "./no-dup-attr";
......@@ -79,6 +80,7 @@ const bundledRules: Record<string, RuleConstructor<any, any>> = {
"long-title": LongTitle,
"meta-refresh": MetaRefresh,
"missing-doctype": MissingDoctype,
"no-autoplay": NoAutoplay,
"no-conditional-comment": NoConditionalComment,
"no-deprecated-attr": NoDeprecatedAttr,
"no-dup-attr": NoDupAttr,
......
import HtmlValidate from "../htmlvalidate";
import "../matchers";
import { processAttribute } from "../transform/mocks/attribute";
describe("rule no-autoplay", () => {
describe("default config", () => {
let htmlvalidate: HtmlValidate;
beforeAll(() => {
htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
});
it.each(["audio", "video"])(
"should not report error when <%s> does not have autoplay",
(tagName) => {
expect.assertions(1);
const report = htmlvalidate.validateString(`<${tagName}></${tagName}>`);
expect(report).toBeValid();
}
);
it("should not report error when autoplay attribute is dynamic", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
'<video dynamic-autoplay="enableAutoplay">',
null,
{
processAttribute,
}
);
expect(report).toBeValid();
});
it.each(["audio", "video"])(
"should report error when <%s> have autoplay",
(tagName) => {
expect.assertions(1);
const report = htmlvalidate.validateString(
`<${tagName} autoplay></${tagName}>`
);
expect(report).toBeInvalid();
}
);
});
it("should not report error when role is excluded", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": ["error", { exclude: ["video"] }] },
});
const valid = htmlvalidate.validateString("<video autoplay></video>");
const invalid = htmlvalidate.validateString("<audio autoplay></audio>");
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should report error only for included roles", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": ["error", { include: ["video"] }] },
});
const valid = htmlvalidate.validateString("<audio autoplay></audio>");
const invalid = htmlvalidate.validateString("<video autoplay></video>");
expect(valid).toBeValid();
expect(invalid).toBeInvalid();
});
it("should contain documentation", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
expect(htmlvalidate.getRuleDocumentation("no-autoplay")).toMatchSnapshot();
});
it("should contain contextual documentation", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
rules: { "no-autoplay": "error" },
});
const context = {
tagName: "video",
};
expect(
htmlvalidate.getRuleDocumentation("no-autoplay", null, context)
).toMatchSnapshot();
});
});
import { DynamicValue } from "../dom";
import { AttributeEvent } from "../event";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
interface RuleContext {
tagName: string;
}
interface RuleOptions {
include: string[] | null;
exclude: string[] | null;
}
const defaults: RuleOptions = {
include: null,
exclude: null,
};
export default class NoAutoplay extends Rule<RuleContext, RuleOptions> {
public constructor(options: RuleOptions) {
super(Object.assign({}, defaults, options));
}
public documentation(context: RuleContext): RuleDocumentation {
return {
description: [
`The autoplay attribute is not allowed${
context ? ` on <${context.tagName}>` : ""
}.`,
"Autoplaying content can be disruptive for users and has accessibilty concerns.",
"Prefer to let the user control playback.",
].join("\n"),
url: ruleDocumentationUrl(__filename),
};
}
public setup(): void {
this.on("attr", (event: AttributeEvent) => {
/* only handle autoplay attribute */
if (event.key.toLowerCase() !== "autoplay") {
return;
}
/* ignore dynamic values */
if (event.value && event.value instanceof DynamicValue) {
return;
}
/* ignore tagnames configured to be ignored */
const tagName = event.target.tagName;
if (this.isIgnored(tagName)) {
return;
}
/* report error */
const context: RuleContext = { tagName };
const location = event.location;
this.report(
event.target,
`The autoplay attribute is not allowed on <${tagName}>`,
location,
context
);
});
}
private isIgnored(tagName: string): boolean {
const { include, exclude } = this.options;
/* ignore tagnames not present in "include" */
if (include && !include.includes(tagName)) {
return true;
}
/* ignore tagnames present in "excludes" */
if (exclude && exclude.includes(tagName)) {
return true;
}
return false;
}
}
<!-- summary must go before flow -->
<details>
<div></div>
<summary></summary>
</details>
<!-- summary is required -->
<details></details>
<!-- open attribute is boolean -->
<details open="foobar">
<summary></summary>
</details>
<!-- should allow in flow -->
<div>
<details><summary></summary></details>
</div>
<!-- should allow flow after summary -->
<details>
<summary>lorem ipsum</summary>
<div>dolor sit amet</div>
</details>
<!-- should allow open boolean attribute -->
<details open>
<summary></summary>
</details>
......@@ -9,3 +9,10 @@
<span>foo</span>
</legend>
</fieldset>
<!-- should allow headings -->
<fieldset>
<legend>
<h1>foo</h1>
</legend>
</fieldset>
<!-- should not allow under arbitrary content -->
<div>
<summary>lorem ipsum</summary>
</div>
<!-- should not allow flow content -->
<details>
<summary>
<p>lorem ipsum</p>
</summary>
</details>
<!-- should allow summary under details -->
<details>
<summary>Lorem ipsum</summary>
</details>
<!-- should allow phrasing and heading -->
<details>
<summary>
<span>lorem</span> <h1>ipsum</h1>
</summary>
</details>
{
"root": true,
"elements": ["./elements.js"],
"rules": {
"deprecated": "error",
......
{
"root": true,
"elements": ["./elements.json"],
"rules": {
"deprecated": "error",
......
{
"root": true,
"extends": ["./config.js"]
}
{
"root": true,
"extends": ["./config.json"]
}
{
"root": true,
"rules": {
"void": "off",
"void-style": ["error", { "style": "selfclose" }]
......