Commits (10)
# html-validate changelog
## [4.5.0](https://gitlab.com/html-validate/html-validate/compare/v4.4.0...v4.5.0) (2021-02-05)
### Features
- **meta:** `transparent` can be limited to specific elements ([bef8a16](https://gitlab.com/html-validate/html-validate/commit/bef8a1663b70539091c203d5a4167446513904b9))
### Bug Fixes
- **html5:** `<audio>` and `<video>` allows `<track>` and `<source>` transparently ([526006c](https://gitlab.com/html-validate/html-validate/commit/526006c6c95418ac7dac2d3ef9f7a9b4158b62d2)), closes [#104](https://gitlab.com/html-validate/html-validate/issues/104)
## [4.4.0](https://gitlab.com/html-validate/html-validate/compare/v4.3.0...v4.4.0) (2021-01-31)
### Features
......
......@@ -35,7 +35,7 @@ export interface MetaElement {
deprecated?: boolean | string | DeprecatedElement;
foreign?: boolean;
void?: boolean;
transparent?: boolean;
transparent?: boolean | string[];
scriptSupporting?: boolean;
form?: boolean;
labelable?: boolean;
......@@ -164,6 +164,9 @@ as a child of a `<span>` element (phrasing) it only allows new phrasing content.
For custom elements it can be useful to set this if the content category isn't
flow.
When set to `true` all children are checked.
When set to array only the listed tagnames or content categories are checked.
### `scriptSupporting`
Elements whose primary purpose is to support scripting should set this flag to `true`.
......
......@@ -91,7 +91,7 @@
"phrasing": true,
"embedded": true,
"interactive": ["hasAttribute", "controls"],
"transparent": true,
"transparent": ["@flow"],
"attributes": {
"preload": ["", "none", "metadata", "auto"]
},
......@@ -1200,7 +1200,7 @@
"phrasing": true,
"embedded": true,
"interactive": ["hasAttribute", "controls"],
"transparent": true,
"transparent": ["@flow"],
"attributes": {
"preload": ["", "none", "metadata", "auto"]
},
......
This diff is collapsed.
{
"name": "html-validate",
"version": "4.4.0",
"version": "4.5.0",
"description": "html linter",
"keywords": [
"html",
......@@ -105,8 +105,8 @@
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.12.10",
"@babel/preset-env": "7.12.11",
"@babel/core": "7.12.13",
"@babel/preset-env": "7.12.13",
"@commitlint/cli": "11.0.0",
"@html-validate/commitlint-config": "1.2.0",
"@html-validate/eslint-config": "3.1.0",
......@@ -133,7 +133,7 @@
"dgeni": "0.4.13",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint": "7.18.0",
"eslint": "7.19.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"font-awesome": "4.7.0",
......@@ -151,13 +151,13 @@
"jquery": "3.5.1",
"lint-staged": "10.5.3",
"load-grunt-tasks": "5.1.0",
"marked": "1.2.8",
"marked": "1.2.9",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.2.4",
"prettier": "2.2.1",
"pretty-format": "26.6.2",
"sass": "1.32.5",
"sass": "1.32.6",
"semantic-release": "17.3.7",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
......
......@@ -51,7 +51,7 @@ export interface MetaData {
deprecated?: boolean | string | DeprecatedElement;
foreign?: boolean;
void?: boolean;
transparent?: boolean;
transparent?: boolean | string[];
implicitClosed?: string[];
scriptSupporting?: boolean;
form?: boolean;
......
......@@ -238,7 +238,7 @@ export class Validator {
* @param defaultMatch - The default return value when node categories is not known.
*/
// eslint-disable-next-line complexity
private static validatePermittedCategory(
public static validatePermittedCategory(
node: HtmlElement,
category: string,
defaultMatch: boolean
......
......@@ -10,6 +10,13 @@ describe("rule element-permitted-content", () => {
});
});
it("should not report error when elements are used correctly", () => {
expect.assertions(1);
const markup = "<div><p><span>foo</span></p></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should report error when @flow is child of @phrasing", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<span><div></div></span>");
......@@ -88,20 +95,84 @@ describe("rule element-permitted-content", () => {
);
});
it("should not report error when phrasing a-element is child of @phrasing", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<span><a><span></span></a></span>");
expect(report).toBeValid();
});
describe("transparent", () => {
it("should not report error when phrasing a-element is child of @phrasing", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<span><a><span></span></a></span>");
expect(report).toBeValid();
});
it("should report error when non-phrasing a-element is child of @phrasing", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<span><a><div></div></a></span>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <div> is not permitted as content in <span>"
);
it("should report error when non-phrasing a-element is child of @phrasing", () => {
expect.assertions(2);
const report = htmlvalidate.validateString("<span><a><div></div></a></span>");
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <div> is not permitted as content in <span>"
);
});
it("should report error for children listed as transparent", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
"transparent-element": {
phrasing: true,
transparent: ["div"],
},
},
],
rules: { "element-permitted-content": "error" },
});
const markup = "<span><transparent-element><div></div></transparent-element></span>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <div> is not permitted as content in <span>"
);
});
it("should not report error for children not listed as transparent", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
"transparent-element": {
phrasing: true,
transparent: ["p"],
},
},
],
rules: { "element-permitted-content": "error" },
});
const markup = "<span><transparent-element><div></div></transparent-element></span>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not report error for transparent unknown element children", () => {
expect.assertions(1);
const htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
"transparent-element": {
phrasing: true,
transparent: ["@flow"],
},
},
],
rules: { "element-permitted-content": "error" },
});
const markup =
"<span><transparent-element><unknown-element></unknown-element></transparent-element></span>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
});
it("should report error when label contains non-phrasing", () => {
......@@ -114,6 +185,26 @@ describe("rule element-permitted-content", () => {
);
});
describe("requiredAncestor", () => {
it("should report error for missing required ancestor", () => {
expect.assertions(2);
const markup = "<div><dt>foo</dt></div>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
'Element <dt> requires an "dl > dt" ancestor'
);
});
it("should not report error for proper required ancestor", () => {
expect.assertions(1);
const markup = "<dl><div><dt>foo</dt></div></dl>";
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
});
it("should handle missing meta entry (child)", () => {
expect.assertions(1);
const report = htmlvalidate.validateString("<p><foo>foo</foo></p>");
......
......@@ -4,6 +4,19 @@ import { Validator } from "../meta";
import { Permitted } from "../meta/element";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
function getTransparentChildren(node: HtmlElement, transparent: boolean | string[]): HtmlElement[] {
if (typeof transparent === "boolean") {
return node.childElements;
} else {
/* only return children which matches one of the given content categories */
return node.childElements.filter((it) => {
return transparent.some((category) => {
return Validator.validatePermittedCategory(it, category, false);
});
});
}
}
export default class ElementPermittedContent extends Rule {
public documentation(): RuleDocumentation {
return {
......@@ -61,11 +74,12 @@ export default class ElementPermittedContent extends Rule {
return true;
}
/* for transparent elements all of the children must be validated against
/* for transparent elements all/listed children must be validated against
* the (this elements) parent, i.e. if this node was removed from the DOM it
* should still be valid. */
if (cur.meta && cur.meta.transparent) {
return cur.childElements
const children = getTransparentChildren(cur, cur.meta.transparent);
return children
.map((child: HtmlElement) => {
return this.validatePermittedContentImpl(child, parent, rules);
})
......
import { Config } from "../../config";
import { DOMTree } from "../../dom";
import { DOMTree, HtmlElement, NodeClosed } from "../../dom";
import { Parser } from "../../parser";
import { processAttribute } from "../../transform/mocks/attribute";
import { inAccessibilityTree, isAriaHidden, isHTMLHidden, isPresentation } from "./a17y";
......@@ -59,6 +59,18 @@ describe("a17y helpers", () => {
const p = root.querySelector("p");
expect(inAccessibilityTree(p)).toBeTruthy();
});
it("should handle missing parent", () => {
expect.assertions(1);
const node = new HtmlElement("foo", null, NodeClosed.EndTag, null, {
filename: "inline",
line: 1,
column: 1,
offset: 0,
size: 1,
});
expect(inAccessibilityTree(node)).toBeTruthy();
});
});
describe("isAriaHidden()", () => {
......@@ -115,6 +127,18 @@ describe("a17y helpers", () => {
expect(isAriaHidden(p)).toBeTruthy();
expect(spy).toHaveBeenCalledTimes(0);
});
it("should handle missing parent", () => {
expect.assertions(1);
const node = new HtmlElement("foo", null, NodeClosed.EndTag, null, {
filename: "inline",
line: 1,
column: 1,
offset: 0,
size: 1,
});
expect(isAriaHidden(node)).toBeFalsy();
});
});
describe("isHTMLHidden()", () => {
......@@ -164,6 +188,18 @@ describe("a17y helpers", () => {
expect(isHTMLHidden(p)).toBeTruthy();
expect(spy).toHaveBeenCalledTimes(0);
});
it("should handle missing parent", () => {
expect.assertions(1);
const node = new HtmlElement("foo", null, NodeClosed.EndTag, null, {
filename: "inline",
line: 1,
column: 1,
offset: 0,
size: 1,
});
expect(isHTMLHidden(node)).toBeFalsy();
});
});
describe("isPresentation()", () => {
......@@ -227,5 +263,17 @@ describe("a17y helpers", () => {
expect(isPresentation(p)).toBeTruthy();
expect(spy).toHaveBeenCalledTimes(0);
});
it("should handle missing parent", () => {
expect.assertions(1);
const node = new HtmlElement("foo", null, NodeClosed.EndTag, null, {
filename: "inline",
line: 1,
column: 1,
offset: 0,
size: 1,
});
expect(isPresentation(node)).toBeFalsy();
});
});
});
......@@ -51,6 +51,8 @@ export default class NoMissingReferences extends Rule<Context> {
}
protected validateReference(document: DOMTree, node: HtmlElement, attr: Attribute | null): void {
/* sanity check: querySelector should never return elements without the attribute */
/* istanbul ignore next */
if (!attr) {
return;
}
......
......@@ -37,6 +37,12 @@ describe("rule script-type", () => {
expect(report).toBeValid();
});
it("should not report for other elements", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<p type="module"></p>');
expect(report).toBeValid();
});
it("should report when script element have empty type", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<script type=""></script>');
......@@ -55,6 +61,16 @@ describe("rule script-type", () => {
);
});
it("should report when script element have javascript type with parameter", () => {
expect.assertions(1);
const markup = '<script type="text/javascript;charset=utf-8"></script>';
const report = htmlvalidate.validateString(markup);
expect(report).toHaveError(
"script-type",
'"type" attribute is unnecessary for javascript resources'
);
});
it("should report when script element have legacy javascript type", () => {
expect.assertions(1);
const report = htmlvalidate.validateString('<script type="text/javascript"></script>');
......
......@@ -45,7 +45,8 @@ export default class ScriptType extends Rule {
}
private isJavascript(mime: string): boolean {
const match = mime.match(/^(.*?)(?:\s*;.*)?$/);
return match ? javascript.includes(match[1]) : false;
/* remove mime parameters, e.g. ";charset=utf-8" */
const type = mime.replace(/;.*/, "");
return javascript.includes(type);
}
}
import { HtmlElement, NodeClosed } from "../dom";
import { TagEndEvent } from "../event";
import { MetaElement } from "../meta";
import { Rule, RuleDocumentation, ruleDocumentationUrl } from "../rule";
type StyleName = "any" | "omit" | "selfclose" | "selfclosing";
......@@ -47,7 +48,7 @@ export default class Void extends Rule<void, RuleOptions> {
}
if (active && active.meta) {
this.validateActive(active);
this.validateActive(active, active.meta);
}
});
}
......@@ -58,15 +59,10 @@ export default class Void extends Rule<void, RuleOptions> {
}
}
/* eslint-disable-next-line complexity */
private validateActive(node: HtmlElement): void {
if (!node.meta) {
return;
}
private validateActive(node: HtmlElement, meta: MetaElement): void {
/* ignore foreign elements, they may or may not be self-closed and both are
* valid */
if (node.meta.foreign) {
if (meta.foreign) {
return;
}
......
......@@ -80,7 +80,7 @@
"transparent": {
"title": "Mark element as transparent",
"description": "Transparent elements follows the same content model as its parent, i.e. the content must be allowed in the parent.",
"type": "boolean"
"anyOf": [{ "type": "boolean" }, { "type": "array", "items": { "type": "string" } }]
},
"implicitClosed": {
......
......@@ -6,6 +6,14 @@
<!-- should be transparent -->
<span><audio><span>foo</span></audio></span>
<!-- should allow <source> and <track> when used transparently -->
<div>
<audio>
<source>
<track>
</audio>
</div>
<!-- should allow children in proper order -->
<audio>
<source>
......
......@@ -6,6 +6,14 @@
<!-- should be transparent -->
<span><video><span>foo</span></video></span>
<!-- should allow <source> and <track> when used transparently -->
<div>
<video>
<source>
<track>
</video>
</div>
<!-- should allow children in proper order -->
<video>
<source>
......