Commits (37)
......@@ -54,7 +54,7 @@ Changelog:
needs: ["NPM"]
dependencies: ["NPM"]
script:
- npm run commitlint -- --from=origin/master --to=${CI_COMMIT_SHA}
- npm exec commitlint -- --from=origin/master --to=${CI_COMMIT_SHA}
ESLint:
stage: test
......@@ -124,7 +124,6 @@ Node 15.x (current):
Release:
stage: release
image: node:14
only:
- web
variables:
......@@ -133,7 +132,7 @@ Release:
GIT_COMMITTER_NAME: ${HTML_VALIDATE_BOT_NAME}
GIT_COMMITTER_EMAIL: ${HTML_VALIDATE_BOT_EMAIL}
script:
- npm run semantic-release
- npm exec semantic-release
.downstream: &downstream
stage: postrelease
......
# html-validate changelog
### [4.6.1](https://gitlab.com/html-validate/html-validate/compare/v4.6.0...v4.6.1) (2021-03-02)
### Bug Fixes
- **dom:** `generateSelector()` escapes characters ([c2e316c](https://gitlab.com/html-validate/html-validate/commit/c2e316c6e980c7814d0a34102f8da529a111b5f6)), closes [#108](https://gitlab.com/html-validate/html-validate/issues/108)
- **dom:** `querySelector` handles escaped characters ([30e7503](https://gitlab.com/html-validate/html-validate/commit/30e75036b71dbf7564021b89a02aab11342647b7))
- **dom:** throw error when selector is missing pseudoclass name ([516ca06](https://gitlab.com/html-validate/html-validate/commit/516ca065dfcbc22d542f2336d91d0685f1870c64))
## [4.6.0](https://gitlab.com/html-validate/html-validate/compare/v4.5.0...v4.6.0) (2021-02-13)
### Features
......
This diff is collapsed.
{
"name": "html-validate",
"version": "4.6.0",
"version": "4.6.1",
"description": "html linter",
"keywords": [
"html",
......@@ -41,16 +41,14 @@
"build": "tsc",
"build:docs": "grunt docs",
"clean": "rm -rf dist public",
"commitlint": "commitlint",
"compatibility": "scripts/compatibility.sh",
"debug": "node --inspect ./node_modules/.bin/jest --runInBand --watch --no-coverage",
"eslint": "eslint .",
"eslint:fix": "eslint --fix .",
"htmlvalidate": "./bin/html-validate.js",
"prepare": "scripts/prepare",
"prepare": "husky install scripts",
"prettier:check": "prettier --check .",
"prettier:write": "prettier --write .",
"semantic-release": "semantic-release",
"start": "grunt connect",
"test": "jest --ci"
},
......@@ -98,14 +96,14 @@
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.12.16",
"@babel/preset-env": "7.12.16",
"@commitlint/cli": "11.0.0",
"@html-validate/commitlint-config": "1.2.0",
"@html-validate/eslint-config": "3.1.0",
"@html-validate/eslint-config-jest": "3.0.0",
"@html-validate/eslint-config-typescript": "3.0.0",
"@html-validate/jest-config": "1.2.4",
"@babel/core": "7.13.8",
"@babel/preset-env": "7.13.8",
"@commitlint/cli": "12.0.1",
"@html-validate/commitlint-config": "1.3.1",
"@html-validate/eslint-config": "4.1.0",
"@html-validate/eslint-config-jest": "4.0.0",
"@html-validate/eslint-config-typescript": "4.0.0",
"@html-validate/jest-config": "1.2.6",
"@html-validate/prettier-config": "1.1.0",
"@html-validate/semantic-release-config": "1.2.6",
"@lodder/grunt-postcss": "3.0.0",
......@@ -116,7 +114,7 @@
"@types/jest": "26.0.20",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.44",
"@types/node": "11.15.47",
"@types/prompts": "2.0.9",
"autoprefixer": "10.2.4",
"babelify": "10.0.0",
......@@ -126,9 +124,9 @@
"dgeni": "0.4.13",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint": "7.19.0",
"eslint": "7.21.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"eslint-plugin-sonarjs": "0.6.0",
"font-awesome": "4.7.0",
"front-matter": "4.0.2",
"grunt": "1.3.0",
......@@ -138,25 +136,25 @@
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.6.0",
"husky": "5.0.9",
"husky": "5.1.1",
"jest": "26.6.3",
"jest-diff": "26.6.2",
"jquery": "3.5.1",
"lint-staged": "10.5.4",
"load-grunt-tasks": "5.1.0",
"marked": "2.0.0",
"marked": "2.0.1",
"minimatch": "3.0.4",
"npm-pkg-lint": "1.3.0",
"postcss": "8.2.6",
"prettier": "2.2.1",
"pretty-format": "26.6.2",
"sass": "1.32.7",
"semantic-release": "17.3.8",
"sass": "1.32.8",
"semantic-release": "17.4.0",
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "26.5.1",
"typescript": "4.1.5"
"ts-jest": "26.5.2",
"typescript": "4.2.2"
},
"engines": {
"node": ">= 10.0"
......
_
\ No newline at end of file
_
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec npx --no-install commitlint --edit $1
exec npm exec commitlint -- --edit $1
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
exec npx --no-install lint-staged
exec npm exec lint-staged
#/bin/sh
echo "Configure git commit.template"
git config --local commit.template ./node_modules/@html-validate/commitlint-config/gitmessage
echo "Installing husky"
npx --no-install husky install "$(dirname "$0")"
......@@ -442,6 +442,45 @@ describe("HtmlElement", () => {
expect(el.generateSelector()).toEqual("#foo > p");
});
it("should handle colon in id", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<div id="foo:">
<p></p>
</div>
</div>
`);
const el = document.querySelector("p");
expect(el.generateSelector()).toEqual("#foo\\: > p");
});
it("should handle space in id", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<div id="foo ">
<p></p>
</div>
</div>
`);
const el = document.querySelector("p");
expect(el.generateSelector()).toEqual("#foo\\ > p");
});
it("should handle bracket in id", () => {
expect.assertions(1);
const document = parser.parseHtml(`
<div>
<div id="foo[bar]">
<p></p>
</div>
</div>
`);
const el = document.querySelector("p");
expect(el.generateSelector()).toEqual("#foo\\[bar\\] > p");
});
it("should normalize tagnames", () => {
expect.assertions(1);
const document = parser.parseHtml(`<dIV></DIv>`);
......
......@@ -143,9 +143,10 @@ export class HtmlElement extends DOMNode {
for (let cur: HtmlElement = this; cur.parent; cur = cur.parent) {
/* if a unique id is present, use it and short-circuit */
if (cur.id) {
const matches = root.querySelectorAll(`#${cur.id}`);
const escaped = cur.id.replace(/([:[\] ])/g, "\\$1");
const matches = root.querySelectorAll(`#${escaped}`);
if (matches.length === 1) {
parts.push(`#${cur.id}`);
parts.push(`#${escaped}`);
break;
}
}
......
......@@ -118,6 +118,30 @@ describe("Selector", () => {
]);
});
it("should match id with escaped colon", () => {
expect.assertions(1);
const parser = new Parser(Config.empty().resolve());
const doc = parser.parseHtml(`<div id="foo:"></div>`).root;
const selector = new Selector("#foo\\:");
expect(fetch(selector.match(doc))).toEqual([expect.objectContaining({ tagName: "div" })]);
});
it("should match id with escaped space", () => {
expect.assertions(1);
const parser = new Parser(Config.empty().resolve());
const doc = parser.parseHtml(`<div id="foo "></div>`).root;
const selector = new Selector("#foo\\ ");
expect(fetch(selector.match(doc))).toEqual([expect.objectContaining({ tagName: "div" })]);
});
it("should match id with escaped bracket", () => {
expect.assertions(1);
const parser = new Parser(Config.empty().resolve());
const doc = parser.parseHtml(`<div id="foo[bar]"></div>`).root;
const selector = new Selector("#foo\\[bar\\]");
expect(fetch(selector.match(doc))).toEqual([expect.objectContaining({ tagName: "div" })]);
});
it("should match having attribute ([wilma])", () => {
expect.assertions(1);
const selector = new Selector("[wilma]");
......@@ -197,6 +221,13 @@ describe("Selector", () => {
]);
});
it("should throw error for missing pseudo-class", () => {
expect.assertions(1);
expect(() => new Selector("foo:")).toThrow(
'Missing pseudo-class after colon in selector pattern "foo:"'
);
});
it("should throw error for invalid pseudo-classes", () => {
expect.assertions(1);
const selector = new Selector("foo:missing");
......
......@@ -3,6 +3,15 @@ import { Combinator, parseCombinator } from "./combinator";
import { HtmlElement } from "./htmlelement";
import { factory as pseudoClassFunction } from "./pseudoclass";
/**
* Homage to PHP: unescapes slashes.
*
* E.g. "foo\:bar" becomes "foo:bar"
*/
function stripslashes(value: string): string {
return value.replace(/\\(.)/g, "$1");
}
class Matcher {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public match(node: HtmlElement): boolean {
......@@ -29,7 +38,7 @@ class IdMatcher extends Matcher {
public constructor(id: string) {
super();
this.id = id;
this.id = stripslashes(id);
}
public match(node: HtmlElement): boolean {
......@@ -69,9 +78,13 @@ class PseudoClassMatcher extends Matcher {
private readonly name: string;
private readonly args: string;
public constructor(pseudoclass: string) {
public constructor(pseudoclass: string, context: string) {
super();
const [, name, args] = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/) as RegExpMatchArray;
const match = pseudoclass.match(/^([^(]+)(?:\((.*)\))?$/);
if (!match) {
throw new Error(`Missing pseudo-class after colon in selector pattern "${context}"`);
}
const [, name, args] = match;
this.name = name;
this.args = args;
}
......@@ -94,15 +107,15 @@ export class Pattern {
this.selector = pattern;
this.combinator = parseCombinator(match.shift());
this.tagName = match.shift() || "*";
const p = match[0] ? match[0].split(/(?=[.#[:])/) : [];
this.pattern = p.map((cur: string) => Pattern.createMatcher(cur));
const p = match[0] ? match[0].split(/(?=(?<!\\)[.#[:])/) : [];
this.pattern = p.map((cur: string) => this.createMatcher(cur));
}
public match(node: HtmlElement): boolean {
return node.is(this.tagName) && this.pattern.every((cur: Matcher) => cur.match(node));
}
private static createMatcher(pattern: string): Matcher {
private createMatcher(pattern: string): Matcher {
switch (pattern[0]) {
case ".":
return new ClassMatcher(pattern.slice(1));
......@@ -111,7 +124,7 @@ export class Pattern {
case "[":
return new AttrMatcher(pattern.slice(1, -1));
case ":":
return new PseudoClassMatcher(pattern.slice(1));
return new PseudoClassMatcher(pattern.slice(1), this.selector);
default:
/* istanbul ignore next: fallback solution, the switch cases should cover
* everything and there is no known way to trigger this fallback */
......@@ -163,7 +176,7 @@ export class Selector {
* easier parsing */
selector = selector.replace(/([+~>]) /g, "$1");
const pattern = selector.split(/ +/);
const pattern = selector.split(/(?:(?<!\\) )+/);
return pattern.map((part: string) => new Pattern(part));
}
......