Commits (14)
# html-validate changelog
## [4.8.0](https://gitlab.com/html-validate/html-validate/compare/v4.7.1...v4.8.0) (2021-03-28)
### Features
- support ignoring files with `.htmlvalidateignore` ([77ec9a6](https://gitlab.com/html-validate/html-validate/commit/77ec9a623c5fabcbd743394d0bb58887d44d73c1)), closes [#109](https://gitlab.com/html-validate/html-validate/issues/109)
### [4.7.1](https://gitlab.com/html-validate/html-validate/compare/v4.7.0...v4.7.1) (2021-03-19)
### Bug Fixes
......
......@@ -7,7 +7,7 @@ module.exports = function highlight() {
function render(code, lang) {
if (lang) {
return hljs.highlight(lang, code).value;
return hljs.highlight(code, { language: lang }).value;
} else {
return hljs.highlightAuto(code).value;
}
......
......@@ -239,3 +239,15 @@ Disables the rule for the next element.
<blink>But this line will</blink>
```
<!-- prettier-ignore-end -->
## Ignoring files
### `.htmlvalidateignore` file
The `.htmlvalidateignore` file works similar to `.gitignore`, i.e. the file should contain a list of file patterns to ignore.
- Lines beginning with `#` are treated as comments
- Lines beginning with `!` are negated patterns that re-include a pattern that was ignored by an earlier pattern. It is not possible to re-include a file excluded from a parent directory.
- Paths are relative to the `.htmlvalidateignore` file
Similar to `.gitignore` if a line starts with `/` it matches from the current directory only, e.g `/foo.html` matches only `foo.html` in the current directory but not `src/foo.html`.
This diff is collapsed.
{
"name": "html-validate",
"version": "4.7.1",
"version": "4.8.0",
"description": "html linter",
"keywords": [
"html",
......@@ -82,22 +82,23 @@
]
},
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@html-validate/stylish": "1.0.0",
"@babel/code-frame": "^7.10.0",
"@html-validate/stylish": "^1.0.0",
"@sidvind/better-ajv-errors": "^0.8.0",
"acorn-walk": "^8.0.0",
"ajv": "^7.0.0",
"chalk": "^4.0.0",
"deepmerge": "^4.2.2",
"deepmerge": "^4.2.0",
"espree": "^7.3.0",
"glob": "^7.1.6",
"json-merge-patch": "^1.0.1",
"minimist": "^1.2.5",
"glob": "^7.1.0",
"ignore": "^5.0.0",
"json-merge-patch": "^1.0.0",
"minimist": "^1.2.0",
"prompts": "^2.0.0"
},
"devDependencies": {
"@babel/core": "7.13.10",
"@babel/preset-env": "7.13.10",
"@babel/core": "7.13.13",
"@babel/preset-env": "7.13.12",
"@commitlint/cli": "12.0.1",
"@html-validate/commitlint-config": "1.3.1",
"@html-validate/eslint-config": "4.1.0",
......@@ -108,13 +109,13 @@
"@html-validate/semantic-release-config": "1.2.6",
"@lodder/grunt-postcss": "3.0.0",
"@types/babel__code-frame": "7.0.2",
"@types/estree": "0.0.46",
"@types/estree": "0.0.47",
"@types/glob": "7.1.3",
"@types/inquirer": "7.3.1",
"@types/jest": "26.0.21",
"@types/jest": "26.0.22",
"@types/json-merge-patch": "0.0.5",
"@types/minimist": "1.2.1",
"@types/node": "11.15.49",
"@types/node": "11.15.50",
"@types/prompts": "2.0.9",
"autoprefixer": "10.2.5",
"babelify": "10.0.0",
......@@ -124,19 +125,19 @@
"dgeni": "0.4.14",
"dgeni-front-matter": "2.0.3",
"dgeni-packages": "0.28.4",
"eslint": "7.22.0",
"eslint": "7.23.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.6.0",
"font-awesome": "4.7.0",
"front-matter": "4.0.2",
"grunt": "1.3.0",
"grunt-browserify": "6.0.0",
"grunt-cli": "1.3.2",
"grunt-cli": "1.4.1",
"grunt-contrib-connect": "3.0.0",
"grunt-contrib-copy": "1.0.0",
"grunt-sass": "3.1.0",
"highlight.js": "10.6.0",
"husky": "5.1.3",
"highlight.js": "10.7.1",
"husky": "5.2.0",
"jest": "26.6.3",
"jest-diff": "26.6.2",
"jquery": "3.6.0",
......
......@@ -5,6 +5,7 @@ import HtmlValidate from "../htmlvalidate";
import { Report } from "../reporter";
import { expandFiles, ExpandOptions } from "./expand-files";
import { getFormatter } from "./formatter";
import { IsIgnored } from "./is-ignored";
import { init, InitResult } from "./init";
const defaultConfig: ConfigData = {
......@@ -19,6 +20,7 @@ export interface CLIOptions {
export class CLI {
private options: CLIOptions;
private config: ConfigData;
private ignored: IsIgnored;
/**
* Create new CLI helper.
......@@ -29,10 +31,14 @@ export class CLI {
public constructor(options?: CLIOptions) {
this.options = options || {};
this.config = this.getConfig();
this.ignored = new IsIgnored();
}
/**
* Returns list of files matching patterns and are not ignored.
*/
public expandFiles(patterns: string[], options: ExpandOptions = {}): string[] {
return expandFiles(patterns, options);
return expandFiles(patterns, options).filter((filename) => !this.isIgnored(filename));
}
public getFormatter(formatters: string): (report: Report) => string {
......@@ -49,6 +55,24 @@ export class CLI {
return init(cwd);
}
/**
* Searches ".htmlvalidateignore" files from filesystem and returns `true` if
* one of them contains a pattern matching given filename.
*/
public isIgnored(filename: string): boolean {
return this.ignored.isIgnored(filename);
}
/**
* Clear cache.
*
* Previously fetched [[HtmlValidate]] instances must either be fetched again
* or call [[HtmlValidate.flushConfigCache]].
*/
public clearCache(): void {
this.ignored.clearCache();
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
......
jest.mock("fs");
jest.mock("glob");
jest.mock("./is-ignored");
import fs from "fs";
import path from "path";
......
import fs from "fs";
import path from "path";
import { CLI } from "./cli";
import { IsIgnored } from "./is-ignored";
import "../matchers";
describe("integration tests", () => {
let ignored: IsIgnored;
const fixturePath = path.resolve(__dirname, "../../test-files/ignored");
beforeEach(() => {
ignored = new IsIgnored();
jest.restoreAllMocks();
});
it("should ignore foo.html", () => {
expect.assertions(1);
const filename = path.join(fixturePath, "foo.html");
expect(ignored.isIgnored(filename)).toBeTruthy();
});
it("should not ignore bar.html", () => {
expect.assertions(1);
const filename = path.join(fixturePath, "foo.html");
expect(ignored.isIgnored(filename)).toBeTruthy();
});
it("should ignore subdir/bar.html", () => {
expect.assertions(1);
const filename = path.join(fixturePath, "subdir/baz.html");
expect(ignored.isIgnored(filename)).toBeTruthy();
});
it("should ignore subdir/baz.html", () => {
expect.assertions(1);
const filename = path.join(fixturePath, "subdir/baz.html");
expect(ignored.isIgnored(filename)).toBeTruthy();
});
it("should cache ignore file", () => {
expect.assertions(3);
const readFileSync = jest.spyOn(fs, "readFileSync");
const filename1 = path.join(fixturePath, "subdir/bar.html");
const filename2 = path.join(fixturePath, "subdir/baz.html");
expect(ignored.isIgnored(filename1)).toBeTruthy();
expect(ignored.isIgnored(filename2)).toBeTruthy();
expect(readFileSync).toHaveBeenCalledTimes(1);
});
it("should clear cache", () => {
expect.assertions(3);
const readFileSync = jest.spyOn(fs, "readFileSync");
const filename1 = path.join(fixturePath, "subdir/bar.html");
const filename2 = path.join(fixturePath, "subdir/baz.html");
expect(ignored.isIgnored(filename1)).toBeTruthy();
ignored.clearCache();
expect(ignored.isIgnored(filename2)).toBeTruthy();
expect(readFileSync).toHaveBeenCalledTimes(2);
});
it("expandFiles() should only return files not ignored", () => {
expect.assertions(1);
const cli = new CLI();
const files = cli.expandFiles(["test-files/ignored"], { extensions: ["html", "vue"] });
expect(files).toEqual([
"test-files/ignored/bar.html",
"test-files/ignored/included/file.html",
"test-files/ignored/subdir/foo.vue",
]);
});
it("validate", () => {
expect.assertions(1);
const cli = new CLI();
const htmlvalidate = cli.getValidator();
const files = cli.expandFiles(["test-files/ignored"], { extensions: ["html", "vue"] });
const report = htmlvalidate.validateMultipleFiles(files);
expect(report).toBeValid();
});
});
import fs from "fs";
import path from "path";
import ignore, { Ignore } from "ignore";
export class IsIgnored {
/** Cache for parsed .htmlvalidateignore files */
private cacheIgnore: Map<string, Ignore | undefined>;
public constructor() {
this.cacheIgnore = new Map();
}
/**
* Searches ".htmlvalidateignore" files from filesystem and returns `true` if
* one of them contains a pattern matching given filename.
*/
public isIgnored(filename: string): boolean {
return this.match(filename);
}
/**
* Clear cache
*/
public clearCache(): void {
this.cacheIgnore.clear();
}
private match(target: string): boolean {
let current = path.dirname(target);
// eslint-disable-next-line no-constant-condition
while (true) {
const relative = path.relative(current, target);
const filename = path.join(current, ".htmlvalidateignore");
/* test filename (relative to the ignore file) against the patterns */
const ig = this.parseFile(filename);
if (ig && ig.ignores(relative)) {
return true;
}
/* get the parent directory */
const child = current;
current = path.dirname(current);
/* stop if this is the root directory */
if (current === child) {
break;
}
}
return false;
}
private parseFile(filename: string): Ignore | undefined {
if (this.cacheIgnore.has(filename)) {
return this.cacheIgnore.get(filename);
}
if (!fs.existsSync(filename)) {
this.cacheIgnore.set(filename, undefined);
return undefined;
}
const content = fs.readFileSync(filename, "utf-8");
const ig = ignore().add(content);
this.cacheIgnore.set(filename, ig);
return ig;
}
}
<!-- this file is not ignored -->
<!-- this file is ignored by "../.htmlvalidateignore" -->
<p>invalid</i>
<!-- this file is ignored by "./.htmlvalidateignore" -->
<p>invalid</i>
<!-- this file is not ignored -->
<!-- while the "included" directory is not ignored "foo.html" is ignored everywhere by "../.htmlvalidateignore" -->
<p>invalid</i>
<!-- this file is ignored by "./.htmlvalidateignore" -->
<p>invalid</i>
<!-- this file is not ignored -->