...
 
Commits (30)
......@@ -43,14 +43,23 @@ pages:
- npm run build
- npm run build:docs
Changelog:
stage: test
needs: ["NPM"]
dependencies: ["NPM"]
script:
- npm run commitlint -- --from=origin/master --to=${CI_COMMIT_SHA}
ESLint:
stage: test
needs: ["NPM"]
script:
- npm run eslint -- --max-warnings 0
Jest:
stage: test
dependencies: ["NPM"]
needs: ["NPM"]
coverage: /Lines\s+:\s(\d+.\d+%)/
artifacts:
name: ${CI_PROJECT_PATH_SLUG}-${CI_PIPELINE_ID}-coverage
......@@ -64,6 +73,7 @@ Jest:
TSLint:
stage: test
dependencies: ["NPM"]
needs: ["NPM"]
script:
- npm run tslint
......
# html-validate changelog
# [1.12.0](https://gitlab.com/html-validate/html-validate/compare/v1.11.0...v1.12.0) (2019-10-08)
### Features
- **cli:** new API to get validator instance ([6f4be7d](https://gitlab.com/html-validate/html-validate/commit/6f4be7d))
- **cli:** support passing options to CLI class ([aa544d6](https://gitlab.com/html-validate/html-validate/commit/aa544d6))
- **config:** add `root` property to stop searching file system ([9040ed5](https://gitlab.com/html-validate/html-validate/commit/9040ed5))
- **shim:** expose HtmlElement in shim ([dbb673f](https://gitlab.com/html-validate/html-validate/commit/dbb673f))
# [1.11.0](https://gitlab.com/html-validate/html-validate/compare/v1.10.0...v1.11.0) (2019-09-23)
### Bug Fixes
......
......@@ -174,3 +174,18 @@ test("should not frobnicate a flux", () => {
);
});
```
## CLI tools
The CLI interface can be wrapped using the `CLI` class.
```js
const cli = new CLI({
configFile: argv.configFile,
rules: argv.rules,
});
const htmlvalidate = cli.getValidator();
const formatter = cli.getFormatter(argv.formatter);
const files = cli.expandFiles("**/*.html");
const report = htmlvalidate.validateMultipleFiles(files);
```
......@@ -42,8 +42,10 @@
<li><a href="/usage/cli.html">Using CLI</a></li>
<li><a href="/usage/elements.html">Elements</a></li>
<li><a href="/usage/transformers.html">Transfomers</a></li>
<li role="separator" class="divider"></li>
<li><a href="/usage/grunt.html">Grunt</a></li>
<li><a href="/usage/protractor.html">Protractor</a></li>
<li><a href="/frameworks/vue.html">Vue.js</a></li>
</ul>
</li>
<li><a href="/rules">Rules</a></li>
......
@ngdoc content
@module frameworks
@name Usage with Vue.js
@description
# Usage with Vue.js
## Using `vue-cli-service`
vue add html-validate
Adds the required libraries and preconfigures `.vue` transformations. A
configuration is dropped in the project root directory with recommended
configuration for Vue.js.
Adds a new CLI service command:
vue-cli-service html-validate
Validates all `.html` and `.vue` files in the `src` folder. Patterns can be
overwritten by passing them as positional arguments.
## Manual configuration
npm install html-validate-vue
[html-validate-vue](https://www.npmjs.com/package/html-validate-vue) is needed
to transform `.vue` single file components and includes elements metadata
overrides for Vue.js.
Configure with:
```js
{
"elements": ["html5", "html-validate-vue/elements"],
"transform": {
"^.*\\.vue$": "html-validate-vue"
}
}
```
......@@ -75,7 +75,7 @@ plugins](dev/writing-plugins.html).**
First-class support for:
- [angular](https://www.npmjs.com/package/html-validate-angular)
- [vue](https://www.npmjs.com/package/html-validate-vue)
- [vue](frameworks/vue.html)
- [protractor](https://www.npmjs.com/package/protractor-html-validate)
@block Examples
......
......@@ -108,6 +108,16 @@ Specify a different configuration file.
html-validate --config myconfig.json file.html
Note that specifying a separate configuration file changes the default
configuration but `.htmlvalidate.json` files will still be searched from the
filesystem. Set the `root` property to `true` to prevent this behaviour:
```js
{
"root": true
}
```
### `--print-config`
Instead of validating file print the configuration generated.
......
......@@ -131,6 +131,38 @@ will transform `*.vue` with the `html-validate-vue` NPM package. Use a relative
path to use a local script (use `<rootDir>` to refer to the path to
`package.json`, e.g. `<rootDir>/my-transformer.js`).
### `root`
By default, configuration is search in the file structure until the root
directory (typically `/`) is found:
- `/home/user/project/src/.htmlvalidate.json`
- `/home/user/project/.htmlvalidate.json`
- `/home/user/.htmlvalidate.json`
- `/home/.htmlvalidate.json`
- `/.htmlvalidate.json`
By setting the `root` property to `true` the search is stopped. This can be used
to prevent searching from outside the project directory or to use a specific
configuration for a specific directory without loading project configuration.
For instance, if `/home/project/.htmlvalidate.json` contains:
```js
{
"root": true
}
```
only the following files would be searched:
- `/home/user/project/src/.htmlvalidate.json`
- `/home/user/project/.htmlvalidate.json`
This also affects CLI `--config` and the API, e.g. when using `--config` with a
configuration using `"root": true` will prevent any additional files to be
loaded.
## Inline configuration
Configuration can be changed inline using directive of the form:
......
This diff is collapsed.
{
"name": "html-validate",
"version": "1.11.0",
"version": "1.12.0",
"description": "html linter",
"keywords": [
"html",
......@@ -29,6 +29,7 @@
"build": "tsc",
"build:docs": "grunt docs",
"clean": "rm -rf build public",
"commitlint": "commitlint",
"eslint": "eslint *.js '{docs,elements,src}/**/*.{js,ts}'",
"eslint:fix": "eslint --fix *.js '{docs,elements,src}/**/*.{js,ts}'",
"htmlvalidate": "./bin/html-validate.js",
......@@ -41,8 +42,14 @@
"tslint": "tslint -t verbose *.ts src/**/*.ts",
"tslint:fix": "tslint -t verbose --fix *.ts src/**/*.ts"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
"pre-commit": "lint-staged",
"pre-push": "./scripts/pre-push"
}
......@@ -105,13 +112,15 @@
"minimist": "^1.2.0"
},
"devDependencies": {
"@babel/core": "7.6.0",
"@babel/preset-env": "7.6.0",
"@babel/core": "7.6.2",
"@babel/preset-env": "7.6.2",
"@commitlint/cli": "8.2.0",
"@commitlint/config-conventional": "8.2.0",
"@semantic-release/changelog": "3.0.4",
"@semantic-release/exec": "3.3.7",
"@semantic-release/git": "7.0.16",
"@semantic-release/gitlab": "3.1.7",
"@semantic-release/npm": "5.1.15",
"@semantic-release/gitlab": "4.0.0",
"@semantic-release/npm": "5.2.0",
"@semantic-release/release-notes-generator": "7.3.0",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.39",
......@@ -119,17 +128,17 @@
"@types/jest": "24.0.18",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.13.20",
"@typescript-eslint/eslint-plugin": "2.3.0",
"@typescript-eslint/parser": "2.3.0",
"autoprefixer": "9.6.1",
"@types/node": "11.13.22",
"@typescript-eslint/eslint-plugin": "2.3.3",
"@typescript-eslint/parser": "2.3.3",
"autoprefixer": "9.6.4",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
"cssnano": "4.1.10",
"dgeni": "0.4.12",
"dgeni-packages": "0.28.1",
"eslint-config-prettier": "6.3.0",
"eslint-config-prettier": "6.4.0",
"eslint-config-sidvind": "1.3.2",
"eslint-plugin-array-func": "3.1.3",
"eslint-plugin-import": "2.18.2",
......@@ -147,16 +156,16 @@
"grunt-postcss": "0.9.0",
"grunt-sass": "3.1.0",
"highlight.js": "9.15.10",
"husky": "3.0.5",
"husky": "3.0.8",
"jest": "24.9.0",
"jest-diff": "24.9.0",
"jest-junit": "8.0.0",
"jquery": "3.4.1",
"lint-staged": "9.3.0",
"lint-staged": "9.4.1",
"load-grunt-tasks": "5.1.0",
"minimatch": "3.0.4",
"prettier": "1.18.2",
"sass": "1.22.12",
"sass": "1.23.0",
"semantic-release": "15.13.24",
"serve-static": "1.14.1",
"strip-ansi": "5.2.0",
......
......@@ -21,4 +21,7 @@ function statSync(dir: string): any {
module.exports = {
setMockDirectories,
statSync,
readFileSync: () => {
throw new Error("ENOENT");
},
};
import fs from "fs";
import HtmlValidate from "../htmlvalidate";
import { CLI } from "./cli";
jest.disableAutomock();
jest.mock("fs");
jest.mock("../htmlvalidate");
describe("CLI", () => {
beforeEach(() => {
(HtmlValidate as any).mockClear();
});
describe("getValidator()", () => {
it("should create new HtmlValidate instance", () => {
const cli = new CLI();
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: ["htmlvalidate:recommended"],
});
expect(htmlvalidate).toBeDefined();
});
it("should use configuration file", () => {
const readFileSync = jest
.spyOn(fs, "readFileSync")
.mockImplementation(() => '{"rules": {"foo": "error"}}');
const cli = new CLI({
configFile: "config.json",
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
rules: {
foo: "error",
},
});
expect(htmlvalidate).toBeDefined();
expect(readFileSync).toHaveBeenCalledWith("config.json", "utf-8");
});
it("should configure single rule", () => {
const cli = new CLI({
rules: "foo:1",
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: [],
rules: {
foo: 1,
},
});
expect(htmlvalidate).toBeDefined();
});
it("should configure multiple rule", () => {
const cli = new CLI({
rules: ["foo:1", "bar:0"],
});
const htmlvalidate = cli.getValidator();
expect(HtmlValidate).toHaveBeenCalledWith({
extends: [],
rules: {
foo: 1,
bar: 0,
},
});
expect(htmlvalidate).toBeDefined();
});
});
});
import { readFileSync } from "fs";
import { ConfigData } from "../config";
import defaultConfig from "../config/default";
import { UserError } from "../error";
import HtmlValidate from "../htmlvalidate";
import { Report } from "../reporter";
import { expandFiles, ExpandOptions } from "./expand-files";
import { getFormatter } from "./formatter";
export interface CLIOptions {
configFile?: string;
rules?: string | string[];
}
export class CLI {
private options: CLIOptions;
private config: ConfigData;
/**
* Create new CLI helper.
*
* Can be used to create tooling with similar properties to bundled CLI
* script.
*/
public constructor(options?: CLIOptions) {
this.options = options || {};
this.config = this.getConfig();
}
public expandFiles(
patterns: string[],
options: ExpandOptions = {}
......@@ -13,4 +37,40 @@ export class CLI {
public getFormatter(formatters: string): (report: Report) => string {
return getFormatter(formatters);
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
*/
public getValidator(): HtmlValidate {
return new HtmlValidate(this.config);
}
private getConfig(): ConfigData {
const { options } = this;
const config: ConfigData = options.configFile
? JSON.parse(readFileSync(options.configFile, "utf-8"))
: defaultConfig;
let rules = options.rules;
if (rules) {
if (Array.isArray(rules)) {
rules = rules.join(",");
}
const raw = rules
.split(",")
.map((x: string) => x.replace(/ *(.*):/, '"$1":'))
.join(",");
try {
const rules = JSON.parse(`{${raw}}`);
config.extends = [];
config.rules = rules;
} catch (e) {
// istanbul ignore next
throw new UserError(
`Error while parsing --rule option "{${raw}}": ${e.message}.\n`
);
}
}
return config;
}
}
......@@ -3,7 +3,7 @@ jest.mock("glob");
import fs from "fs";
import glob from "glob";
import { expandFiles } from "./expand-files";
import { CLI } from "./cli";
declare module "fs" {
function setMockDirectories(directories: string[]): void;
......@@ -14,7 +14,10 @@ declare module "glob" {
function resetMock(): void;
}
let cli: CLI;
beforeEach(() => {
cli = new CLI();
glob.setMockFiles([
"/dev/stdin",
"foo.html",
......@@ -36,7 +39,7 @@ afterAll(() => {
describe("expandFiles()", () => {
it("should expand globs", () => {
const spy = jest.spyOn(glob, "sync");
expect(expandFiles(["foo.html", "bar/**/*.html"])).toEqual([
expect(cli.expandFiles(["foo.html", "bar/**/*.html"])).toEqual([
"foo.html",
"bar/fred.html",
"bar/barney.html",
......@@ -46,18 +49,21 @@ describe("expandFiles()", () => {
});
it("should expand directories (default extensions)", () => {
expect(expandFiles(["bar"])).toEqual(["bar/fred.html", "bar/barney.html"]);
expect(cli.expandFiles(["bar"])).toEqual([
"bar/fred.html",
"bar/barney.html",
]);
});
it("should expand directories (explicit extensions)", () => {
expect(expandFiles(["bar"], { extensions: ["js", "json"] })).toEqual([
expect(cli.expandFiles(["bar"], { extensions: ["js", "json"] })).toEqual([
"bar/fred.json",
"bar/barney.js",
]);
});
it("should expand directories (no extensions => all files)", () => {
expect(expandFiles(["bar"], { extensions: [] })).toEqual([
expect(cli.expandFiles(["bar"], { extensions: [] })).toEqual([
"bar/fred.html",
"bar/fred.json",
"bar/barney.html",
......@@ -66,10 +72,10 @@ describe("expandFiles()", () => {
});
it("should remove duplicates", () => {
expect(expandFiles(["foo.html", "foo.html"])).toEqual(["foo.html"]);
expect(cli.expandFiles(["foo.html", "foo.html"])).toEqual(["foo.html"]);
});
it("should replace - placeholder", () => {
expect(expandFiles(["-"])).toEqual(["/dev/stdin"]);
expect(cli.expandFiles(["-"])).toEqual(["/dev/stdin"]);
});
});
......@@ -39,7 +39,7 @@ function directoryPattern(extensions: string[]): string {
*/
export function expandFiles(
patterns: string[],
options: ExpandOptions = {}
options: ExpandOptions
): string[] {
const cwd = options.cwd || process.cwd();
const extensions = options.extensions || DEFAULT_EXTENSIONS;
......
import { Report } from "../reporter";
import { getFormatter } from "./formatter";
import { CLI } from "./cli";
/* all mocked formatters must return empty string */
const textFormatter = jest.fn((report: Report) => ""); // eslint-disable-line @typescript-eslint/no-unused-vars
......@@ -44,22 +44,28 @@ const report: Report = {
warningCount: 0,
};
let cli: CLI;
beforeEach(() => {
cli = new CLI();
});
describe("cli/formatters", () => {
it("should call formatter", () => {
const wrapped = getFormatter("text");
const wrapped = cli.getFormatter("text");
wrapped(report);
expect(textFormatter).toHaveBeenCalledWith(report.results);
});
it("should call multiple formatters", () => {
const wrapped = getFormatter("text,json");
const wrapped = cli.getFormatter("text,json");
wrapped(report);
expect(textFormatter).toHaveBeenCalledWith(report.results);
expect(jsonFormatter).toHaveBeenCalledWith(report.results);
});
it("should redirect output", () => {
const wrapped = getFormatter("text=foo.txt");
const wrapped = cli.getFormatter("text=foo.txt");
wrapped(report);
expect(writeFileSync).toHaveBeenCalledWith("foo.txt", "", "utf-8");
});
......
/* eslint-disable no-console, no-process-exit, sonarjs/no-duplicate-string */
import { ConfigData } from "../config";
import defaultConfig from "../config/default";
import { TokenDump } from "../engine";
import { UserError } from "../error/user-error";
import HtmlValidate from "../htmlvalidate";
import { Report, Reporter, Result } from "../reporter";
import { expandFiles } from "./expand-files";
import { getFormatter } from "./formatter";
import { eventFormatter } from "./json";
const pkg = require("../../package.json");
import chalk from "chalk";
import minimist from "minimist";
import { CLI } from "./cli";
enum Mode {
LINT,
......@@ -42,35 +38,6 @@ function getMode(argv: { [key: string]: any }): Mode {
return Mode.LINT;
}
function getGlobalConfig(
configFile: string,
rules?: string | string[]
): ConfigData {
const baseConfig: ConfigData = configFile
? require(`${process.cwd()}/${configFile}`)
: defaultConfig;
const config: any = Object.assign({}, baseConfig);
if (rules) {
if (Array.isArray(rules)) {
rules = rules.join(",");
}
const raw = rules
.split(",")
.map((x: string) => x.replace(/ *(.*):/, '"$1":'))
.join(",");
try {
const rules = JSON.parse(`{${raw}}`);
config.extends = [];
config.rules = rules;
} catch (e) {
process.stderr.write(
`Error while parsing "${rules}": ${e.message}, rules ignored.\n`
);
}
}
return config;
}
function lint(files: string[]): Report {
const reports = files.map((filename: string) => {
try {
......@@ -208,11 +175,14 @@ if (argv.help || argv._.length === 0) {
process.exit();
}
const cli = new CLI({
configFile: argv.config,
rules: argv.rule,
});
const mode = getMode(argv);
const config = getGlobalConfig(argv.config, argv.rule);
const formatter = getFormatter(argv.formatter);
const formatter = cli.getFormatter(argv.formatter);
const maxWarnings = parseInt(argv["max-warnings"] || "-1", 10);
const htmlvalidate = new HtmlValidate(config);
const htmlvalidate = cli.getValidator();
/* sanity check: ensure maxWarnings has a valid value */
if (isNaN(maxWarnings)) {
......@@ -227,7 +197,7 @@ const extensions = (argv.ext || "html").split(",").map((cur: string) => {
return cur[0] === "." ? cur.slice(1) : cur;
});
const files = expandFiles(argv._, { extensions });
const files = cli.expandFiles(argv._, { extensions });
if (files.length === 0) {
console.error("No files matching patterns", argv._);
process.exit(1);
......
......@@ -5,6 +5,11 @@ export interface TransformMap {
}
export interface ConfigData {
/**
* If set to true no new configurations will be searched.
*/
root?: boolean;
extends?: string[];
/**
......
......@@ -17,6 +17,9 @@ class MockConfig {
public static fromFile(filename: string): Config {
return Config.fromObject({
/* set root to true for if the last directory name is literal "root" */
root: path.basename(path.dirname(filename)) === "root",
mockFilenames: [filename],
});
}
......@@ -83,6 +86,20 @@ describe("ConfigLoader", () => {
);
});
it("should stop searching when root is found", () => {
jest.spyOn(fs, "existsSync").mockImplementation(() => true);
const config = loader.fromTarget("/project/root/src/target.html");
expect(config.get()).toEqual(
expect.objectContaining({
mockFilenames: [
/* ConfigMock adds all visited filenames to this array */
path.resolve("/project/root/.htmlvalidate.json"),
path.resolve("/project/root/src/.htmlvalidate.json"),
],
})
);
});
it("should load empty config for inline sources", () => {
const config = loader.fromTarget("inline");
expect(config.get()).toEqual(Config.empty().get());
......
......@@ -69,6 +69,11 @@ export class ConfigLoader {
config = local.merge(config);
}
/* stop if a configuration with "root" is set to true */
if (config.isRootFound()) {
break;
}
/* get the parent directory */
const child = current;
current = path.dirname(current);
......
......@@ -34,6 +34,12 @@ function mergeInternal(base: ConfigData, rhs: ConfigData): ConfigData {
dst.rules = deepmerge(dst.rules, rhs.rules, { arrayMerge: overwriteMerge });
}
/* root property is merged with boolean "or" since it should always be truthy
* if any config has it set. */
if (base.root || rhs.root) {
dst.root = base.root || rhs.root;
}
return dst;
}
......@@ -152,6 +158,13 @@ export class Config {
);
}
/**
* Returns true if this configuration is marked as "root".
*/
public isRootFound(): boolean {
return this.config.root;
}
/**
* Returns a new configuration as a merge of the two. Entries from the passed
* object takes priority over this object.
......
......@@ -151,6 +151,52 @@ describe("HtmlValidate", () => {
});
});
describe("getConfigFor()", () => {
it("should load configuration files and merge result", () => {
const htmlvalidate = new HtmlValidate({
rules: {
fred: "error",
},
});
const fromTarget = jest
.spyOn((htmlvalidate as any).configLoader, "fromTarget")
.mockImplementation(() =>
Config.fromObject({
rules: {
barney: "error",
},
})
);
const config = htmlvalidate.getConfigFor("my-file.html");
expect(fromTarget).toHaveBeenCalledWith("my-file.html");
expect(config.get()).toEqual(
expect.objectContaining({
rules: {
fred: "error",
barney: "error",
},
})
);
});
it("should not load configuration files if global config is root", () => {
const htmlvalidate = new HtmlValidate({
root: true,
});
const fromTarget = jest.spyOn(
(htmlvalidate as any).configLoader,
"fromTarget"
);
const config = htmlvalidate.getConfigFor("my-file.html");
expect(fromTarget).not.toHaveBeenCalled();
expect(config.get()).toEqual(
expect.objectContaining({
root: true,
})
);
});
});
it("getParserFor() should create a parser for given filename", () => {
const htmlvalidate = new HtmlValidate();
const config = Config.empty();
......
......@@ -177,6 +177,12 @@ class HtmlValidate {
* @param filename - Filename to get configuration for.
*/
public getConfigFor(filename: string): Config {
/* special case when the global configuration is marked as root, should not
* try to load and more configuration files */
if (this.globalConfig.isRootFound()) {
return this.globalConfig;
}
const config = this.configLoader.fromTarget(filename);
const merged = this.globalConfig.merge(config);
merged.init();
......
......@@ -4,7 +4,7 @@ export { default as HtmlValidate } from "./htmlvalidate";
export { AttributeData } from "./parser";
export { CLI } from "./cli/cli";
export { Config, ConfigData, ConfigLoader } from "./config";
export { DynamicValue } from "./dom/dynamic-value";
export { DynamicValue, HtmlElement } from "./dom";
export { Rule } from "./rule";
export { Source } from "./context";
export { Reporter } from "./reporter";
......