Commit 931a39f0 authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(parser): add full location to `attr` event (key, quotes, value)

parent 75aa5f0f
Pipeline #288292361 passed with stages
in 10 minutes and 27 seconds
......@@ -18,7 +18,7 @@ Array [
"ruleUrl": "https://html-validate.org/rules/attr-quotes.html",
"selector": "p",
"severity": 2,
"size": 5,
"size": 11,
},
],
"source": "<p class='foo'></p>",
......
......@@ -20,7 +20,7 @@ Array [
"ruleUrl": "https://html-validate.org/rules/no-inline-style.html",
"selector": "p",
"severity": 2,
"size": 5,
"size": 18,
},
],
"source": "<p style=\\"color: red\\"></p>",
......
......@@ -87,7 +87,7 @@ export interface ElementReadyEvent extends Event {
* Event emitted when attributes are encountered.
*/
export interface AttributeEvent extends Event {
/** Event location. */
/** Location of the full attribute (key, quotes and value) */
location: Location;
/** Attribute name. */
......@@ -106,6 +106,9 @@ export interface AttributeEvent extends Event {
/** HTML element this attribute belongs to. */
target: HtmlElement;
/** Location of the attribute key */
keyLocation: Location;
/** Location of the attribute value */
valueLocation: Location | null;
}
......
......@@ -11,8 +11,10 @@ import { Parser } from "./parser";
function mergeEvent(event: string, data: any): any {
const merged = { event, ...data };
/* not useful for these tests */
delete merged.location;
/* legacy: not useful for these tests */
if (event !== "attr") {
delete merged.location;
}
/* change HtmlElement instances to just tagname for easier testing */
for (const key of ["target", "previous"]) {
......@@ -235,9 +237,20 @@ describe("parser", () => {
value: "bar",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 2,
column: 1,
size: 9,
}),
keyLocation: expect.objectContaining({
line: 2,
column: 1,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 2,
column: 6,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -263,9 +276,20 @@ describe("parser", () => {
value: "bar",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 9,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 11,
size: 3,
}),
});
expect(events.shift()).toEqual({
......@@ -274,9 +298,20 @@ describe("parser", () => {
value: "ham",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 2,
column: 1,
size: 10,
}),
keyLocation: expect.objectContaining({
line: 2,
column: 1,
size: 4,
}),
valueLocation: expect.objectContaining({
line: 2,
column: 7,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -345,9 +380,20 @@ describe("parser", () => {
value: "bar",
quote: null,
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 7,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 10,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -373,9 +419,20 @@ describe("parser", () => {
value: "bar",
quote: "'",
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 9,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 11,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -401,9 +458,20 @@ describe("parser", () => {
value: "bar",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 9,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 11,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -421,7 +489,7 @@ describe("parser", () => {
it("with nested quotes", () => {
expect.assertions(7);
parser.parseHtml("<div foo='\"foo\"' bar=\"'foo'\"></div>");
parser.parseHtml(`<div foo='"foo"' bar="'foo'"></div>`);
expect(events.shift()).toEqual({ event: "tag:start", target: "div" });
expect(events.shift()).toEqual({
event: "attr",
......@@ -429,9 +497,20 @@ describe("parser", () => {
value: '"foo"',
quote: "'",
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 11,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 11,
size: 5,
}),
});
expect(events.shift()).toEqual({
......@@ -440,9 +519,20 @@ describe("parser", () => {
value: "'foo'",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 18,
size: 11,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 18,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 23,
size: 5,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -468,6 +558,16 @@ describe("parser", () => {
value: null,
quote: null,
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -485,7 +585,7 @@ describe("parser", () => {
it("with empty value", () => {
expect.assertions(7);
parser.parseHtml("<div foo=\"\" bar=''></div>");
parser.parseHtml(`<div foo="" bar=''></div>`);
expect(events.shift()).toEqual({ event: "tag:start", target: "div" });
expect(events.shift()).toEqual({
event: "attr",
......@@ -493,6 +593,16 @@ describe("parser", () => {
value: "",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 6,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: null,
});
expect(events.shift()).toEqual({
......@@ -501,6 +611,16 @@ describe("parser", () => {
value: "",
quote: "'",
target: "div",
location: expect.objectContaining({
line: 1,
column: 13,
size: 6,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 13,
size: 3,
}),
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -526,6 +646,16 @@ describe("parser", () => {
value: null,
quote: null,
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 11,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 11,
}),
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -551,9 +681,20 @@ describe("parser", () => {
value: "foo bar baz",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 19,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 5,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 13,
size: 11,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -579,9 +720,20 @@ describe("parser", () => {
value: "foo",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 12,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 6,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 14,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -607,9 +759,20 @@ describe("parser", () => {
value: "bar",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 9,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 3,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 11,
size: 3,
}),
});
expect(events.shift()).toEqual({
......@@ -618,9 +781,20 @@ describe("parser", () => {
value: "ham",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 16,
size: 10,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 16,
size: 4,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 22,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -646,9 +820,20 @@ describe("parser", () => {
value: "text",
quote: '"',
target: "input",
location: expect.objectContaining({
line: 1,
column: 8,
size: 11,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 8,
size: 4,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 14,
size: 4,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
......@@ -674,9 +859,20 @@ describe("parser", () => {
value: "baz",
quote: '"',
target: "div",
location: expect.objectContaining({
line: 1,
column: 6,
size: 13,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 6,
size: 7,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 15,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
......@@ -1057,6 +1253,16 @@ describe("parser", () => {
value: "foo",
quote: '"',
target: "input",
location: expect.objectContaining({
line: 1,
column: 8,
size: 8,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 8,
size: 2,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 12,
......@@ -1069,6 +1275,16 @@ describe("parser", () => {
quote: '"',
originalAttribute: "id",
target: "input",
location: expect.objectContaining({
line: 1,
column: 8,
size: 8,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 8,
size: 2,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 12,
......@@ -1112,9 +1328,20 @@ describe("parser", () => {
value: "barney",
quote: '"',
target: "input",
location: expect.objectContaining({
line: 1,
column: 8,
size: 8,
}),
keyLocation: expect.objectContaining({
line: 1,
column: 8,
size: 2,
}),
valueLocation: expect.objectContaining({
line: 1,
column: 12,
size: 3,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
......
......@@ -348,8 +348,9 @@ export class Parser {
}
protected consumeAttribute(source: Source, node: HtmlElement, token: Token, next?: Token): void {
const keyLocation = token.location;
const keyLocation = this.getAttributeKeyLocation(token);
const valueLocation = this.getAttributeValueLocation(next);
const location = this.getAttributeLocation(token, next);
const haveValue = next && next.type === TokenType.ATTR_VALUE;
const attrData: AttributeData = {
key: token.data[1],
......@@ -391,7 +392,8 @@ export class Parser {
value: attr.value,
quote: attr.quote,
originalAttribute: attr.originalAttribute,
location: keyLocation,
location,
keyLocation,
valueLocation,
};
this.trigger("attr", event);
......@@ -399,6 +401,13 @@ export class Parser {
}
}
/**
* Takes attribute key token an returns location.
*/
private getAttributeKeyLocation(token: Token): Location {
return token.location;
}
/**
* Take attribute value token and return a new location referring to only the
* value.
......@@ -418,6 +427,22 @@ export class Parser {
}
}
/**
* Take attribute key and value token an returns a new location referring to
* an aggregate location covering key, quotes if present and value.
*/
private getAttributeLocation(key: Token, value?: Token): Location {
const begin = key.location;
const end = value && value.type === TokenType.ATTR_VALUE ? value.location : undefined;
return {
filename: begin.filename,
line: begin.line,
column: begin.column,
size: begin.size + (end?.size ?? 0),
offset: begin.offset,
};
}
protected consumeDirective(token: Token): void {
const directive = token.data[1];
const match = directive.match(/^([a-zA-Z0-9-]+)\s*(.*?)(?:\s*:\s*(.*))?$/);
......
......@@ -29,7 +29,7 @@ Array [
"ruleUrl": "https://html-validate.org/rules/no-inline-style.html",
"selector": "p",
"severity": 2,
"size": 5,
"size": 18,
},
],
"source": "<p style=\\"color: red\\"></p>
......
......@@ -67,7 +67,11 @@ export default class AttrCase extends Rule<void, RuleOptions> {
const letters = event.key.replace(/[^a-z]+/gi, "");
if (!this.style.match(letters)) {
this.report(event.target, `Attribute "${event.key}" should be ${this.style.name}`);
this.report(
event.target,
`Attribute "${event.key}" should be ${this.style.name}`,
event.keyLocation
);
}
});
}
......
......@@ -22,7 +22,11 @@ export default class NoDeprecatedAttr extends Rule {
const deprecated = meta.deprecatedAttributes || [];
if (deprecated.includes(attr)) {
this.report(node, `Attribute "${event.key}" is deprecated on <${node.tagName}> element`);
this.report(
node,
`Attribute "${event.key}" is deprecated on <${node.tagName}> element`,
event.keyLocation
);
}
});
}
......
......@@ -25,7 +25,7 @@ export default class NoDupAttr extends Rule {
const name = event.key.toLowerCase();
if (name in attr) {
this.report(event.target, `Attribute "${name}" duplicated`);
this.report(event.target, `Attribute "${name}" duplicated`, event.keyLocation);
}
attr[event.key] = true;
});
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment