Commits (16)
......@@ -106,7 +106,7 @@ Docs:
- npm run --if-present build
- npm run compatibility
Node 10.x (LTS):
Node 10.x:
<<: *compat
image: node:10
......@@ -118,9 +118,9 @@ Node 14.x (LTS):
<<: *compat
image: node:14
Node 15.x (current):
Node 16.x (current):
<<: *compat
image: node:15
image: node:16
Release:
stage: release
......
# html-validate changelog
## [4.12.0](https://gitlab.com/html-validate/html-validate/compare/v4.11.0...v4.12.0) (2021-05-17)
### Features
- **rules:** enforce initial heading-level in sectioning roots ([c4306ad](https://gitlab.com/html-validate/html-validate/commit/c4306ad6920005c760431c2681d37e2fc25949fd))
## [4.11.0](https://gitlab.com/html-validate/html-validate/compare/v4.10.1...v4.11.0) (2021-05-08)
### Features
......
......@@ -4,9 +4,8 @@ const pkg = new Package("dgeni-bootstrap", []);
pkg.config(function (inlineTagProcessor, getInjectables) {
const inlineTagsDefs = getInjectables(require("./inline-tag-defs"));
inlineTagProcessor.inlineTagDefinitions = inlineTagProcessor.inlineTagDefinitions.concat(
inlineTagsDefs
);
inlineTagProcessor.inlineTagDefinitions =
inlineTagProcessor.inlineTagDefinitions.concat(inlineTagsDefs);
});
module.exports = pkg;
This diff is collapsed.
{
"name": "html-validate",
"version": "4.11.0",
"version": "4.12.0",
"description": "html linter",
"keywords": [
"html",
......@@ -30,6 +30,7 @@
"elements",
"jest.{js,d.ts}",
"test-utils.{js,d.ts}",
"!**/*.map",
"!**/*.snap",
"!**/*.spec.d.ts",
"!**/*.spec.js",
......@@ -46,7 +47,7 @@
"debug": "node --inspect ./node_modules/.bin/jest --runInBand --watch --no-coverage",
"eslint": "eslint .",
"eslint:fix": "eslint --fix .",
"htmlvalidate": "./bin/html-validate.js",
"htmlvalidate": "node ./bin/html-validate.js",
"prepare": "husky install scripts",
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
......@@ -132,14 +133,14 @@
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.14.0",
"@babel/preset-env": "7.14.1",
"@commitlint/cli": "12.1.1",
"@babel/core": "7.14.2",
"@babel/preset-env": "7.14.2",
"@commitlint/cli": "12.1.4",
"@html-validate/commitlint-config": "1.3.1",
"@html-validate/eslint-config": "4.4.0",
"@html-validate/eslint-config-jest": "4.4.0",
"@html-validate/eslint-config-typescript": "4.4.0",
"@html-validate/jest-config": "1.2.9",
"@html-validate/jest-config": "1.2.10",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.2.13",
"@lodder/grunt-postcss": "3.0.1",
......@@ -151,7 +152,7 @@
"@types/jest": "26.0.23",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.53",
"@types/node": "11.15.54",
"@types/prompts": "2.0.11",
"autoprefixer": "10.2.5",
"babar": "0.2.0",
......@@ -162,7 +163,7 @@
"dgeni": "0.4.14",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.29.1",
"eslint": "7.25.0",
"eslint": "7.26.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.7.0",
"font-awesome": "4.7.0",
......@@ -183,11 +184,11 @@
"marked": "2.0.3",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.2.14",
"prettier": "2.2.1",
"postcss": "8.2.15",
"prettier": "2.3.0",
"pretty-format": "26.6.2",
"sass": "1.32.12",
"semantic-release": "17.4.2",
"sass": "1.32.13",
"semantic-release": "17.4.3",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"ts-jest": "26.5.6",
......
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec npm exec commitlint -- --edit $1
exec npx --no-install commitlint --edit "$1"
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec npm exec lint-staged
exec npx --no-install lint-staged
......@@ -43,12 +43,12 @@ describe("parseSeverity()", () => {
it("should throw error on null", () => {
expect.assertions(1);
expect(() => parseSeverity((null as unknown) as string)).toThrow('Invalid severity "null"');
expect(() => parseSeverity(null as unknown as string)).toThrow('Invalid severity "null"');
});
it("should throw error on undefined", () => {
expect.assertions(1);
expect(() => parseSeverity((undefined as unknown) as string)).toThrow(
expect(() => parseSeverity(undefined as unknown as string)).toThrow(
'Invalid severity "undefined"'
);
});
......
......@@ -323,9 +323,7 @@ export class Engine<T extends Parser = Parser> {
/*
* Initialize all plugins. This should only be done once for all sessions.
*/
protected initPlugins(
config: ResolvedConfig
): {
protected initPlugins(config: ResolvedConfig): {
availableRules: { [key: string]: RuleConstructor<any, any> };
} {
for (const plugin of config.getPlugins()) {
......
......@@ -15,9 +15,9 @@ it("SchemaValidationError should pretty-print validation errors", () => {
const table = new MetaTable();
try {
table.loadFromObject({
foo: ({
foo: {
flow: "spam",
} as unknown) as MetaElement,
} as unknown as MetaElement,
});
} catch (err) {
if (err instanceof SchemaValidationError) {
......
......@@ -22,20 +22,18 @@ it("should compute correct line, column and offset when using transformed source
/* create a mock transformer which will create a new source for each line */
function transformer(source: Source): Source[] {
const lines = source.data.split("\n");
return lines.filter(Boolean).map(
(line: string, index: number): Source => {
/* all lines have the same length */
const offset = (line.length + 1) * index;
return {
data: line,
filename: source.filename,
line: index + 1,
column: 1,
offset,
originalData: source.data,
};
}
);
return lines.filter(Boolean).map((line: string, index: number): Source => {
/* all lines have the same length */
const offset = (line.length + 1) * index;
return {
data: line,
filename: source.filename,
line: index + 1,
column: 1,
offset,
originalData: source.data,
};
});
}
transformer.api = TRANSFORMER_API.VERSION;
......
......@@ -59,7 +59,7 @@ describe("MetaTable", () => {
const table = new MetaTable();
const fn = (): void =>
table.loadFromObject({
foo: mockEntry(({ invalid: true } as unknown) as Partial<MetaData>),
foo: mockEntry({ invalid: true } as unknown as Partial<MetaData>),
});
expect(fn).toThrow(SchemaValidationError);
expect(fn).toThrow(
......@@ -98,12 +98,12 @@ describe("MetaTable", () => {
it("should ignore $schema property", () => {
expect.assertions(2);
const table = new MetaTable();
table.loadFromObject(({
table.loadFromObject({
$schema: "https://example.net/schema.json",
foo: {
flow: true,
},
} as unknown) as MetaDataTable);
} as unknown as MetaDataTable);
expect(table.getMetaFor("foo")).toBeDefined();
expect(table.getMetaFor("$schema")).toBeNull();
});
......@@ -189,10 +189,10 @@ describe("MetaTable", () => {
expect.assertions(1);
metaTable = new MetaTable();
metaTable.loadFromObject({
invalid: mockEntry(({
invalid: mockEntry({
interactive: ["invalid"],
void: true,
} as unknown) as Partial<MetaData>),
} as unknown as Partial<MetaData>),
});
const parser = new Parser(new ResolvedConfig({ metaTable, plugins: [], rules: new Map() }));
expect(() => parser.parseHtml("<invalid/>")).toThrow(
......
......@@ -32,7 +32,7 @@ export class Parser {
*/
public constructor(config: ResolvedConfig) {
this.event = new EventHandler();
this.dom = (null as unknown) as DOMTree;
this.dom = null as unknown as DOMTree;
this.metaTable = config.getMetaTable();
}
......
......@@ -261,7 +261,7 @@ describe("rule base class", () => {
const spy = jest.fn();
const eventData: TagStartEvent = {
location,
target: (null as unknown) as HtmlElement,
target: null as unknown as HtmlElement,
};
rule.on("tag:open", spy);
parser.trigger("tag:start", eventData);
......@@ -273,8 +273,8 @@ describe("rule base class", () => {
const spy = jest.fn();
const eventData: TagEndEvent = {
location,
target: (null as unknown) as HtmlElement,
previous: (null as unknown) as HtmlElement,
target: null as unknown as HtmlElement,
previous: null as unknown as HtmlElement,
};
rule.on("tag:close", spy);
parser.trigger("tag:end", eventData);
......
......@@ -81,10 +81,10 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
public constructor(options: OptionsType) {
/* faux initialization, properly initialized by init(). This is to keep TS happy without adding null-checks everywhere */
this.reporter = (null as unknown) as Reporter;
this.parser = (null as unknown) as Parser;
this.meta = (null as unknown) as MetaTable;
this.event = (null as unknown) as Event;
this.reporter = null as unknown as Reporter;
this.parser = null as unknown as Parser;
this.meta = null as unknown as MetaTable;
this.event = null as unknown as Event;
this.options = options;
this.enabled = true;
......
......@@ -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];
}
......
......@@ -49,21 +49,19 @@ export class CaseStyle {
}
private parseStyle(style: CaseStyleName[], ruleId: string): Style[] {
return style.map(
(cur: string): Style => {
switch (cur.toLowerCase()) {
case "lowercase":
return { pattern: /^[a-z]*$/, name: "lowercase" };
case "uppercase":
return { pattern: /^[A-Z]*$/, name: "uppercase" };
case "pascalcase":
return { pattern: /^[A-Z][A-Za-z]*$/, name: "PascalCase" };
case "camelcase":
return { pattern: /^[a-z][A-Za-z]*$/, name: "camelCase" };
default:
throw new ConfigError(`Invalid style "${style}" for ${ruleId} rule`);
}
return style.map((cur: string): Style => {
switch (cur.toLowerCase()) {
case "lowercase":
return { pattern: /^[a-z]*$/, name: "lowercase" };
case "uppercase":
return { pattern: /^[A-Z]*$/, name: "uppercase" };
case "pascalcase":
return { pattern: /^[A-Z][A-Za-z]*$/, name: "PascalCase" };
case "camelcase":
return { pattern: /^[a-z][A-Za-z]*$/, name: "camelCase" };
default:
throw new ConfigError(`Invalid style "${style}" for ${ruleId} rule`);
}
);
});
}
}
......@@ -23,12 +23,10 @@ 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() };
}
);
.map((it): CSSDeclaration => {
const [property, value] = it.split(":", 2);
return { property: property.trim(), value: value.trim() };
});
}
export default class NoInlineStyle extends Rule<void, RuleOptions> {
......
......@@ -9,7 +9,7 @@
"noImplicitAny": true,
"outDir": "dist",
"resolveJsonModule": true,
"sourceMap": false,
"sourceMap": true,
"target": "es2017",
"strict": true
},
......