GitLab Commit is coming up on August 3-4. Learn how to innovate together using GitLab, the DevOps platform. Register for free: gitlabcommitvirtual2021.com

Commit 58ba1aa4 authored by David Sveningsson's avatar David Sveningsson
Browse files

fix(rules): handle unknown elements better in `element-permitted-content`

fixes #95
parent 07afa1aa
Pipeline #184084794 passed with stages
in 9 minutes and 58 seconds
......@@ -20,6 +20,29 @@ describe("rule element-permitted-content", () => {
);
});
it("should report error when child is disallowed (referenced by tagname without meta)", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
"custom-link": {
permittedContent: [{ exclude: "custom-element" }],
},
},
],
rules: { "element-permitted-content": "error" },
});
const report = htmlvalidate.validateString(
"<custom-link><custom-element></custom-element></custom-link>"
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <custom-element> is not permitted as content in <custom-link>"
);
});
it("should report error when descendant is disallowed", () => {
expect.assertions(2);
const report = htmlvalidate.validateString(
......@@ -32,6 +55,41 @@ describe("rule element-permitted-content", () => {
);
});
it("should report error when descendant is disallowed (referenced by tagname without meta)", () => {
expect.assertions(2);
const htmlvalidate = new HtmlValidate({
elements: [
"html5",
{
"custom-link": {
permittedDescendants: [{ exclude: "custom-element" }],
},
},
],
rules: { "element-permitted-content": "error" },
});
const report = htmlvalidate.validateString(
"<custom-link><span><custom-element></custom-element></span></custom-link>"
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <custom-element> is not permitted as descendant of <custom-link>"
);
});
it("should report error when descendant is disallowed (intermediate element without meta)", () => {
expect.assertions(2);
const report = htmlvalidate.validateString(
"<a><custom-element><button></button></custom-element></a>"
);
expect(report).toBeInvalid();
expect(report).toHaveError(
"element-permitted-content",
"Element <button> is not permitted as descendant of <a>"
);
});
it("should not report error when phrasing a-element is child of @phrasing", () => {
expect.assertions(1);
const report = htmlvalidate.validateString(
......
......@@ -17,26 +17,19 @@ export default class ElementPermittedContent extends Rule {
this.on("dom:ready", (event: DOMReadyEvent) => {
const doc = event.document;
doc.visitDepthFirst((node: HtmlElement) => {
/* dont verify root element, assume any element is allowed */
if (node.parent.isRootElement()) {
return;
}
const parent = node.parent;
/* if parent doesn't have metadata (unknown element) skip checking permitted
* content */
if (!node.parent.meta) {
/* dont verify root element, assume any element is allowed */
if (parent.isRootElement()) {
return;
}
const parent = node.parent;
const rules = parent.meta.permittedContent;
/* Run each validation step, stop as soon as any errors are
* reported. This is to prevent multiple similar errors on the same
* element, such as "<dd> is not permitted content under <span>" and
* "<dd> has no permitted ancestors". */
[
() => this.validatePermittedContent(node, parent, rules),
() => this.validatePermittedContent(node, parent),
() => this.validatePermittedDescendant(node, parent),
() => this.validatePermittedAncestors(node),
].some((fn) => fn());
......@@ -45,9 +38,23 @@ export default class ElementPermittedContent extends Rule {
}
private validatePermittedContent(
cur: HtmlElement,
parent: HtmlElement
): boolean {
/* if parent doesn't have metadata (unknown element) skip checking permitted
* content */
if (!parent.meta) {
return false;
}
const rules = parent.meta.permittedContent ?? null;
return this.validatePermittedContentImpl(cur, parent, rules);
}
private validatePermittedContentImpl(
cur: HtmlElement,
parent: HtmlElement,
rules: Permitted
rules: Permitted | null
): boolean {
if (!Validator.validatePermitted(cur, rules)) {
this.report(
......@@ -63,7 +70,7 @@ export default class ElementPermittedContent extends Rule {
if (cur.meta && cur.meta.transparent) {
return cur.childElements
.map((child: HtmlElement) => {
return this.validatePermittedContent(child, parent, rules);
return this.validatePermittedContentImpl(child, parent, rules);
})
.some(Boolean);
}
......@@ -75,19 +82,23 @@ export default class ElementPermittedContent extends Rule {
node: HtmlElement,
parent: HtmlElement
): boolean {
while (!parent.isRootElement()) {
if (
parent.meta &&
node.meta &&
!Validator.validatePermitted(node, parent.meta.permittedDescendants)
) {
this.report(
node,
`Element <${node.tagName}> is not permitted as descendant of ${parent.annotatedName}`
);
return true;
for (let cur = parent; !cur.isRootElement(); cur = cur.parent) {
const meta = cur.meta;
/* ignore checking parent without meta */
if (!meta) {
continue;
}
if (Validator.validatePermitted(node, meta.permittedDescendants)) {
continue;
}
parent = parent.parent;
this.report(
node,
`Element <${node.tagName}> is not permitted as descendant of ${cur.annotatedName}`
);
return true;
}
return false;
}
......
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