-
-
Notifications
You must be signed in to change notification settings - Fork 48
/
Copy pathhtml-self-closing.ts
141 lines (125 loc) · 3.81 KB
/
html-self-closing.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import type { AST } from "svelte-eslint-parser"
import { createRule } from "../utils"
import { getNodeName, isVoidHtmlElement } from "../utils/ast-utils"
const TYPE_MESSAGES = {
normal: "HTML elements",
void: "HTML void elements",
component: "Svelte custom components",
svelte: "Svelte special elements",
}
type ElementTypes = "normal" | "void" | "component" | "svelte"
export default createRule("html-self-closing", {
meta: {
docs: {
description: "enforce self-closing style",
category: "Stylistic Issues",
recommended: false,
conflictWithPrettier: true,
},
type: "layout",
fixable: "code",
messages: {
requireClosing: "Require self-closing on {{type}}.",
disallowClosing: "Disallow self-closing on {{type}}.",
},
schema: [
{
type: "object",
properties: {
void: {
enum: ["never", "always", "ignore"],
},
normal: {
enum: ["never", "always", "ignore"],
},
component: {
enum: ["never", "always", "ignore"],
},
svelte: {
enum: ["never", "always", "ignore"],
},
},
additionalProperties: false,
},
],
},
create(ctx) {
const options = {
void: "always",
normal: "always",
component: "always",
svelte: "always",
...ctx.options?.[0],
}
/**
* Get SvelteElement type.
* If element is custom component "component" is returned
* If element is svelte special element such as svelte:self "svelte" is returned
* If element is void element "void" is returned
* otherwise "normal" is returned
*/
function getElementType(node: AST.SvelteElement): ElementTypes {
if (node.kind === "component") return "component"
if (node.kind === "special") return "svelte"
if (isVoidHtmlElement(node)) return "void"
return "normal"
}
/**
* Returns true if element has no children, or has only whitespace text
*/
function isElementEmpty(node: AST.SvelteElement): boolean {
if (node.children.length <= 0) return true
for (const child of node.children) {
if (child.type !== "SvelteText") return false
if (!/^\s*$/.test(child.value)) return false
}
return true
}
/**
* Report
*/
function report(node: AST.SvelteElement, close: boolean) {
const elementType = getElementType(node)
ctx.report({
node,
messageId: close ? "requireClosing" : "disallowClosing",
data: {
type: TYPE_MESSAGES[elementType],
},
*fix(fixer) {
if (close) {
for (const child of node.children) {
yield fixer.removeRange(child.range)
}
yield fixer.insertTextBeforeRange(
[node.startTag.range[1] - 1, node.startTag.range[1]],
"/",
)
if (node.endTag) yield fixer.removeRange(node.endTag.range)
} else {
yield fixer.removeRange([
node.startTag.range[1] - 2,
node.startTag.range[1] - 1,
])
if (!isVoidHtmlElement(node))
yield fixer.insertTextAfter(node, `</${getNodeName(node)}>`)
}
},
})
}
return {
SvelteElement(node: AST.SvelteElement) {
if (!isElementEmpty(node)) return
const elementType = getElementType(node)
const elementTypeOptions = options[elementType]
if (elementTypeOptions === "ignore") return
const shouldBeClosed = elementTypeOptions === "always"
if (shouldBeClosed && !node.startTag.selfClosing) {
report(node, true)
} else if (!shouldBeClosed && node.startTag.selfClosing) {
report(node, false)
}
},
}
},
})