void-style.ts 3.14 KB
Newer Older
1
import { HtmlElement, NodeClosed } from "../dom";
2
import { TagEndEvent } from "../event";
3
import { Rule, RuleDocumentation, ruleDocumentationUrl, SchemaObject } from "../rule";
4
5
6
7
8
9
10
11
12
13
14
15

enum Style {
	AlwaysOmit = 1,
	AlwaysSelfclose = 2,
}

interface RuleContext {
	style: Style;
	tagName: string;
}

interface RuleOptions {
16
	style: "omit" | "selfclose" | "selfclosing";
17
18
}

19
20
21
22
const defaults: RuleOptions = {
	style: "omit",
};

23
export default class VoidStyle extends Rule<RuleContext, RuleOptions> {
24
25
	private style: Style;

26
	public constructor(options: Partial<RuleOptions>) {
27
		super({ ...defaults, ...options });
28
29
30
		this.style = parseStyle(this.options.style);
	}

31
32
33
34
35
36
37
38
39
	public static schema(): SchemaObject {
		return {
			style: {
				enum: ["omit", "selfclose", "selfclosing"],
				type: "string",
			},
		};
	}

40
41
	public documentation(context: RuleContext): RuleDocumentation {
		const doc: RuleDocumentation = {
42
			description: "The current configuration requires a specific style for ending void elements.",
43
44
45
46
47
48
49
50
51
52
53
54
			url: ruleDocumentationUrl(__filename),
		};

		if (context) {
			const [desc, end] = styleDescription(context.style);
			doc.description = `The current configuration requires void elements to ${desc}, use <${context.tagName}${end}> instead.`;
		}

		return doc;
	}

	public setup(): void {
55
		this.on("tag:end", (event: TagEndEvent) => {
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
			const active = event.previous; // The current active element (that is, the current element on the stack)

			if (active && active.meta) {
				this.validateActive(active);
			}
		});
	}

	private validateActive(node: HtmlElement): void {
		/* ignore non-void elements, they must be closed with regular end tag */
		if (!node.voidElement) {
			return;
		}

		if (this.shouldBeOmitted(node)) {
			this.report(
				node,
				`Expected omitted end tag <${node.tagName}> instead of self-closing element <${node.tagName}/>`
			);
		}

		if (this.shouldBeSelfClosed(node)) {
			this.report(
				node,
				`Expected self-closing element <${node.tagName}/> instead of omitted end-tag <${node.tagName}>`
			);
		}
	}

	public report(node: HtmlElement, message: string): void {
		const context: RuleContext = {
			style: this.style,
			tagName: node.tagName,
		};
		super.report(node, message, null, context);
	}

	private shouldBeOmitted(node: HtmlElement): boolean {
94
		return this.style === Style.AlwaysOmit && node.closed === NodeClosed.VoidSelfClosed;
95
96
97
	}

	private shouldBeSelfClosed(node: HtmlElement): boolean {
98
		return this.style === Style.AlwaysSelfclose && node.closed === NodeClosed.VoidOmitted;
99
100
101
102
103
104
105
106
107
108
	}
}

function parseStyle(name: string): Style {
	switch (name) {
		case "omit":
			return Style.AlwaysOmit;
		case "selfclose":
		case "selfclosing":
			return Style.AlwaysSelfclose;
109
		/* istanbul ignore next: covered by schema validation */
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
		default:
			throw new Error(`Invalid style "${name}" for "void-style" rule`);
	}
}

function styleDescription(style: Style): [string, string] {
	switch (style) {
		case Style.AlwaysOmit:
			return ["omit end tag", ""];
		case Style.AlwaysSelfclose:
			return ["be self-closed", "/"];
		// istanbul ignore next: will only happen if new styles are added, otherwise this isn't reached
		default:
			throw new Error(`Unknown style`);
	}
}