Commits (28)
# html-validate changelog
# [2.20.0](https://gitlab.com/html-validate/html-validate/compare/v2.19.0...v2.20.0) (2020-04-05)
### Bug Fixes
- **meta:** add missing null return type to MetaTable.getMetaFor ([44eac5b](https://gitlab.com/html-validate/html-validate/commit/44eac5b4efffdd0bcf6973364b595501eabe9b25))
- allow loading elements from js-file again ([5569a94](https://gitlab.com/html-validate/html-validate/commit/5569a9428cef8ca168d79a2e75be851e141838e8))
- make `ast` property private ([cb1a2c8](https://gitlab.com/html-validate/html-validate/commit/cb1a2c867583616819488102a3a46431821615a6))
### Features
- support loading custom formatters ([0b02a31](https://gitlab.com/html-validate/html-validate/commit/0b02a31c4f34cca840c9ada60e76634976461f38))
- **formatters:** use factory to load formatters to make it more webpack-friendly ([81bef6e](https://gitlab.com/html-validate/html-validate/commit/81bef6e79287884ee2a6c804cefe136e222c1b78))
# [2.19.0](https://gitlab.com/html-validate/html-validate/compare/v2.18.1...v2.19.0) (2020-03-24)
### Bug Fixes
......
const chalk = require("chalk");
const HtmlValidate = require("../../../../build/htmlvalidate").default;
const codeframe = require("../../../../build/formatters/codeframe");
const codeframe = require("../../../../build/formatters/codeframe").default;
module.exports = function generateValidationResultsProcessor(log, validateMap) {
return {
......
......@@ -44,6 +44,8 @@ Possible formats are:
Multiples formatters can be set with a comma-separated list: `--formatter stylish,checkstyle`.
Output can be redirected to a file using `name=path`: `--formatter checkstyle=result.xml`.
Custom formatters can be used by specifying a package name: `--formater my-custom-formatter`.
html-validate --formatter stylish file.html
### `--max-warnings`
......
This diff is collapsed.
{
"name": "html-validate",
"version": "2.19.0",
"version": "2.20.0",
"description": "html linter",
"keywords": [
"html",
......@@ -95,19 +95,19 @@
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.0",
"@commitlint/cli": "8.3.5",
"@html-validate/commitlint-config": "1.0.2",
"@html-validate/eslint-config": "1.2.0",
"@html-validate/prettier-config": "1.0.0",
"@html-validate/semantic-release-config": "1.0.12",
"@html-validate/commitlint-config": "1.0.3",
"@html-validate/eslint-config": "1.2.1",
"@html-validate/prettier-config": "1.0.1",
"@html-validate/semantic-release-config": "1.0.14",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.44",
"@types/glob": "7.1.1",
"@types/inquirer": "6.5.0",
"@types/jest": "25.1.4",
"@types/jest": "25.2.1",
"@types/json-merge-patch": "0.0.4",
"@types/minimist": "1.2.0",
"@types/node": "11.15.7",
"autoprefixer": "9.7.4",
"@types/node": "11.15.9",
"autoprefixer": "9.7.5",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
......@@ -116,7 +116,7 @@
"dgeni-front-matter": "1.0.2",
"dgeni-packages": "0.28.3",
"eslint-plugin-array-func": "3.1.4",
"eslint-plugin-node": "11.0.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-security": "1.4.0",
"eslint-plugin-sonarjs": "0.5.0",
"font-awesome": "4.7.0",
......@@ -130,11 +130,11 @@
"grunt-sass": "3.1.0",
"highlight.js": "9.18.1",
"husky": "4.2.3",
"jest": "25.1.0",
"jest-diff": "25.1.0",
"jest": "25.2.7",
"jest-diff": "25.2.6",
"jest-junit": "10.0.0",
"jquery": "3.4.1",
"lint-staged": "10.0.8",
"lint-staged": "10.1.1",
"load-grunt-tasks": "5.1.0",
"marked": "0.8.2",
"minimatch": "3.0.4",
......@@ -144,7 +144,7 @@
"serve-static": "1.14.1",
"stringmap": "0.2.2",
"strip-ansi": "6.0.0",
"ts-jest": "25.2.1",
"ts-jest": "25.3.1",
"typescript": "3.8.3"
},
"jest": {
......
......@@ -4,6 +4,7 @@ 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
const jsonFormatter = jest.fn((report: Report) => ""); // eslint-disable-line @typescript-eslint/no-unused-vars
const customFormatter = jest.fn((report: Report) => ""); // eslint-disable-line @typescript-eslint/no-unused-vars
jest.mock("../formatters/text", () => {
return (report: Report) => textFormatter(report);
......@@ -13,6 +14,14 @@ jest.mock("../formatters/json", () => {
return (report: Report) => jsonFormatter(report);
});
jest.mock(
"custom-formatter",
() => {
return (report: Report) => customFormatter(report);
},
{ virtual: true }
);
const fs = {
existsSync: jest.fn().mockReturnValue(true),
mkdirSync: jest.fn(),
......@@ -74,6 +83,13 @@ describe("cli/formatters", () => {
expect(jsonFormatter).toHaveBeenCalledWith(report.results);
});
it("should call custom formatter", () => {
expect.assertions(1);
const wrapped = cli.getFormatter("custom-formatter");
wrapped(report);
expect(customFormatter).toHaveBeenCalledWith(report.results);
});
it("should redirect output to file", () => {
expect.assertions(2);
const wrapped = cli.getFormatter("text=foo.txt");
......@@ -90,4 +106,11 @@ describe("cli/formatters", () => {
expect(fs.mkdirSync).toHaveBeenCalledWith("mydir", { recursive: true });
expect(fs.writeFileSync).toHaveBeenCalledWith("mydir/foo.txt", "", "utf-8");
});
it("should throw error when formatter is missing", () => {
expect.assertions(1);
expect(() => cli.getFormatter("missing")).toThrow(
'No formatter named "missing"'
);
});
});
import fs from "fs";
import path from "path";
import { Formatter } from "../formatters";
import { getFormatter as formatterFactory, Formatter } from "../formatters";
import { Report, Result } from "../reporter";
import { UserError } from "../error";
type WrappedFormatter = (results: Result[]) => string;
......@@ -24,13 +25,25 @@ function wrap(
};
}
function loadFormatter(name: string): Formatter {
const fn = formatterFactory(name);
if (fn) {
return fn;
}
try {
/* eslint-disable-next-line import/no-dynamic-require */
return require(name);
} catch (error) {
throw new UserError(`No formatter named "${name}"`, error);
}
}
export function getFormatter(formatters: string): (report: Report) => string {
const fn: WrappedFormatter[] = formatters.split(",").map((cur) => {
const [name, dst] = cur.split("=", 2);
const moduleName = name.replace(/[^a-z]+/g, "");
/* eslint-disable-next-line import/no-dynamic-require */
const formatter = require(`../formatters/${moduleName}`);
return wrap(formatter, dst);
const fn = loadFormatter(name);
return wrap(fn, dst);
});
return (report: Report) => {
return fn
......
......@@ -8,7 +8,7 @@ export type ProcessAttributeCallback = (
) => Iterable<AttributeData>;
export interface ProcessElementContext {
getMetaFor(tagName: string): MetaElement;
getMetaFor(tagName: string): MetaElement | null;
}
export type ProcessElementCallback = (
......
import { Message, Result } from "../reporter";
import { FormatterModule } from ".";
import { Formatter } from "./formatter";
const entities: { [key: string]: string } = {
">": "&gt;",
......@@ -27,7 +27,7 @@ function getMessageType(message: Message): "error" | "warning" {
}
}
export default function checkstyleFormatter(results: Result[]): string {
function checkstyleFormatter(results: Result[]): string {
let output = "";
output += `<?xml version="1.0" encoding="utf-8"?>`;
......@@ -61,5 +61,5 @@ export default function checkstyleFormatter(results: Result[]): string {
return output;
}
declare const module: FormatterModule;
module.exports = checkstyleFormatter;
const formatter: Formatter = checkstyleFormatter;
export default formatter;
import path from "path";
import { codeFrameColumns } from "@babel/code-frame";
import { Message, Result } from "../reporter";
import { FormatterModule } from ".";
import { Formatter } from "./formatter";
import chalk = require("chalk");
......@@ -14,8 +14,6 @@ interface SourcePoint {
* Codeframe formatter based on ESLint codeframe.
*/
declare const module: FormatterModule;
/**
* Given a word and a count, append an s if count is not one.
* @param {string} word A word in its singular form.
......@@ -137,7 +135,7 @@ function formatSummary(errors: number, warnings: number): string {
return chalk[summaryColor].bold(`${summary.join(" and ")} found.`);
}
export default function codeframe(results: Result[]): string {
function codeframe(results: Result[]): string {
let errors = 0;
let warnings = 0;
......@@ -165,4 +163,5 @@ export default function codeframe(results: Result[]): string {
return errors + warnings > 0 ? output : "";
}
module.exports = codeframe;
const formatter: Formatter = codeframe;
export default formatter;
import { Result } from "../reporter";
export type Formatter = (results: Result[]) => string;
import { Result } from "../reporter";
import checkstyle from "./checkstyle";
import codeframe from "./codeframe";
import json from "./json";
import stylish from "./stylish";
import text from "./text";
import { Formatter } from "./formatter";
export type Formatter = (results: Result[]) => string;
export { Formatter } from "./formatter";
export interface FormatterModule {
exports: Formatter;
const availableFormatters: Record<string, Formatter> = {
checkstyle,
codeframe,
json,
stylish,
text,
};
/**
* Get formatter function by name.
*
* @param name - Name of formatter.
* @returns Formatter function or null if it doesn't exist.
*/
export function getFormatter(name: string): Formatter | null {
return availableFormatters[name] ?? null;
}
import { Result } from "../reporter";
import { FormatterModule } from ".";
import { Formatter } from "./formatter";
export default function jsonFormatter(results: Result[]): string {
function jsonFormatter(results: Result[]): string {
return JSON.stringify(results);
}
declare const module: FormatterModule;
module.exports = jsonFormatter;
const formatter: Formatter = jsonFormatter;
export default formatter;
import { FormatterModule } from ".";
import { Formatter } from "./formatter";
const stylish = require("eslint/lib/cli-engine/formatters/stylish");
export default stylish;
declare const module: FormatterModule;
module.exports = stylish;
const formatter: Formatter = stylish;
export default formatter;
import { Result } from "../reporter";
import { FormatterModule } from ".";
import { Formatter } from "./formatter";
export default function textFormatter(results: Result[]): string {
function textFormatter(results: Result[]): string {
let output = "";
let total = 0;
......@@ -33,5 +33,5 @@ export default function textFormatter(results: Result[]): string {
return total > 0 ? output : "";
}
declare const module: FormatterModule;
module.exports = textFormatter;
const formatter: Formatter = textFormatter;
export default formatter;
import path from "path";
/* mock ajv for easier testing of errors and to allow invalid values though the
* validation to ensure the code works anyway */
interface Validate {
......@@ -94,6 +96,49 @@ describe("MetaTable", () => {
expect(table.getMetaFor("$schema")).toBeNull();
});
describe("should load metadata from", () => {
const fileDir = path.resolve(__dirname, "../../test-files/meta");
it("json file", () => {
expect.assertions(1);
const table = new MetaTable();
const filename = path.join(fileDir, "elements-json.json");
table.loadFromFile(filename);
expect(table.getMetaFor("foo")).toMatchInlineSnapshot(`
Object {
"flow": true,
"tagName": "foo",
}
`);
});
it("js file", () => {
expect.assertions(1);
const table = new MetaTable();
const filename = path.join(fileDir, "elements-js.js");
table.loadFromFile(filename);
expect(table.getMetaFor("foo")).toMatchInlineSnapshot(`
Object {
"flow": true,
"tagName": "foo",
}
`);
});
it("js without extension", () => {
expect.assertions(1);
const table = new MetaTable();
const filename = path.join(fileDir, "elements-js");
table.loadFromFile(filename);
expect(table.getMetaFor("foo")).toMatchInlineSnapshot(`
Object {
"flow": true,
"tagName": "foo",
}
`);
});
});
describe("getMetaFor", () => {
let table: MetaTable;
......
import fs from "fs";
import Ajv from "ajv";
import deepmerge from "deepmerge";
import jsonMergePatch from "json-merge-patch";
......@@ -137,17 +136,21 @@ export class MetaTable {
* Load metadata table from filename
*/
public loadFromFile(filename: string): void {
let json;
try {
const data = fs.readFileSync(filename, "utf-8");
json = JSON.parse(data);
/* remove cached copy so we always load a fresh copy, important for
* editors which keep a long-running instance of [[HtmlValidate]]
* around. */
delete require.cache[require.resolve(filename)];
/* load using require as it can process both js and json */
const data = require(filename); // eslint-disable-line import/no-dynamic-require
this.loadFromObject(data, filename);
} catch (err) {
throw new UserError(
`Failed to load element metadata from "${filename}"`,
err
);
}
this.loadFromObject(clone(json), filename);
}
/**
......@@ -155,7 +158,7 @@ export class MetaTable {
*
* @returns A shallow copy of metadata.
*/
public getMetaFor(tagName: string): MetaElement {
public getMetaFor(tagName: string): MetaElement | null {
tagName = tagName.toLowerCase();
return this.elements[tagName]
? Object.assign({}, this.elements[tagName])
......
......@@ -21,7 +21,7 @@ import {
WhitespaceEvent,
} from "../event";
import { Lexer, Token, TokenStream, TokenType } from "../lexer";
import { MetaTable } from "../meta";
import { MetaTable, MetaElement } from "../meta";
import { AttributeData } from "./attribute-data";
import { parseConditionalComment } from "./conditional-comment";
import { ParserError } from "./parser-error";
......@@ -294,7 +294,7 @@ export class Parser {
const processElement = source.hooks.processElement;
const metaTable = this.metaTable;
const context: ProcessElementContext = {
getMetaFor(tagName: string) {
getMetaFor(tagName: string): MetaElement | null {
return metaTable.getMetaFor(tagName);
},
};
......
......@@ -142,7 +142,8 @@ function compareKey(
}
export class TemplateExtractor {
protected ast: ESTree.Program;
private ast: ESTree.Program;
private filename: string;
private data: string;
......
module.exports = {
foo: {
flow: true,
},
};
{
"foo": {
"flow": true
}
}