Commit cfbf3dce authored by David Sveningsson's avatar David Sveningsson
Browse files

feat(events): new event `tag:ready` emitted when start tag is parsed

parent 91acae7d
......@@ -5,7 +5,9 @@ title: Events
# Events
## `config:ready`
## Engine
### `config:ready`
```typescript
{
......@@ -16,7 +18,9 @@ title: Events
Emitted after after configuration is ready but before DOM is initialized.
## `dom:load`
## Document
### `dom:load`
```typescript
{
......@@ -26,7 +30,7 @@ Emitted after after configuration is ready but before DOM is initialized.
Emitted after initialization but before tokenization and parsing occurs. Can be
used to initialize state in rules.
## `dom:ready`
### `dom:ready`
```typescript
{
......@@ -36,7 +40,7 @@ used to initialize state in rules.
Emitted after the parsing has finished loading the DOM tree.
## `doctype`
### `doctype`
```typescript
{
......@@ -52,7 +56,23 @@ Emitted when a doctype is encountered. `value` is the doctype (without
`location` refers to the doctype opening tag and `valueLocation` to the value
(as described above)
## `tag:open`
## DOM Nodes
```plaintext
attr attr
tag:open | tag:ready tag:open | tag:ready
| | / | | /
v vv v vv
<div class="foobar"> <input class="foobar">
.. ^
</div> \
^ element:ready
|\
| element:ready (tag:close not emitted)
tag:end
```
### `tag:open`
```typescript
{
......@@ -60,11 +80,13 @@ Emitted when a doctype is encountered. `value` is the doctype (without
}
```
Emitted when an opening element is parsed: `<div>`. `target` will be newly
created Node. The element will not have its attribute nor children yet. Use
`element:ready` to wait for the element to be complete.
Emitted when a start tag is parsed: `<div>`.
`target` will be newly created element.
The element will not have its attribute nor children yet.
Use `tag:ready` (all attributes parsed) or `element:ready` (all children parsed) if you need to wait for element to be ready.
## `tag:close`
### `tag:close`
```typescript
{
......@@ -73,11 +95,25 @@ created Node. The element will not have its attribute nor children yet. Use
}
```
Emitted when a closing element is parsed: `</div>`. `target` refers to
the close-tag itself and `previous` is the current active element
about to be closed.
Emitted when an end tag is parsed: `</div>`.
It is similar to `element:ready` but will not be emitted for `void` elements.
`target` refers to the close-tag itself and `previous` is the current active element about to be closed.
### `tag:ready`
```typescript
{
target: Node,
}
```
Emitted when a start tag is finished parsing (i.e. the node and all attributes are consumed by the parser).
## `element:ready`
`target` will be the element.
The children will not yet be parsed.
### `element:ready`
```typescript
{
......@@ -86,9 +122,11 @@ about to be closed.
```
Emitted when an element is fully constructed (including its children).
It is similar to `tag:close` but will be emitted for `void` elements as well.
`target` will be the element.
## `attr`
### `attr`
```typescript
{
......@@ -104,14 +142,13 @@ Emitted when an element is fully constructed (including its children).
Emitted when an element attribute is parsed: `<div foo="bar">`.
Target node will not have been updated with the new attribute yet
(e.g. `node.getAttribute(...)` will return `undefined` or a previous
value).
Target node will not have been updated with the new attribute yet (e.g. `node.getAttribute(...)` will return `undefined` or a previous value).
`originalAttribute` is set when a transformer has modified the attribute and contains the original attribute name, e.g. `ng-class` or `v-bind:class`.
`originalAttribute` is set when a transformer has modified the attribute and
contains the original attribute name, e.g. `ng-class` or `v-bind:class`.
## Misc
## `whitespace`
### `whitespace`
```typescript
{
......@@ -121,7 +158,7 @@ contains the original attribute name, e.g. `ng-class` or `v-bind:class`.
Emitted when inter-element, leading and trailing whitespace is parsed.
## `conditional`
### `conditional`
```typescript
{
......
......@@ -296,14 +296,44 @@ describe("Engine", () => {
expect(report).toBeValid();
});
it('"disable-next" should disable rule once', () => {
expect.assertions(2);
const source: Source[] = [
inline("<!-- [html-validate-disable-next close-order] --><p></i><p></i>"),
];
const report = engine.lint(source);
expect(report).toBeInvalid();
expect(report).toHaveError("close-order", expect.any(String));
describe('"disable-next"', () => {
it("should disable next error on element", () => {
expect.assertions(1);
const markup = `
<!-- [html-validate-disable-next void-style] -->
<input type="hidden" />
`;
const source: Source[] = [inline(markup)];
const report = engine.lint(source);
expect(report).toBeValid();
});
it("should disable next error once", () => {
expect.assertions(2);
const markup = `
<!-- [html-validate-disable-next void-style] -->
<input type="hidden" />
<input type="hidden" />
`;
const source: Source[] = [inline(markup)];
const report = engine.lint(source);
expect(report).toBeInvalid();
expect(report).toHaveError("void-style", expect.any(String));
});
it("should be canceled by end tag", () => {
expect.assertions(2);
const markup = `
<div>
<!-- [html-validate-disable-next void-style] -->
</div>
<input type="hidden" />
`;
const source: Source[] = [inline(markup)];
const report = engine.lint(source);
expect(report).toBeInvalid();
expect(report).toHaveError("void-style", expect.any(String));
});
});
it('"disable-next" should disable rule on nodes', () => {
......@@ -329,20 +359,22 @@ describe("Engine", () => {
describe("dumpEvents()", () => {
it("should dump parser events", () => {
expect.assertions(11);
expect.assertions(13);
const source: Source[] = [inline('<div id="foo"><p class="bar">baz</p></div>')];
const lines = engine.dumpEvents(source);
expect(lines).toHaveLength(10);
expect(lines).toHaveLength(12);
expect(lines[0].event).toEqual("dom:load");
expect(lines[1].event).toEqual("tag:open");
expect(lines[2].event).toEqual("attr");
expect(lines[3].event).toEqual("tag:open");
expect(lines[4].event).toEqual("attr");
expect(lines[5].event).toEqual("tag:close");
expect(lines[6].event).toEqual("element:ready");
expect(lines[3].event).toEqual("tag:ready");
expect(lines[4].event).toEqual("tag:open");
expect(lines[5].event).toEqual("attr");
expect(lines[6].event).toEqual("tag:ready");
expect(lines[7].event).toEqual("tag:close");
expect(lines[8].event).toEqual("element:ready");
expect(lines[9].event).toEqual("dom:ready");
expect(lines[9].event).toEqual("tag:close");
expect(lines[10].event).toEqual("element:ready");
expect(lines[11].event).toEqual("dom:ready");
});
});
......
......@@ -283,7 +283,7 @@ export class Engine<T extends Parser = Parser> {
});
/* disable directive after next event occurs */
parser.once("tag:open, tag:close, attr", () => {
parser.once("tag:ready, tag:close, attr", () => {
unregister();
parser.defer(() => {
for (const rule of rules) {
......
......@@ -45,6 +45,18 @@ export interface TagCloseEvent extends Event {
previous: HtmlElement;
}
/**
* Event emitted when a tag is ready (i.e. all the attributes has been
* parsed). The children of the element will not yet be finished.
*/
export interface TagReadyEvent extends Event {
/** Event location. */
location: Location;
/** The node that is finished parsing. */
target: HtmlElement;
}
/**
* Event emitted when an element is fully constructed (including its children).
*/
......
......@@ -59,9 +59,10 @@ describe("parser", () => {
describe("should parse elements", () => {
it("simple element", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<div></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -75,9 +76,10 @@ describe("parser", () => {
});
it("with numbers", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<h1></h1>");
expect(events.shift()).toEqual({ event: "tag:open", target: "h1" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "h1" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "h1",
......@@ -91,9 +93,10 @@ describe("parser", () => {
});
it("with dashes", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<foo-bar></foo-bar>");
expect(events.shift()).toEqual({ event: "tag:open", target: "foo-bar" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "foo-bar" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "foo-bar",
......@@ -107,10 +110,12 @@ describe("parser", () => {
});
it("elements closed on wrong order", () => {
expect.assertions(7);
expect.assertions(9);
parser.parseHtml("<div><label></div></label>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({ event: "tag:open", target: "label" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "label" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -133,9 +138,10 @@ describe("parser", () => {
});
it("self-closing elements", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<input/>");
expect(events.shift()).toEqual({ event: "tag:open", target: "input" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "input",
......@@ -149,9 +155,10 @@ describe("parser", () => {
});
it("void elements", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<input>");
expect(events.shift()).toEqual({ event: "tag:open", target: "input" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "input",
......@@ -165,9 +172,10 @@ describe("parser", () => {
});
it("void elements with close tag", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml("<input></input>");
expect(events.shift()).toEqual({ event: "tag:open", target: "input" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "input",
......@@ -218,7 +226,7 @@ describe("parser", () => {
});
it("with newlines", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<div\nfoo="bar"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -232,6 +240,7 @@ describe("parser", () => {
column: 6,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -245,7 +254,7 @@ describe("parser", () => {
});
it("with newline after attribute", () => {
expect.assertions(6);
expect.assertions(7);
parser.parseHtml('<div foo="bar"\nspam="ham"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -270,6 +279,7 @@ describe("parser", () => {
column: 7,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -283,9 +293,10 @@ describe("parser", () => {
});
it("with xml namespaces", () => {
expect.assertions(4);
expect.assertions(5);
parser.parseHtml("<foo:div></foo:div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "foo:div" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "foo:div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "foo:div",
......@@ -325,7 +336,7 @@ describe("parser", () => {
describe("should parse attributes", () => {
it("without quotes", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml("<div foo=bar></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -339,6 +350,7 @@ describe("parser", () => {
column: 10,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -352,7 +364,7 @@ describe("parser", () => {
});
it("with single quotes", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml("<div foo='bar'></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -366,6 +378,7 @@ describe("parser", () => {
column: 11,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -379,7 +392,7 @@ describe("parser", () => {
});
it("with double quote", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<div foo="bar"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -393,6 +406,7 @@ describe("parser", () => {
column: 11,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -406,7 +420,7 @@ describe("parser", () => {
});
it("with nested quotes", () => {
expect.assertions(6);
expect.assertions(7);
parser.parseHtml("<div foo='\"foo\"' bar=\"'foo'\"></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -431,6 +445,7 @@ describe("parser", () => {
column: 23,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -444,7 +459,7 @@ describe("parser", () => {
});
it("without value", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml("<div foo></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -455,6 +470,7 @@ describe("parser", () => {
target: "div",
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -468,7 +484,7 @@ describe("parser", () => {
});
it("with empty value", () => {
expect.assertions(6);
expect.assertions(7);
parser.parseHtml("<div foo=\"\" bar=''></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -487,6 +503,7 @@ describe("parser", () => {
target: "div",
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -500,7 +517,7 @@ describe("parser", () => {
});
it("with dashes", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml("<div foo-bar-baz></div>");
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -511,6 +528,7 @@ describe("parser", () => {
target: "div",
valueLocation: null,
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -524,7 +542,7 @@ describe("parser", () => {
});
it("with spaces inside", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<div class="foo bar baz"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -538,6 +556,7 @@ describe("parser", () => {
column: 13,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -551,7 +570,7 @@ describe("parser", () => {
});
it("with uncommon characters", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<div a2?()!="foo"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -565,6 +584,7 @@ describe("parser", () => {
column: 14,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -578,7 +598,7 @@ describe("parser", () => {
});
it("with multiple attributes", () => {
expect.assertions(6);
expect.assertions(7);
parser.parseHtml('<div foo="bar" spam="ham"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -603,6 +623,7 @@ describe("parser", () => {
column: 22,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -616,7 +637,7 @@ describe("parser", () => {
});
it("on self-closing elements", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<input type="text"/>');
expect(events.shift()).toEqual({ event: "tag:open", target: "input" });
expect(events.shift()).toEqual({
......@@ -630,6 +651,7 @@ describe("parser", () => {
column: 14,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "input" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "input",
......@@ -643,7 +665,7 @@ describe("parser", () => {
});
it("with xml namespaces", () => {
expect.assertions(5);
expect.assertions(6);
parser.parseHtml('<div foo:bar="baz"></div>');
expect(events.shift()).toEqual({ event: "tag:open", target: "div" });
expect(events.shift()).toEqual({
......@@ -657,6 +679,7 @@ describe("parser", () => {
column: 15,
}),
});
expect(events.shift()).toEqual({ event: "tag:ready", target: "div" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "div",
......@@ -739,7 +762,7 @@ describe("parser", () => {
describe("should handle optional end tags", () => {
it("<li>", () => {
expect.assertions(22);
expect.assertions(29);
parser.parseHtml(`
<ul>
<li>explicit</li>
......@@ -748,7 +771,11 @@ describe("parser", () => {
<li><input>
</ul>`);
expect(events.shift()).toEqual({ event: "tag:open", target: "ul" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "ul" });
/* 1: explicitly closed <li> */
expect(events.shift()).toEqual({ event: "tag:open", target: "li" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "li" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "li",
......@@ -758,7 +785,10 @@ describe("parser", () => {
event: "element:ready",
target: "li",
});
/* 2: implicitly closed <li> */
expect(events.shift()).toEqual({ event: "tag:open", target: "li" });
expect(events.shift()).toEqual({ event: "tag:ready", target: "li" });
expect(events.shift()).toEqual({
event: "tag:close",
target: "li",
......@@ -768,8 +798,12 @@ describe("parser", () => {