...
 
Commits (23)
# html-validate changelog
# [1.15.0](https://gitlab.com/html-validate/html-validate/compare/v1.14.1...v1.15.0) (2019-11-03)
### Bug Fixes
- **cli:** `--help` does not take an argument ([e22293f](https://gitlab.com/html-validate/html-validate/commit/e22293fc3257f6ba9732016d2be44214299e23c2))
### Features
- **cli:** add `--dump-source` to debug transformers ([4d32a0d](https://gitlab.com/html-validate/html-validate/commit/4d32a0d6fc8e3caaa62107affa94fe0fe16aab1f))
- **cli:** add `--init` to create initial configuration ([6852d30](https://gitlab.com/html-validate/html-validate/commit/6852d30dcbccc5ebed3267c6dd181146156646f0))
## [1.14.1](https://gitlab.com/html-validate/html-validate/compare/v1.14.0...v1.14.1) (2019-10-27)
### Bug Fixes
......
......@@ -118,6 +118,15 @@ filesystem. Set the `root` property to `true` to prevent this behaviour:
}
```
### `--init`
Initialize project with a new configuration.
html-validate --init
The new configuration will be written to the current directory (in
`.htmlvalidate.json`).
### `--print-config`
Instead of validating file print the configuration generated.
......
This diff is collapsed.
{
"name": "html-validate",
"version": "1.14.1",
"version": "1.15.0",
"description": "html linter",
"keywords": [
"html",
......@@ -108,6 +108,7 @@
"eslint": "^6.0.0",
"espree": "^6.0.0",
"glob": "^7.1.3",
"inquirer": "^7.0.0",
"json-merge-patch": "^0.2.3",
"minimist": "^1.2.0"
},
......@@ -120,29 +121,30 @@
"@semantic-release/exec": "3.3.8",
"@semantic-release/git": "7.0.17",
"@semantic-release/gitlab": "4.0.3",
"@semantic-release/npm": "5.3.1",
"@semantic-release/npm": "5.3.2",
"@semantic-release/release-notes-generator": "7.3.2",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.39",
"@types/glob": "7.1.1",
"@types/jest": "24.0.19",
"@types/inquirer": "6.5.0",
"@types/jest": "24.0.21",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.15.0",
"@typescript-eslint/eslint-plugin": "2.5.0",
"@typescript-eslint/parser": "2.5.0",
"autoprefixer": "9.7.0",
"@types/node": "11.15.2",
"@typescript-eslint/eslint-plugin": "2.6.0",
"@typescript-eslint/parser": "2.6.0",
"autoprefixer": "9.7.1",
"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.4.0",
"eslint-config-prettier": "6.5.0",
"eslint-config-sidvind": "1.3.2",
"eslint-plugin-array-func": "3.1.3",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "22.20.0",
"eslint-plugin-jest": "23.0.2",
"eslint-plugin-node": "10.0.0",
"eslint-plugin-prettier": "3.1.1",
"eslint-plugin-security": "1.4.0",
......@@ -155,17 +157,17 @@
"grunt-contrib-copy": "1.0.0",
"grunt-postcss": "0.9.0",
"grunt-sass": "3.1.0",
"highlight.js": "9.15.10",
"highlight.js": "9.16.2",
"husky": "3.0.9",
"jest": "24.9.0",
"jest-diff": "24.9.0",
"jest-junit": "8.0.0",
"jest-junit": "9.0.0",
"jquery": "3.4.1",
"lint-staged": "9.4.2",
"load-grunt-tasks": "5.1.0",
"minimatch": "3.0.4",
"prettier": "1.18.2",
"sass": "1.23.1",
"sass": "1.23.3",
"semantic-release": "15.13.28",
"serve-static": "1.14.1",
"strip-ansi": "5.2.0",
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`should generate configuration for angularjs 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.js$\\": \\"html-validate-angular/js\\",
\\"^.*\\\\\\\\.html$\\": \\"html-validate-angular/html\\"
}
}"
`;
exports[`should generate configuration for combined 1`] = `
"{
\\"elements\\": [
\\"html5\\",
\\"html-validate-vue/elements\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.js$\\": \\"html-validate-angular/js\\",
\\"^.*\\\\\\\\.html$\\": \\"html-validate-angular/html\\",
\\"^.*\\\\\\\\.vue$\\": \\"html-validate-vue\\",
\\"^.*\\\\\\\\.md$\\": \\"html-validate-markdown\\"
}
}"
`;
exports[`should generate configuration for default 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
]
}"
`;
exports[`should generate configuration for markdown 1`] = `
"{
\\"elements\\": [
\\"html5\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.md$\\": \\"html-validate-markdown\\"
}
}"
`;
exports[`should generate configuration for vuejs 1`] = `
"{
\\"elements\\": [
\\"html5\\",
\\"html-validate-vue/elements\\"
],
\\"extends\\": [
\\"htmlvalidate:recommended\\"
],
\\"transform\\": {
\\"^.*\\\\\\\\.vue$\\": \\"html-validate-vue\\"
}
}"
`;
......@@ -6,6 +6,7 @@ import HtmlValidate from "../htmlvalidate";
import { Report } from "../reporter";
import { expandFiles, ExpandOptions } from "./expand-files";
import { getFormatter } from "./formatter";
import { init, InitResult } from "./init";
export interface CLIOptions {
configFile?: string;
......@@ -38,6 +39,16 @@ export class CLI {
return getFormatter(formatters);
}
/**
* Initialize project with a new configuration.
*
* A new `.htmlvalidate.json` file will be placed in the path provided by
* `cwd`.
*/
public init(cwd: string): Promise<InitResult> {
return init(cwd);
}
/**
* Get HtmlValidate instance with configuration based on options passed to the
* constructor.
......
......@@ -12,17 +12,27 @@ import { CLI } from "./cli";
enum Mode {
LINT,
INIT,
DUMP_EVENTS,
DUMP_TOKENS,
DUMP_TREE,
DUMP_SOURCE,
PRINT_CONFIG,
}
function getMode(argv: { [key: string]: any }): Mode {
if (argv.init) {
return Mode.INIT;
}
if (argv["dump-events"]) {
return Mode.DUMP_EVENTS;
}
if (argv["dump-source"]) {
return Mode.DUMP_SOURCE;
}
if (argv["dump-tokens"]) {
return Mode.DUMP_TOKENS;
}
......@@ -69,6 +79,11 @@ function dump(files: string[], mode: Mode): string {
case Mode.DUMP_TREE:
lines = files.map((filename: string) => htmlvalidate.dumpTree(filename));
break;
case Mode.DUMP_SOURCE:
lines = files.map((filename: string) =>
htmlvalidate.dumpSource(filename)
);
break;
default:
throw new Error(`Unknown mode "${mode}"`);
}
......@@ -92,16 +107,18 @@ const argv: minimist.ParsedArgs = minimist(process.argv.slice(2), {
"ext",
"f",
"formatter",
"h",
"help",
"max-warnings",
"rule",
"stdin-filename",
],
boolean: [
"init",
"dump-events",
"dump-source",
"dump-tokens",
"dump-tree",
"h",
"help",
"print-config",
"stdin",
"version",
......@@ -136,17 +153,19 @@ Common options:
--stdin process markup from stdin.
--stdin-filename=STRING specify filename to report when using stdin
Debugging options:
--dump-events output events during parsing.
--dump-tokens output tokens from lexing stage.
--dump-tree output nodes from the dom tree.
Miscellaneous:
-c, --config=STRING use custom configuration file.
--init initialize project with a new configuration
--print-config output configuration for given file.
-h, --help show help.
--version show version.
Debugging options:
--dump-events output events during parsing.
--dump-source output post-transformed source data.
--dump-tokens output tokens from lexing stage.
--dump-tree output nodes from the dom tree.
Formatters:
Multiple formatters can be specified with a comma-separated list,
......@@ -170,7 +189,7 @@ if (argv.version) {
process.exit();
}
if (argv.help || argv._.length === 0) {
if (argv.help || (argv._.length === 0 && !argv.init)) {
showUsage();
process.exit();
}
......@@ -198,7 +217,7 @@ const extensions = (argv.ext || "html").split(",").map((cur: string) => {
});
const files = cli.expandFiles(argv._, { extensions });
if (files.length === 0) {
if (files.length === 0 && mode !== Mode.INIT) {
console.error("No files matching patterns", argv._);
process.exit(1);
}
......@@ -222,6 +241,18 @@ try {
}
process.exit(result.valid ? 0 : 1);
} else if (mode === Mode.INIT) {
cli
.init(process.cwd())
.then(result => {
console.log(`Configuration written to "${result.filename}"`);
})
.catch(err => {
if (err) {
console.error(err);
}
process.exit(1);
});
} else if (mode === Mode.PRINT_CONFIG) {
const config = htmlvalidate.getConfigFor(files[0]);
const json = JSON.stringify(config.get(), null, 2);
......
const fs = {
existsSync: jest.fn(),
writeFile: jest.fn().mockImplementation((fn, data, cb) => cb()),
};
const inquirer = {
prompt: jest.fn(),
};
jest.mock("inquirer", () => inquirer);
jest.mock("fs", () => fs);
import { CLI } from "./cli";
let cli: CLI;
beforeEach(() => {
jest.clearAllMocks();
cli = new CLI();
});
it.each([
["default", []],
["angularjs", ["AngularJS"]],
["vuejs", ["Vue.js"]],
["markdown", ["Markdown"]],
["combined", ["AngularJS", "Vue.js", "Markdown"]],
])("should generate configuration for %s", async (name, frameworks) => {
expect.assertions(2);
fs.existsSync.mockReturnValue(false);
inquirer.prompt.mockResolvedValue({
write: true,
frameworks,
});
await cli.init(".");
expect(fs.writeFile).toHaveBeenCalledWith(
"./.htmlvalidate.json",
expect.anything(),
expect.anything()
);
expect(fs.writeFile.mock.calls[0][1]).toMatchSnapshot();
});
it("should not overwrite configuration unless requested", async () => {
expect.assertions(1);
fs.existsSync.mockReturnValue(true);
inquirer.prompt.mockResolvedValue({
write: false,
});
try {
await cli.init(".");
} catch (err) {
/* do nothing */
}
expect(fs.writeFile).not.toHaveBeenCalled();
});
it("should propagate errors from fs.writeFile", async () => {
expect.assertions(1);
fs.existsSync.mockReturnValue(false);
fs.writeFile.mockImplementationOnce((fn, data, cb) => cb("mock error"));
inquirer.prompt.mockResolvedValue({
write: true,
frameworks: [],
});
await expect(cli.init(".")).rejects.toEqual("mock error");
});
import deepmerge from "deepmerge";
import fs from "fs";
import inquirer from "inquirer";
import { ConfigData } from "../config";
export interface InitResult {
filename: string;
}
export enum Frameworks {
angularjs = "AngularJS",
vuejs = "Vue.js",
markdown = "Markdown",
}
const frameworkConfig: Record<string, ConfigData> = {
[Frameworks.angularjs]: {
transform: {
"^.*\\.js$": "html-validate-angular/js",
"^.*\\.html$": "html-validate-angular/html",
},
},
[Frameworks.vuejs]: {
elements: ["html-validate-vue/elements"],
transform: {
"^.*\\.vue$": "html-validate-vue",
},
},
[Frameworks.markdown]: {
transform: {
"^.*\\.md$": "html-validate-markdown",
},
},
};
function addFrameworks(src: ConfigData, frameworks: string[]): ConfigData {
let config = src;
for (const framework of frameworks) {
config = deepmerge(config, frameworkConfig[framework]);
}
return config;
}
function writeConfig(dst: string, config: ConfigData): Promise<void> {
return new Promise((resolve, reject) => {
fs.writeFile(dst, JSON.stringify(config, null, 2), err => {
if (err) reject(err);
resolve();
});
});
}
export async function init(cwd: string): Promise<InitResult> {
const filename = `${cwd}/.htmlvalidate.json`;
const exists = fs.existsSync(filename);
const initialConfig: ConfigData = {
elements: ["html5"],
extends: ["htmlvalidate:recommended"],
};
const when = /* istanbul ignore next */ (answers: any): boolean => {
return !exists || answers.write;
};
const questions: inquirer.QuestionCollection = [
{
name: "write",
type: "confirm",
default: false,
when: exists,
message:
"A .htmlvalidate.json file already exists, do you want to overwrite it?",
},
{
name: "frameworks",
type: "checkbox",
choices: [Frameworks.angularjs, Frameworks.vuejs, Frameworks.markdown],
message: "Support additional frameworks?",
when,
},
];
/* prompt user for questions */
const answers = await inquirer.prompt(questions);
if (!answers.write) {
return Promise.reject();
}
/* write configuration to file */
let config = initialConfig;
config = addFrameworks(config, answers.frameworks);
await writeConfig(filename, config);
return {
filename,
};
}
......@@ -277,6 +277,41 @@ describe("HtmlValidate", () => {
]);
});
it("dumpSources() should dump sources", () => {
const htmlvalidate = new HtmlValidate();
const filename = "foo.html";
const config = Config.empty();
config.init();
config.transform = jest.fn((filename: string) => [
{
column: 1,
data: `first markup`,
filename,
line: 1,
},
{
column: 3,
data: `second markup`,
filename,
line: 5,
},
]);
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(() => config);
const output = htmlvalidate.dumpSource(filename);
expect(output).toMatchInlineSnapshot(`
Array [
"Source foo.html@1:1",
"---",
"first markup",
"---",
"Source foo.html@5:3",
"---",
"second markup",
"---",
]
`);
});
it("getRuleDocumentation() should delegate call to engine", () => {
const htmlvalidate = new HtmlValidate();
const config = Config.empty();
......
......@@ -135,6 +135,31 @@ class HtmlValidate {
return engine.dumpTree(source);
}
/**
* Transform filename and output source data.
*
* Using CLI this is enabled with `--dump-source`. Mostly useful for
* debugging.
*
* @param filename - Filename to dump source from.
*/
public dumpSource(filename: string): string[] {
const config = this.getConfigFor(filename);
const sources = config.transform(filename);
return sources.reduce(
(result: string[], source: Source) => {
result.push(
`Source ${source.filename}@${source.line}:${source.column}`
);
result.push("---");
result = result.concat(source.data.split("\n"));
result.push("---");
return result;
},
[] as string[]
);
}
/**
* Get contextual documentation for the given rule.
*
......
......@@ -12,7 +12,7 @@ interface TokenMatcher {
declare global {
namespace jest {
interface Matchers<R> {
interface Matchers<R, T> {
toBeValid(): R;
toBeInvalid(): R;
toBeToken(expected: TokenMatcher): R;
......
......@@ -15,6 +15,9 @@ it("MetaValidationError should pretty-print validation errors ", () => {
} catch (err) {
if (err instanceof MetaValidationError) {
const output = (err.prettyError() as unknown) as string;
/* cannot test prettyError() method with builtin helpers */
/* eslint-disable-next-line jest/no-try-expect */
expect(stripAnsi(output)).toMatchSnapshot();
}
}
......
......@@ -178,29 +178,29 @@ describe("parser", () => {
});
it("with text node", () => {
parser.parseHtml("<p>Lorem ipsum</p>");
expect(() => parser.parseHtml("<p>Lorem ipsum</p>")).not.toThrow();
});
it("with trailing text", () => {
parser.parseHtml("<p>Lorem ipsum</p>\n");
expect(() => parser.parseHtml("<p>Lorem ipsum</p>\n")).not.toThrow();
});
it("unclosed", () => {
parser.parseHtml("<p>");
expect(() => parser.parseHtml("<p>")).not.toThrow();
});
it("unopened", () => {
parser.parseHtml("</p>");
expect(() => parser.parseHtml("</p>")).not.toThrow();
});
it("multiple unopened", () => {
/* mostly for regression testing: root element should never be
* popped from node stack. */
parser.parseHtml("</p></p></p></p></p></p>");
expect(() => parser.parseHtml("</p></p></p></p></p></p>")).not.toThrow();
});
it("with only text", () => {
parser.parseHtml("Lorem ipsum");
expect(() => parser.parseHtml("Lorem ipsum")).not.toThrow();
});
it("with newlines", () => {
......