...
 
Commits (11)
# html-validate changelog
# [2.4.0](https://gitlab.com/html-validate/html-validate/compare/v2.3.0...v2.4.0) (2019-12-01)
### Bug Fixes
- **config:** `init` can now safely be called multiple times ([ed46c19](https://gitlab.com/html-validate/html-validate/commit/ed46c19ef8c3f8a01a5db51f0a879f10fde597a4))
- **htmlvalidate:** initialize global config if needed ([6d05747](https://gitlab.com/html-validate/html-validate/commit/6d05747de0114b72188955a8c2a11f3816dfdc6d))
### Features
- **htmlvalidate:** retain `offset` when yielding multiple sources ([fe1705e](https://gitlab.com/html-validate/html-validate/commit/fe1705e13950c0bbb281e1806432b12d3eebed1a))
- **transform:** add `offsetToLineColumn` helper ([1e61d00](https://gitlab.com/html-validate/html-validate/commit/1e61d001fcd29d434bd2d68a7e7d9a8a12feea5b))
# [2.3.0](https://gitlab.com/html-validate/html-validate/compare/v2.2.0...v2.3.0) (2019-11-27)
### Bug Fixes
......
......@@ -90,6 +90,31 @@ const te = TemplateExtractor.fromFilename("my-file.js");
const source = te.extractObjectProperty("template");
```
## Source
The validator engine works on a `Source` object.
A transformer take a source object as argument and returns zero or more new sources.
```typescript
export interface Source {
data: string;
filename: string;
line: number;
column: number;
offset: number;
originalData?: string;
hooks?: SourceHooks;
}
```
The `data` property is the markup/source code for the source object.
Since the `data` property might be only a small part of the original file there is also the related property `originalData` which is the full markup/source code of the file.
`line`, `column` and `offset` is the starting location of the `data` relative to `originalData`.
Line and column start at 1 and offset start at 0.
`filename` is always the original filename.
If the transformer wants to apply hooks for later processing they are set directly on the `hooks` property.
Hooks should only be added in the last transformer, if chaining is used the hook might be overwritten or ignored.
## Source hooks
Transformers can add hooks for additional processing by setting `source.hooks`:
......
{
"name": "html-validate",
"version": "2.3.0",
"version": "2.4.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -4446,9 +4446,9 @@
"dev": true
},
"@types/estree": {
"version": "0.0.39",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
"integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==",
"version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.40.tgz",
"integrity": "sha512-p3KZgMto/JyxosKGmnLDJ/dG5wf+qTRMUjHJcspC2oQKa4jP7mz+tv0ND56lLBu3ojHlhzY33Ol+khLyNmilkA==",
"dev": true
},
"@types/events": {
......@@ -4736,13 +4736,13 @@
}
},
"@typescript-eslint/experimental-utils": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz",
"integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.9.0.tgz",
"integrity": "sha512-0lOLFdpdJsCMqMSZT7l7W2ta0+GX8A3iefG3FovJjrX+QR8y6htFlFdU7aOVPL6pDvt6XcsOb8fxk5sq+girTw==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "2.7.0",
"@typescript-eslint/typescript-estree": "2.9.0",
"eslint-scope": "^5.0.0"
},
"dependencies": {
......@@ -4850,13 +4850,14 @@
}
},
"@typescript-eslint/typescript-estree": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz",
"integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.9.0.tgz",
"integrity": "sha512-v6btSPXEWCP594eZbM+JCXuFoXWXyF/z8kaSBSdCb83DF+Y7+xItW29SsKtSULgLemqJBT+LpT+0ZqdfH7QVmA==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"glob": "^7.1.4",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
"semver": "^6.3.0",
......@@ -4872,6 +4873,12 @@
"ms": "^2.1.1"
}
},
"eslint-visitor-keys": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
"dev": true
},
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
......@@ -5590,13 +5597,13 @@
"dev": true
},
"autoprefixer": {
"version": "9.7.2",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.2.tgz",
"integrity": "sha512-LCAfcdej1182uVvPOZnytbq61AhnOZ/4JelDaJGDeNwewyU1AMaNthcHsyz1NRjTmd2FkurMckLWfkHg3Z//KA==",
"version": "9.7.3",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.7.3.tgz",
"integrity": "sha512-8T5Y1C5Iyj6PgkPSFd0ODvK9DIleuPKUPYniNxybS47g2k2wFgLZ46lGQHlBuGKIAEV8fbCDfKCCRS1tvOgc3Q==",
"dev": true,
"requires": {
"browserslist": "^4.7.3",
"caniuse-lite": "^1.0.30001010",
"browserslist": "^4.8.0",
"caniuse-lite": "^1.0.30001012",
"chalk": "^2.4.2",
"normalize-range": "^0.1.2",
"num2fraction": "^1.2.2",
......@@ -5614,20 +5621,20 @@
}
},
"browserslist": {
"version": "4.7.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.7.3.tgz",
"integrity": "sha512-jWvmhqYpx+9EZm/FxcZSbUZyDEvDTLDi3nSAKbzEkyWvtI0mNSmUosey+5awDW1RUlrgXbQb5A6qY1xQH9U6MQ==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.8.0.tgz",
"integrity": "sha512-HYnxc/oLRWvJ3TsGegR0SRL/UDnknGq2s/a8dYYEO+kOQ9m9apKoS5oiathLKZdh/e9uE+/J3j92qPlGD/vTqA==",
"dev": true,
"requires": {
"caniuse-lite": "^1.0.30001010",
"electron-to-chromium": "^1.3.306",
"node-releases": "^1.1.40"
"caniuse-lite": "^1.0.30001012",
"electron-to-chromium": "^1.3.317",
"node-releases": "^1.1.41"
}
},
"caniuse-lite": {
"version": "1.0.30001010",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001010.tgz",
"integrity": "sha512-RA5GH9YjFNea4ZQszdWgh2SC+dpLiRAg4VDQS2b5JRI45OxmbGrYocYHTa9x0bKMQUE7uvHkNPNffUr+pCxSGw==",
"version": "1.0.30001012",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001012.tgz",
"integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==",
"dev": true
},
"chalk": {
......@@ -5639,29 +5646,18 @@
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
"supports-color": "^5.3.0"
},
"dependencies": {
"supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"electron-to-chromium": {
"version": "1.3.306",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.306.tgz",
"integrity": "sha512-frDqXvrIROoYvikSKTIKbHbzO6M3/qC6kCIt/1FOa9kALe++c4VAJnwjSFvf1tYLEUsP2n9XZ4XSCyqc3l7A/A==",
"version": "1.3.319",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.319.tgz",
"integrity": "sha512-t/lYNZPwS9jLJ9SBLGd6ERYtCtsYPAXzsE1VYLshrUWpQCTAswO1pERZV4iOZipW2uVsGQrJtm2iWiYVp1zTZw==",
"dev": true
},
"node-releases": {
"version": "1.1.40",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.40.tgz",
"integrity": "sha512-r4LPcC5b/bS8BdtWH1fbeK88ib/wg9aqmg6/s3ngNLn2Ewkn/8J6Iw3P9RTlfIAdSdvYvQl2thCY5Y+qTAQ2iQ==",
"version": "1.1.41",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.41.tgz",
"integrity": "sha512-+IctMa7wIs8Cfsa8iYzeaLTFwv5Y4r5jZud+4AnfymzeEXKBCavFX0KBgzVaPVqf0ywa6PrO8/b+bPqdwjGBSg==",
"dev": true,
"requires": {
"semver": "^6.3.0"
......@@ -5676,6 +5672,17 @@
"chalk": "^2.4.2",
"source-map": "^0.6.1",
"supports-color": "^6.1.0"
},
"dependencies": {
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
}
}
},
"postcss-value-parser": {
......@@ -5691,9 +5698,9 @@
"dev": true
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
......@@ -9497,9 +9504,9 @@
}
},
"eslint-plugin-jest": {
"version": "23.0.4",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.0.4.tgz",
"integrity": "sha512-OaP8hhT8chJNodUPvLJ6vl8gnalcsU/Ww1t9oR3HnGdEWjm/DdCCUXLOral+IPGAeWu/EwgVQCK/QtxALpH1Yw==",
"version": "23.0.5",
"resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.0.5.tgz",
"integrity": "sha512-etxXrWsFWzxsrxKwJnFC38uppH/vlJ3oF7Wmp/cxedqxRIxVhXup8e5y5MmtVXelevgxrgA1QS1vo8j889iK5Q==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "^2.5.0"
......@@ -15520,9 +15527,9 @@
"dev": true
},
"lint-staged": {
"version": "9.4.3",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.4.3.tgz",
"integrity": "sha512-PejnI+rwOAmKAIO+5UuAZU9gxdej/ovSEOAY34yMfC3OS4Ac82vCBPzAWLReR9zCPOMqeVwQRaZ3bUBpAsaL2Q==",
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-9.5.0.tgz",
"integrity": "sha512-nawMob9cb/G1J98nb8v3VC/E8rcX1rryUYXVZ69aT9kde6YWX+uvNOEHY5yf2gcWcTJGiD0kqXmCnS3oD75GIA==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
......@@ -15712,9 +15719,9 @@
}
},
"path-key": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz",
"integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==",
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true
},
"shebang-command": {
......@@ -15751,9 +15758,9 @@
}
},
"which": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz",
"integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==",
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"requires": {
"isexe": "^2.0.0"
......
{
"name": "html-validate",
"version": "2.3.0",
"version": "2.4.0",
"description": "html linter",
"keywords": [
"html",
......@@ -147,7 +147,7 @@
"@semantic-release/npm": "5.3.4",
"@semantic-release/release-notes-generator": "7.3.4",
"@types/babel__code-frame": "7.0.1",
"@types/estree": "0.0.39",
"@types/estree": "0.0.40",
"@types/glob": "7.1.1",
"@types/inquirer": "6.5.0",
"@types/jest": "24.0.23",
......@@ -156,7 +156,7 @@
"@types/node": "11.15.3",
"@typescript-eslint/eslint-plugin": "2.9.0",
"@typescript-eslint/parser": "2.9.0",
"autoprefixer": "9.7.2",
"autoprefixer": "9.7.3",
"babelify": "10.0.0",
"bootstrap-sass": "3.4.1",
"canonical-path": "1.0.0",
......@@ -167,7 +167,7 @@
"eslint-config-sidvind": "1.3.2",
"eslint-plugin-array-func": "3.1.3",
"eslint-plugin-import": "2.18.2",
"eslint-plugin-jest": "23.0.4",
"eslint-plugin-jest": "23.0.5",
"eslint-plugin-node": "10.0.0",
"eslint-plugin-prettier": "3.1.1",
"eslint-plugin-security": "1.4.0",
......@@ -186,7 +186,7 @@
"jest-diff": "24.9.0",
"jest-junit": "9.0.0",
"jquery": "3.4.1",
"lint-staged": "9.4.3",
"lint-staged": "9.5.0",
"load-grunt-tasks": "5.1.0",
"minimatch": "3.0.4",
"prettier": "1.19.1",
......
......@@ -338,6 +338,7 @@ describe("config", () => {
data: "original data",
line: 2,
column: 3,
offset: 4,
};
});
......@@ -355,6 +356,7 @@ describe("config", () => {
"data": "transformed source (was: original data)",
"filename": "/path/to/test.foo",
"line": 1,
"offset": 0,
"originalData": "original data",
},
]
......@@ -410,6 +412,7 @@ describe("config", () => {
"data": "transformed from foo.unnamed",
"filename": "foo.unnamed",
"line": 2,
"offset": 4,
},
]
`);
......@@ -425,6 +428,7 @@ describe("config", () => {
"data": "transformed from bar.named",
"filename": "bar.named",
"line": 2,
"offset": 4,
},
]
`);
......@@ -440,6 +444,7 @@ describe("config", () => {
"data": "transformed source (was: original data)",
"filename": "bar.nonplugin",
"line": 1,
"offset": 0,
"originalData": "original data",
},
]
......@@ -472,6 +477,7 @@ describe("config", () => {
"data": "original data",
"filename": "/path/to/test.foo",
"line": 2,
"offset": 4,
},
]
`);
......@@ -493,6 +499,7 @@ describe("config", () => {
"data": "transformed source (was: data from mock-transform-chain-foo (was: original data))",
"filename": "/path/to/test.bar",
"line": 1,
"offset": 0,
"originalData": "original data",
},
]
......@@ -515,6 +522,7 @@ describe("config", () => {
"data": "transformed source (was: data from mock-transform-optional-chain (was: original data))",
"filename": "/path/to/test.bar.foo",
"line": 1,
"offset": 0,
"originalData": "original data",
},
]
......@@ -537,6 +545,7 @@ describe("config", () => {
"data": "transformed source (was: original data)",
"filename": "/path/to/test.foo",
"line": 1,
"offset": 0,
"originalData": "original data",
},
]
......@@ -605,6 +614,7 @@ describe("config", () => {
",
"filename": "test-files/parser/simple.html",
"line": 1,
"offset": 0,
"originalData": "<p>Lorem ipsum</p>
",
},
......@@ -614,6 +624,17 @@ describe("config", () => {
});
describe("init()", () => {
it("should handle being called multiple times", () => {
expect.assertions(1);
const config = Config.fromObject({});
const spy = jest
.spyOn(config as any, "precompileTransformers")
.mockReturnValue([]);
config.init();
config.init();
expect(spy).toHaveBeenCalledTimes(1);
});
it("should handle unset fields", () => {
const config = Config.fromObject({
plugins: null,
......
......@@ -75,6 +75,8 @@ function loadFromFile(filename: string): ConfigData {
export class Config {
private config: ConfigData;
private configurations: Map<string, ConfigData>;
private initialized: boolean;
protected metaTable: MetaTable;
protected plugins: Plugin[];
protected transformers: TransformerEntry[];
......@@ -130,6 +132,7 @@ export class Config {
this.config = mergeInternal(initial, options || {});
this.metaTable = null;
this.rootDir = this.findRootDir();
this.initialized = false;
/* load plugins */
this.plugins = this.loadPlugins(this.config.plugins || []);
......@@ -150,13 +153,20 @@ export class Config {
/**
* Initialize plugins, transforms etc.
*
* Must be called before trying to use config.
* Must be called before trying to use config. Can safely be called multiple
* times.
*/
public init(): void {
if (this.initialized) {
return;
}
/* precompile transform patterns */
this.transformers = this.precompileTransformers(
this.config.transform || {}
);
this.initialized = true;
}
/**
......@@ -377,6 +387,7 @@ export class Config {
filename,
line: 1,
column: 1,
offset: 0,
originalData: data,
};
return this.transformSource(source);
......
......@@ -19,7 +19,7 @@ export class Context {
this.state = undefined;
this.string = source.data;
this.filename = source.filename;
this.offset = 0;
this.offset = source.offset;
this.line = source.line;
this.column = source.column;
this.contentModel = ContentModel.TEXT;
......
......@@ -39,8 +39,37 @@ export interface SourceHooks {
export interface Source {
data: string;
filename: string;
/**
* Line in the original data.
*
* Starts at 1 (first line).
*/
line: number;
/**
* Column in the original data.
*
* Starts at 1 (first column).
*/
column: number;
/**
* Offset in the original data.
*
* Starts at 0 (first character).
*/
offset: number;
/**
* Original data. When a transformer extracts a portion of the original source
* this must be set to the full original source.
*
* Since the transformer might be chained always test if the input source
* itself has `originalData` set, e.g.:
*
* `originalData = input.originalData || input.data`.
*/
originalData?: string;
/**
......
......@@ -41,6 +41,7 @@ describe("HtmlElement", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
hooks: {
processAttribute,
},
......
......@@ -14,6 +14,7 @@ function inline(source: string): Source {
filename: "inline",
line: 1,
column: 1,
offset: 0,
data: source,
};
}
......
......@@ -24,10 +24,11 @@ function mockConfig(): Config {
config.init();
config.transformFilename = jest.fn((filename: string) => [
{
column: 1,
data: `source from ${filename}`,
filename,
line: 1,
column: 1,
offset: 0,
},
]);
return config;
......@@ -70,6 +71,7 @@ describe("HtmlValidate", () => {
data: str,
filename: "inline",
line: 1,
offset: 0,
},
]);
});
......@@ -94,6 +96,7 @@ describe("HtmlValidate", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
const report = htmlvalidate.validateSource(source);
expect(report).toEqual(mockReport);
......@@ -114,6 +117,7 @@ describe("HtmlValidate", () => {
data: "source from foo.html",
filename,
line: 1,
offset: 0,
},
]);
});
......@@ -243,6 +247,7 @@ describe("HtmlValidate", () => {
data: "source from foo.html",
filename,
line: 1,
offset: 0,
},
]);
});
......@@ -258,6 +263,7 @@ describe("HtmlValidate", () => {
data: "source from foo.html",
filename,
line: 1,
offset: 0,
},
]);
});
......@@ -273,6 +279,7 @@ describe("HtmlValidate", () => {
data: "source from foo.html",
filename,
line: 1,
offset: 0,
},
]);
});
......@@ -284,16 +291,18 @@ describe("HtmlValidate", () => {
config.init();
config.transformFilename = jest.fn((filename: string) => [
{
column: 1,
data: `first markup`,
filename,
line: 1,
column: 1,
offset: 0,
},
{
column: 3,
data: `second markup`,
filename,
line: 5,
column: 3,
offset: 29,
},
]);
jest.spyOn(htmlvalidate, "getConfigFor").mockImplementation(() => config);
......@@ -376,6 +385,7 @@ describe("HtmlValidate", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
const parser = htmlvalidate.getParserFor(source);
expect(parser).toBeInstanceOf(Parser);
......
......@@ -43,10 +43,11 @@ class HtmlValidate {
hooks?: SourceHooks
): Report {
const source = {
column: 1,
data: str,
filename: filename || "inline",
line: 1,
column: 1,
offset: 0,
hooks,
};
return this.validateSource(source);
......@@ -206,6 +207,7 @@ class HtmlValidate {
/* special case when the global configuration is marked as root, should not
* try to load and more configuration files */
if (this.globalConfig.isRootFound()) {
this.globalConfig.init();
return this.globalConfig;
}
......
import HtmlValidate from "./htmlvalidate";
import { Source } from "./context";
import { TRANSFORMER_API } from "./transform";
import { Plugin } from "./plugin";
import "./matchers";
import { Rule } from "./rule";
import { DOMReadyEvent } from "./event";
it("should compute correct line, column and offset when using transformed sources", () => {
expect.assertions(2);
/* create a mock rule which reports error on root element */
class MockRule extends Rule {
public setup(): void {
this.on("dom:ready", (event: DOMReadyEvent) => {
const root = event.document.root;
this.report(root, "mock error");
});
}
}
/* create a mock transformer which will create a new source for each line */
function transformer(source: Source): Source[] {
const lines = source.data.split("\n");
return lines.filter(Boolean).map(
(line: string, index: number): Source => {
/* all lines have the same length */
const offset = (line.length + 1) * index;
return {
data: line,
filename: source.filename,
line: index + 1,
column: 1,
offset,
originalData: source.data,
};
}
);
}
transformer.api = TRANSFORMER_API.VERSION;
/* create a mock plugin to expose mocks */
const plugin: Plugin = {
rules: {
"mock-rule": MockRule,
},
transformer,
};
jest.mock("plugin", () => plugin, { virtual: true });
/* create validator instance configured to use mock */
const htmlvalidate = new HtmlValidate({
root: true,
plugins: ["plugin"],
transform: {
".*": "plugin",
},
rules: {
"mock-rule": "error",
},
});
/* ensure line, column and offsets are correct */
const report = htmlvalidate.validateString(
"<p>line 1</p>\n<p>line 2</p>\n<p>line 3</p>\n"
);
expect(report).toBeInvalid();
expect(report.results[0]).toMatchInlineSnapshot(`
Object {
"errorCount": 3,
"filePath": "inline",
"messages": Array [
Object {
"column": 1,
"context": undefined,
"line": 1,
"message": "mock error",
"offset": 0,
"ruleId": "mock-rule",
"severity": 2,
"size": 0,
},
Object {
"column": 1,
"context": undefined,
"line": 2,
"message": "mock error",
"offset": 14,
"ruleId": "mock-rule",
"severity": 2,
"size": 0,
},
Object {
"column": 1,
"context": undefined,
"line": 3,
"message": "mock error",
"offset": 28,
"ruleId": "mock-rule",
"severity": 2,
"size": 0,
},
],
"source": "<p>line 1</p>
<p>line 2</p>
<p>line 3</p>
",
"warningCount": 0,
}
`);
});
......@@ -9,6 +9,7 @@ function inlineSource(source: string, { line = 1, column = 1 } = {}): Source {
filename: "inline",
line,
column,
offset: 0,
};
}
......
......@@ -900,6 +900,7 @@ describe("parser", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
hooks: {
processAttribute,
},
......@@ -952,6 +953,7 @@ describe("parser", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
hooks: {
processAttribute,
},
......@@ -988,6 +990,7 @@ describe("parser", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
hooks: {
processElement,
},
......
......@@ -55,13 +55,14 @@ export class Parser {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
}
/* reset DOM in case there are multiple calls in the same session */
this.dom = new DOMTree({
filename: source.filename,
offset: 0,
offset: source.offset,
line: source.line,
column: source.column,
});
......
......@@ -21,6 +21,7 @@ describe("Plugin", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
};
/* reset mock */
......@@ -284,6 +285,7 @@ describe("Plugin", () => {
filename: source.filename,
line: source.line,
column: source.column,
offset: source.offset,
originalData: source.data,
},
];
......@@ -302,6 +304,7 @@ describe("Plugin", () => {
filename: "/path/to/mock.filename",
line: 2,
column: 3,
offset: 4,
});
expect(sources).toMatchInlineSnapshot(`
Array [
......@@ -310,6 +313,7 @@ describe("Plugin", () => {
"data": "transformed from unnamed transformer",
"filename": "/path/to/mock.filename",
"line": 2,
"offset": 4,
"originalData": "original data",
},
]
......@@ -325,6 +329,7 @@ describe("Plugin", () => {
filename: source.filename,
line: source.line,
column: source.column,
offset: source.offset,
originalData: source.data,
},
];
......@@ -345,6 +350,7 @@ describe("Plugin", () => {
filename: "/path/to/mock.filename",
line: 2,
column: 3,
offset: 4,
});
expect(sources).toMatchInlineSnapshot(`
Array [
......@@ -353,6 +359,7 @@ describe("Plugin", () => {
"data": "transformed from named transformer",
"filename": "/path/to/mock.filename",
"line": 2,
"offset": 4,
"originalData": "original data",
},
]
......
......@@ -169,13 +169,20 @@ describe("Reporter", () => {
it("should map filenames to sources", () => {
const report = new Reporter();
const sources: Source[] = [
{ filename: "foo.html", data: "<foo></foo>", line: 1, column: 1 },
{
filename: "foo.html",
data: "<foo></foo>",
line: 1,
column: 1,
offset: 0,
},
{
filename: "bar.html",
data: "transformed",
originalData: "<bar></bar>",
line: 1,
column: 1,
offset: 0,
},
];
report.addManual("foo.html", createMessage("error", 1));
......
......@@ -17,6 +17,7 @@ describe("a17y helpers", () => {
filename: "inline",
line: 1,
column: 1,
offset: 0,
hooks: {
processAttribute,
},
......
......@@ -14,6 +14,7 @@ function* mockTransformChainFoo(
filename: source.filename,
line: 1,
column: 1,
offset: 0,
originalData: source.originalData || source.data,
},
`${source.filename}.foo`
......
......@@ -17,6 +17,7 @@ function* mockTransformOptionalChain(
filename: source.filename,
line: 1,
column: 1,
offset: 0,
originalData: source.originalData || source.data,
},
next
......
......@@ -11,6 +11,7 @@ function mockTransform(source: Source): Iterable<Source> {
filename: source.filename,
line: 1,
column: 1,
offset: 0,
originalData: source.originalData || source.data,
},
];
......
import { offsetToLineColumn } from "./helpers";
describe("offsetToLineColumn() should calculate line and column from offset", () => {
const data = "lorem\nipsum dolor\nsit amet";
it.each`
offset | line | column | character
${0} | ${1} | ${1} | ${"l"}
${1} | ${1} | ${2} | ${"o"}
${5} | ${1} | ${6} | ${"\n"}
${6} | ${2} | ${1} | ${"i"}
${16} | ${2} | ${11} | ${"r"}
${18} | ${3} | ${1} | ${"s"}
${25} | ${3} | ${8} | ${"t"}
`(
"Offset $offset should be $line:$column",
({ offset, line, column, character }) => {
expect.assertions(2);
expect(data[offset]).toEqual(character);
expect(offsetToLineColumn(data, offset)).toEqual([line, column]);
}
);
});
/**
* Given an offset into a source, calculate the corresponding line and column.
*/
export function offsetToLineColumn(
data: string,
offset: number
): [number, number] {
let line = 1;
let prev = 0;
let pos = data.indexOf("\n");
/* step one line at a time until newline position is beyond wanted offset */
while (pos !== -1) {
if (pos >= offset) {
return [line, offset - prev + 1];
}
line++;
prev = pos + 1;
pos = data.indexOf("\n", pos + 1);
}
/* missing final newline */
return [line, offset - prev + 1];
}
......@@ -3,6 +3,7 @@ import { TransformContext } from "./context";
export { TransformContext } from "./context";
export { TemplateExtractor } from "./template";
export { offsetToLineColumn } from "./helpers";
export type Transformer = (
this: TransformContext,
......
......@@ -5,7 +5,13 @@ describe("TemplateExtractor", () => {
it("should extract templates from object property", () => {
const te = TemplateExtractor.fromString('foo({template: "<b>foo</b>"})');
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b>foo</b>", filename: "inline", line: 1, column: 16 },
{
data: "<b>foo</b>",
filename: "inline",
line: 1,
column: 16,
offset: 16,
},
]);
});
......@@ -14,7 +20,13 @@ describe("TemplateExtractor", () => {
'foo({"template": "<b>foo</b>"})'
);
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b>foo</b>", filename: "inline", line: 1, column: 18 },
{
data: "<b>foo</b>",
filename: "inline",
line: 1,
column: 18,
offset: 18,
},
]);
});
......@@ -26,14 +38,26 @@ describe("TemplateExtractor", () => {
it("should handle single quotes", () => {
const te = TemplateExtractor.fromString("foo({template: '<b>foo</b>'})");
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b>foo</b>", filename: "inline", line: 1, column: 16 },
{
data: "<b>foo</b>",
filename: "inline",
line: 1,
column: 16,
offset: 16,
},
]);
});
it("should handle double quotes", () => {
const te = TemplateExtractor.fromString('foo({template: "<b>foo</b>"})');
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b>foo</b>", filename: "inline", line: 1, column: 16 },
{
data: "<b>foo</b>",
filename: "inline",
line: 1,
column: 16,
offset: 16,
},
]);
});
......@@ -42,7 +66,13 @@ describe("TemplateExtractor", () => {
"foo({template: `<b>${foo}</b>`})"
);
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b> </b>", filename: "inline", line: 1, column: 16 },
{
data: "<b> </b>",
filename: "inline",
line: 1,
column: 16,
offset: 16,
},
]);
});
......@@ -51,7 +81,13 @@ describe("TemplateExtractor", () => {
"foo({template: foo`<b>${foo}</b>`})"
);
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b> </b>", filename: "inline", line: 1, column: 19 },
{
data: "<b> </b>",
filename: "inline",
line: 1,
column: 19,
offset: 19,
},
]);
});
......@@ -60,7 +96,13 @@ describe("TemplateExtractor", () => {
"foo({template: (foo) => `<b>${foo}</b>`})"
);
expect(te.extractObjectProperty("template")).toEqual([
{ data: "<b> </b>", filename: "inline", line: 1, column: 25 },
{
data: "<b> </b>",
filename: "inline",
line: 1,
column: 25,
offset: 25,
},
]);
});
......@@ -89,18 +131,21 @@ describe("TemplateExtractor", () => {
filename: "test-files/extract.js",
line: 2,
column: 12,
offset: 53,
},
{
data: "<b>foo</b>",
filename: "test-files/extract.js",
line: 6,
column: 12,
offset: 113,
},
{
data: "<p>foo</p>",
filename: "test-files/extract.js",
line: 10,
column: 12,
offset: 165,
},
]);
});
......@@ -112,6 +157,7 @@ describe("TemplateExtractor", () => {
filename: "test-files/extract.js",
line: 1,
column: 1,
offset: 0,
},
]);
});
......
......@@ -27,13 +27,39 @@ function joinTemplateLiteral(nodes: ESTree.TemplateElement[]): string {
return output;
}
/**
* espree locations does not include offset, need to calculate it manually.
*/
function computeOffset(position: ESTree.Position, data: string): number {
let line = position.line;
let column = position.column + 1;
for (let i = 0; i < data.length; i++) {
if (line > 1) {
/* not yet on the correct line */
if (data[i] === "\n") {
line--;
}
} else if (column > 1) {
/* not yet on the correct column */
column--;
} else {
/* line/column found, return current position */
return i;
}
}
/* istanbul ignore next: should never reach this line unless espree passes bad
* positions, no sane way to test */
throw new Error("Failed to compute location offset from position");
}
function extractLiteral(
node:
| ESTree.Expression
| ESTree.Pattern
| ESTree.Literal
| ESTree.BlockStatement,
filename: string
filename: string,
data: string
): Source {
switch (node.type) {
/* ignored nodes */
......@@ -46,6 +72,7 @@ function extractLiteral(
filename: null,
line: node.loc.start.line,
column: node.loc.start.column + 1,
offset: computeOffset(node.loc.start, data) + 1,
};
case "TemplateLiteral":
......@@ -54,6 +81,7 @@ function extractLiteral(
filename: null,
line: node.loc.start.line,
column: node.loc.start.column + 1,
offset: computeOffset(node.loc.start, data) + 1,
};
case "TaggedTemplateExpression":
......@@ -62,12 +90,13 @@ function extractLiteral(
filename: null,
line: node.quasi.loc.start.line,
column: node.quasi.loc.start.column + 1,
offset: computeOffset(node.quasi.loc.start, data) + 1,
};
case "ArrowFunctionExpression": {
const whitelist = ["Literal", "TemplateLiteral"];
if (whitelist.includes(node.body.type)) {
return extractLiteral(node.body, filename);
return extractLiteral(node.body, filename, data);
} else {
return null;
}
......@@ -110,20 +139,22 @@ function compareKey(
export class TemplateExtractor {
protected ast: ESTree.Program;
private filename: string;
private data: string;
private constructor(ast: ESTree.Program, filename: string) {
private constructor(ast: ESTree.Program, filename: string, data: string) {
this.ast = ast;
this.filename = filename;
this.data = data;
}
public static fromFilename(filename: string): TemplateExtractor {
const source = fs.readFileSync(filename);
const source = fs.readFileSync(filename, "utf-8");
const ast = espree.parse(source, {
ecmaVersion: 2017,
sourceType: "module",
loc: true,
});
return new TemplateExtractor(ast, filename);
return new TemplateExtractor(ast, filename, source);
}
/**
......@@ -146,7 +177,7 @@ export class TemplateExtractor {
sourceType: "module",
loc: true,
});
return new TemplateExtractor(ast, filename || "inline");
return new TemplateExtractor(ast, filename || "inline", source);
}
/**
......@@ -165,6 +196,7 @@ export class TemplateExtractor {
data,
filename,
line: 1,
offset: 0,
},
];
}
......@@ -187,11 +219,11 @@ export class TemplateExtractor {
*/
public extractObjectProperty(key: string): Source[] {
const result: Source[] = [];
const filename = this.filename;
const { filename, data } = this;
walk.simple(this.ast, {
Property(node: ESTree.Property) {
if (compareKey(node.key, key, filename)) {
const source = extractLiteral(node.value, filename);
const source = extractLiteral(node.value, filename, data);
if (source) {
source.filename = filename;
result.push(source);
......
......@@ -19,6 +19,7 @@ it("transformFile() should read file and apply transformer", () => {
"data": "mocked file data",
"filename": "foo.html",
"line": 1,
"offset": 0,
},
]
`);
......@@ -34,6 +35,7 @@ it("transformString() should apply transformer", () => {
"data": "inline data",
"filename": "inline",
"line": 1,
"offset": 0,
},
]
`);
......@@ -44,6 +46,7 @@ it("transformSource() should apply transformer", () => {
filename: "bar.html",
line: 1,
column: 2,
offset: 3,
data: "source data",
};
const transformer = jest.fn((source: Source) => [source]);
......@@ -55,6 +58,7 @@ it("transformSource() should apply transformer", () => {
"data": "source data",
"filename": "bar.html",
"line": 1,
"offset": 3,
},
]
`);
......@@ -65,6 +69,7 @@ it("transformSource() should support chaining", () => {
filename: "bar.html",
line: 1,
column: 2,
offset: 3,
data: "source data",
};
const transformer = jest.fn(function(this: TransformContext, source: Source) {
......@@ -78,6 +83,7 @@ it("transformSource() should support chaining", () => {
"data": "source data",
"filename": "bar.html",
"line": 1,
"offset": 3,
},
]
`);
......@@ -88,6 +94,7 @@ it("transformSource() should support custom chaining", () => {
filename: "bar.html",
line: 1,
column: 2,
offset: 3,
data: "source data",
};
const chain = jest.fn((source: Source) => [source]);
......@@ -103,6 +110,7 @@ it("transformSource() should support custom chaining", () => {
"data": "source data",
"filename": "bar.html",
"line": 1,
"offset": 3,
},
]
`);
......
......@@ -19,6 +19,7 @@ export function transformFile(
filename,
line: 1,
column: 1,
offset: 0,
data,
};
return transformSource(fn, source, chain);
......@@ -40,6 +41,7 @@ export function transformString(
filename: "inline",
line: 1,
column: 1,
offset: 0,
data,
};
return transformSource(fn, source, chain);
......