Commit c4306ad6 authored by Mihkel Eidast's avatar Mihkel Eidast
Browse files

feat(rules): enforce initial heading-level in sectioning roots

parent 786aa9f6
......@@ -109,6 +109,40 @@ describe("rule heading-level", () => {
const report = htmlvalidate.validateString(markup);
expect(report).toBeValid();
});
it("should not allow skipping heading levels", () => {
expect.assertions(2);
const markup = `
<h1>heading 1</h1>
<h2>heading 2</h2>
<h3>heading 2</h3>
<div role="dialog">
<h5>modal header</h5>
</div>
<h3>heading 2</h3>
`;
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"heading-level",
"Initial heading level for sectioning root must be between <h1> and <h4> but got <h5>"
);
});
it("should enforce h1 as initial heading level if sectioning root is the only content in document", () => {
expect.assertions(2);
const markup = `
<div role="dialog">
<h5>modal header</h5>
</div>
`;
const report = htmlvalidate.validateString(markup);
expect(report).toBeInvalid();
expect(report).toHaveError(
"heading-level",
"Initial heading level for sectioning root must be <h1> but got <h5>"
);
});
});
it("smoketest", () => {
......
import { sliceLocation } from "../context";
import { Location, sliceLocation } from "../context";
import { HtmlElement, Pattern } from "../dom";
import { DOMInternalID } from "../dom/domnode";
import { SelectorContext } from "../dom/selector-context";
......@@ -111,20 +111,54 @@ export default class HeadingLevel extends Rule<void, RuleOptions> {
return;
}
/* validate heading level was only incremented by one */
this.checkLevelIncrementation(root, event, level);
root.current = level;
}
/**
* Validate heading level was only incremented by one.
*/
private checkLevelIncrementation(
root: SectioningRoot,
event: TagStartEvent,
level: number
): void {
const expected = root.current + 1;
if (level !== expected) {
const location = sliceLocation(event.location, 1);
if (root.current > 0) {
const msg = `Heading level can only increase by one, expected <h${expected}> but got <h${level}>`;
this.report(event.target, msg, location);
} else if (this.stack.length === 1) {
const msg = `Initial heading level must be <h${expected}> but got <h${level}>`;
this.report(event.target, msg, location);
} else {
this.checkInitialLevel(event, location, level, expected);
}
}
}
root.current = level;
private checkInitialLevel(
event: TagStartEvent,
location: Location,
level: number,
expected: number
): void {
if (this.stack.length === 1) {
const msg = `Initial heading level must be <h${expected}> but got <h${level}>`;
this.report(event.target, msg, location);
} else {
const prevRoot = this.getPrevRoot();
const prevRootExpected = prevRoot.current + 1;
if (level > prevRootExpected) {
if (expected === prevRootExpected) {
const msg = `Initial heading level for sectioning root must be <h${expected}> but got <h${level}>`;
this.report(event.target, msg, location);
} else {
const msg = `Initial heading level for sectioning root must be between <h${expected}> and <h${prevRootExpected}> but got <h${level}>`;
this.report(event.target, msg, location);
}
}
}
}
/**
......@@ -155,6 +189,10 @@ export default class HeadingLevel extends Rule<void, RuleOptions> {
this.stack.pop();
}
private getPrevRoot(): SectioningRoot {
return this.stack[this.stack.length - 2];
}
private getCurrentRoot(): SectioningRoot {
return this.stack[this.stack.length - 1];
}
......
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