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

feat(rule): support filter callback for rule events

Rule authors can now pass an optional callback to filter events before calling the listener.
parent 33befd77
Pipeline #240497093 passed with stages
in 10 minutes and 27 seconds
......@@ -11,7 +11,7 @@ method:
```typescript
import { Rule, RuleDocumentation } from "html-validate";
class MyRule extends Rule {
export default class MyRule extends Rule {
documentation(): RuleDocumentation {
return {
description: "Lorem ipsum",
......@@ -30,8 +30,6 @@ class MyRule extends Rule {
});
}
}
module.exports = MyRule;
```
All (enabled) rules run the `setup()` callback before the source document is being parsed and is used to setup any event listeners relevant for this rule.
......@@ -164,10 +162,11 @@ Options are accessed using `this.options`.
When using typescript: pass the datatype as the second template argument when extending `Rule`.
Default is `void` (i.e. no options)
### `on(event: string, callback: (event: Event)): void`
### `on(event: string, [filter: (event: Event) => boolean], callback: (event: Event) => void): void`
Listen for events. See [events](/dev/events.html) for a full list of available events and data.
Listen for events. See [events](/dev/events.html) for a full list of available
events and data.
If `filter` is passed the callback is only called if the filter function evaluates to true.
### `report(node: DOMNode, message: string, location?: Location, context?: RuleContext): void`
......
......@@ -187,40 +187,72 @@ describe("rule base class", () => {
let delivered: boolean;
let callback: (event: string, data: Event) => void;
beforeEach(() => {
delivered = false;
rule.on("*", () => {
delivered = true;
describe("severity", () => {
beforeEach(() => {
delivered = false;
rule.on("*", () => {
delivered = true;
});
callback = (parser.on as any).mock.calls[0][1];
});
callback = (parser.on as any).mock.calls[0][1];
});
it('should not deliver events with severity "disabled"', () => {
expect.assertions(1);
rule.setServerity(Severity.DISABLED);
callback("event", mockEvent);
expect(delivered).toBeFalsy();
});
it('should not deliver events with severity "disabled"', () => {
expect.assertions(1);
rule.setServerity(Severity.DISABLED);
callback("event", mockEvent);
expect(delivered).toBeFalsy();
});
it('should deliver events with severity "warn"', () => {
expect.assertions(1);
rule.setServerity(Severity.WARN);
callback("event", mockEvent);
expect(delivered).toBeTruthy();
});
it('should deliver events with severity "warn"', () => {
expect.assertions(1);
rule.setServerity(Severity.WARN);
callback("event", mockEvent);
expect(delivered).toBeTruthy();
});
it('should deliver events with severity "error"', () => {
expect.assertions(1);
rule.setServerity(Severity.ERROR);
callback("event", mockEvent);
expect(delivered).toBeTruthy();
it('should deliver events with severity "error"', () => {
expect.assertions(1);
rule.setServerity(Severity.ERROR);
callback("event", mockEvent);
expect(delivered).toBeTruthy();
});
it("should not deliver events when disabled", () => {
expect.assertions(1);
rule.setEnabled(false);
callback("event", mockEvent);
expect(delivered).toBeFalsy();
});
});
it("should not deliver events when disabled", () => {
expect.assertions(1);
rule.setEnabled(false);
callback("event", mockEvent);
expect(delivered).toBeFalsy();
describe("filter", () => {
let filterResult: boolean;
beforeEach(() => {
delivered = false;
rule.on(
"*",
() => filterResult,
() => {
delivered = true;
}
);
callback = (parser.on as any).mock.calls[0][1];
});
it("should deliver event when filter return true", () => {
expect.assertions(1);
filterResult = true;
callback("event", mockEvent);
expect(delivered).toBeTruthy();
});
it("should not deliver event when filter return false", () => {
expect.assertions(1);
filterResult = false;
callback("event", mockEvent);
expect(delivered).toBeFalsy();
});
});
});
......
......@@ -40,7 +40,7 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
private meta: MetaTable;
private enabled: boolean; // rule enabled/disabled, irregardless of severity
private severity: number; // rule severity, 0: off, 1: warning 2: error
private event: any;
private event: Event;
/**
* Rule name. Defaults to filename without extension but can be overwritten by
......@@ -58,6 +58,7 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
this.reporter = (null as unknown) as Reporter;
this.parser = (null as unknown) as Parser;
this.meta = (null as unknown) as MetaTable;
this.event = (null as unknown) as Event;
this.options = options;
this.enabled = true;
......@@ -194,23 +195,45 @@ export abstract class Rule<ContextType = void, OptionsType = void> {
* Adding listeners can be done even if the rule is disabled but for the
* events to be delivered the rule must be enabled.
*
* If the optional filter callback is used it must be a function taking an
* event of the same type as the listener. The filter is called before the
* listener and if the filter returns false the event is discarded.
*
* @param event - Event name
* @param filter - Optional filter function. Callback is only called if filter functions return true.
* @param callback - Callback to handle event.
*/
public on(event: "config:ready", callback: (event: ConfigReadyEvent) => void): void;
public on(event: "tag:open", callback: (event: TagOpenEvent) => void): void;
public on(event: "tag:close", callback: (event: TagCloseEvent) => void): void;
public on(event: "element:ready", callback: (event: ElementReadyEvent) => void): void;
public on(event: "dom:load", callback: (event: Event) => void): void;
public on(event: "dom:ready", callback: (event: DOMReadyEvent) => void): void;
public on(event: "doctype", callback: (event: DoctypeEvent) => void): void;
public on(event: "attr", callback: (event: AttributeEvent) => void): void;
public on(event: "whitespace", callback: (event: WhitespaceEvent) => void): void;
public on(event: "conditional", callback: (event: ConditionalEvent) => void): void;
public on(event: "*", callback: (event: Event) => void): void;
/* eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types */
public on(event: string, callback: any): void {
this.parser.on(event, (event: string, data: any) => {
if (this.isEnabled()) {
/* prettier-ignore */ public on(event: "config:ready", callback: (event: ConfigReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "config:ready", filter: (event: ConfigReadyEvent) => boolean, callback: (event: ConfigReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "tag:open", callback: (event: TagOpenEvent) => void): void;
/* prettier-ignore */ public on(event: "tag:open", filter: (event: TagOpenEvent) => boolean, callback: (event: TagOpenEvent) => void): void;
/* prettier-ignore */ public on(event: "tag:close", callback: (event: TagCloseEvent) => void): void;
/* prettier-ignore */ public on(event: "tag:close", filter: (event: TagCloseEvent) => boolean, callback: (event: TagCloseEvent) => void): void;
/* prettier-ignore */ public on(event: "element:ready", callback: (event: ElementReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "element:ready", filter: (event: ElementReadyEvent) => boolean, callback: (event: ElementReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "dom:load", callback: (event: Event) => void): void;
/* prettier-ignore */ public on(event: "dom:load", filter: (event: Event) => boolean, callback: (event: Event) => void): void;
/* prettier-ignore */ public on(event: "dom:ready", callback: (event: DOMReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "dom:ready", filter: (event: DOMReadyEvent) => boolean, callback: (event: DOMReadyEvent) => void): void;
/* prettier-ignore */ public on(event: "doctype", callback: (event: DoctypeEvent) => void): void;
/* prettier-ignore */ public on(event: "doctype", filter: (event: DoctypeEvent) => boolean, callback: (event: DoctypeEvent) => void): void;
/* prettier-ignore */ public on(event: "attr", callback: (event: AttributeEvent) => void): void;
/* prettier-ignore */ public on(event: "attr", filter: (event: AttributeEvent) => boolean, callback: (event: AttributeEvent) => void): void;
/* prettier-ignore */ public on(event: "whitespace", callback: (event: WhitespaceEvent) => void): void;
/* prettier-ignore */ public on(event: "whitespace", filter: (event: WhitespaceEvent) => boolean, callback: (event: WhitespaceEvent) => void): void;
/* prettier-ignore */ public on(event: "conditional", callback: (event: ConditionalEvent) => void): void;
/* prettier-ignore */ public on(event: "conditional", filter: (event: ConditionalEvent) => boolean, callback: (event: ConditionalEvent) => void): void;
/* prettier-ignore */ public on(event: "*", callback: (event: Event) => void): void;
/* prettier-ignore */ public on(event: "*", filter: (event: Event) => boolean, callback: (event: Event) => void): void;
public on<TEvent extends Event>(
event: string,
...args: [(event: TEvent) => void] | [(event: TEvent) => boolean, (event: TEvent) => void]
): void {
const callback = args.pop() as (event: TEvent) => void;
const filter = (args.pop() as (event: TEvent) => boolean) ?? (() => true);
this.parser.on(event, (_event: string, data: TEvent) => {
if (this.isEnabled() && filter(data)) {
this.event = data;
callback(data);
}
......
Markdown is supported
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