Skip to content

Commit 671dfca

Browse files
authored
feat: add svelte/html-closing-bracket-spacing (#186)
* feat: add `svelte/html-self-closing` rule * chore: add .idea to gitignore * chore: increase coverage * fix(html-self-closing): ignore if element children is whitespace text, fixer remove child whitespace * fix(html-self-closing): add / remove closing from void elements * feat: add `svelte/no-spaces-around-equal-signs-in-attribute` rule * fix(no-spaces-around-equal-signs-in-attribute): throw error even if there is no value (key="") * chore(html-self-closing): naming changes * docs: add missing jsdoc content * chore: add "any" option to html-self-closing, add rules documentation * fix: since version in `html-self-closing` and `no-spaces-around-equal-signs-in-attribute` * feat: add `svelte/html-closing-bracket-spacing` rule * docs: add `html-closing-bracket-spacing` docs * chore: lint * chore: some requested changes * docs: fix incorrect default info * chore: use startTag.selfClosing instead of checking for "/". Actually rename any to ignore in html-closing-bracket-spacing * chore: remove .idea from gitignore, disable resolveJsonModule * chore: move each rule into separate PR * chore: change fixable to whitespace
1 parent 1f73a20 commit 671dfca

23 files changed

+342
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
292292
| Rule ID | Description | |
293293
|:--------|:------------|:---|
294294
| [svelte/first-attribute-linebreak](https://ota-meshi.github.io/eslint-plugin-svelte/rules/first-attribute-linebreak/) | enforce the location of first attribute | :wrench: |
295+
| [svelte/html-closing-bracket-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-closing-bracket-spacing/) | require or disallow a space before tag's closing brackets | :wrench: |
295296
| [svelte/html-quotes](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-quotes/) | enforce quotes style of HTML attributes | :wrench: |
296297
| [svelte/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent/) | enforce consistent indentation | :wrench: |
297298
| [svelte/max-attributes-per-line](https://ota-meshi.github.io/eslint-plugin-svelte/rules/max-attributes-per-line/) | enforce the maximum number of attributes per line | :wrench: |

docs/rules.md

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
5252
| Rule ID | Description | |
5353
|:--------|:------------|:---|
5454
| [svelte/first-attribute-linebreak](./rules/first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
55+
| [svelte/html-closing-bracket-spacing](./rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
5556
| [svelte/html-quotes](./rules/html-quotes.md) | enforce quotes style of HTML attributes | :wrench: |
5657
| [svelte/indent](./rules/indent.md) | enforce consistent indentation | :wrench: |
5758
| [svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
---
2+
pageClass: "rule-details"
3+
sidebarDepth: 0
4+
title: "svelte/html-closing-bracket-spacing"
5+
description: "require or disallow a space before tag's closing brackets"
6+
---
7+
8+
# svelte/html-closing-bracket-spacing
9+
10+
> require or disallow a space before tag's closing brackets
11+
12+
- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
13+
- :wrench: The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
14+
15+
## :book: Rule Details
16+
17+
You can choose either two styles for spacing before closing bracket
18+
19+
- always: `<div />`
20+
- never: `<div/>`
21+
22+
<ESLintCodeBlock fix>
23+
24+
<!-- prettier-ignore-start -->
25+
<!--eslint-skip-->
26+
27+
```svelte
28+
<script>
29+
/* eslint svelte/html-closing-bracket-spacing: "error" */
30+
</script>
31+
32+
<!-- ✓ GOOD -->
33+
<div />
34+
<p>Hello</p>
35+
<div
36+
>
37+
</div>
38+
39+
<!-- ✗ BAD -->
40+
<div/>
41+
<p >Hello</p >
42+
<div >
43+
</div >
44+
```
45+
46+
<!-- prettier-ignore-end -->
47+
48+
</ESLintCodeBlock>
49+
50+
## :wrench: Options
51+
52+
```json
53+
{
54+
"svelte/html-closing-bracket-spacing": [
55+
"error",
56+
{
57+
"startTag": "never", // or "always" or "ignore"
58+
"endTag": "never", // or "always" or "ignore"
59+
"selfClosingTag": "always" // or "never" or "ignore"
60+
}
61+
]
62+
}
63+
```
64+
65+
- `startTag` (`"never"` by default)... Spacing in start tags
66+
- `endTag` (`"never"` by default)... Spacing in end tags
67+
- `selfClosingTag` (`"always"` by default)... Spacing in self closing tags
68+
69+
Every option can be set to
70+
- "always" (`<div />`)
71+
- "never" (`<div/>`)
72+
- "ignore" (either `<div />` or `<div/>`)
73+
74+
## :mag: Implementation
75+
76+
- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/html-closing-bracket-spacing.ts)
77+
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/html-closing-bracket-spacing.ts)

src/configs/prettier.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export = {
77
rules: {
88
// eslint-plugin-svelte rules
99
"svelte/first-attribute-linebreak": "off",
10+
"svelte/html-closing-bracket-spacing": "off",
1011
"svelte/html-quotes": "off",
1112
"svelte/indent": "off",
1213
"svelte/max-attributes-per-line": "off",
+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { createRule } from "../utils"
2+
import type { AST } from "svelte-eslint-parser"
3+
4+
export default createRule("html-closing-bracket-spacing", {
5+
meta: {
6+
docs: {
7+
description: "require or disallow a space before tag's closing brackets",
8+
category: "Stylistic Issues",
9+
conflictWithPrettier: true,
10+
recommended: false,
11+
},
12+
schema: [
13+
{
14+
type: "object",
15+
properties: {
16+
startTag: {
17+
enum: ["always", "never", "ignore"],
18+
},
19+
endTag: {
20+
enum: ["always", "never", "ignore"],
21+
},
22+
selfClosingTag: {
23+
enum: ["always", "never", "ignore"],
24+
},
25+
},
26+
additionalProperties: false,
27+
},
28+
],
29+
messages: {
30+
expectedSpace: "Expected space before '>', but not found.",
31+
unexpectedSpace: "Expected no space before '>', but found.",
32+
},
33+
fixable: "whitespace",
34+
type: "layout",
35+
},
36+
create(ctx) {
37+
const options = {
38+
startTag: "never",
39+
endTag: "never",
40+
selfClosingTag: "always",
41+
...ctx.options[0],
42+
}
43+
const src = ctx.getSourceCode()
44+
45+
/**
46+
* Returns true if string contains newline characters
47+
*/
48+
function containsNewline(string: string): boolean {
49+
return string.includes("\n")
50+
}
51+
52+
/**
53+
* Report
54+
*/
55+
function report(
56+
node: AST.SvelteStartTag | AST.SvelteEndTag,
57+
shouldHave: boolean,
58+
) {
59+
const tagSrc = src.getText(node)
60+
const match = /(\s*)\/?>$/.exec(tagSrc)
61+
62+
const end = node.range[1]
63+
const start = node.range[1] - match![0].length
64+
const loc = {
65+
start: src.getLocFromIndex(start),
66+
end: src.getLocFromIndex(end),
67+
}
68+
69+
ctx.report({
70+
loc,
71+
messageId: shouldHave ? "expectedSpace" : "unexpectedSpace",
72+
*fix(fixer) {
73+
if (shouldHave) {
74+
yield fixer.insertTextBeforeRange([start, end], " ")
75+
} else {
76+
const spaces = match![1]
77+
78+
yield fixer.removeRange([start, start + spaces.length])
79+
}
80+
},
81+
})
82+
}
83+
84+
return {
85+
"SvelteStartTag, SvelteEndTag"(
86+
node: AST.SvelteStartTag | AST.SvelteEndTag,
87+
) {
88+
const tagType =
89+
node.type === "SvelteEndTag"
90+
? "endTag"
91+
: node.selfClosing
92+
? "selfClosingTag"
93+
: "startTag"
94+
95+
if (options[tagType] === "ignore") return
96+
97+
const tagSrc = src.getText(node)
98+
const match = /(\s*)\/?>$/.exec(tagSrc)
99+
if (containsNewline(match![1])) return
100+
101+
if (options[tagType] === "always" && !match![1]) {
102+
report(node, true)
103+
} else if (options[tagType] === "never" && match![1]) {
104+
report(node, false)
105+
}
106+
},
107+
}
108+
},
109+
})

src/utils/rules.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { RuleModule } from "../types"
22
import buttonHasType from "../rules/button-has-type"
33
import commentDirective from "../rules/comment-directive"
44
import firstAttributeLinebreak from "../rules/first-attribute-linebreak"
5+
import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing"
56
import htmlQuotes from "../rules/html-quotes"
67
import indent from "../rules/indent"
78
import maxAttributesPerLine from "../rules/max-attributes-per-line"
@@ -33,6 +34,7 @@ export const rules = [
3334
buttonHasType,
3435
commentDirective,
3536
firstAttributeLinebreak,
37+
htmlClosingBracketSpacing,
3638
htmlQuotes,
3739
indent,
3840
maxAttributesPerLine,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"selfClosingTag": "ignore"
5+
}
6+
]
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Expected no space before '>', but found.",
4+
"line": 2,
5+
"column": 3
6+
},
7+
{
8+
"message": "Expected no space before '>', but found.",
9+
"line": 2,
10+
"column": 14
11+
}
12+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p >Hello</p >
3+
<!-- prettier-ignore -->
4+
<div/>
5+
<div />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p>Hello</p>
3+
<!-- prettier-ignore -->
4+
<div/>
5+
<div />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"endTag": "ignore"
5+
}
6+
]
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Expected no space before '>', but found.",
4+
"line": 2,
5+
"column": 3
6+
},
7+
{
8+
"message": "Expected space before '>', but not found.",
9+
"line": 5,
10+
"column": 5
11+
}
12+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p >Hello</p >
3+
<p>Hi</p>
4+
<!-- prettier-ignore -->
5+
<div/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p>Hello</p >
3+
<p>Hi</p>
4+
<!-- prettier-ignore -->
5+
<div />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"options": [
3+
{
4+
"startTag": "ignore"
5+
}
6+
]
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
[
2+
{
3+
"message": "Expected no space before '>', but found.",
4+
"line": 2,
5+
"column": 14
6+
},
7+
{
8+
"message": "Expected space before '>', but not found.",
9+
"line": 5,
10+
"column": 5
11+
}
12+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p >Hello</p >
3+
<p>Hi</p>
4+
<!-- prettier-ignore -->
5+
<div/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<!-- prettier-ignore -->
2+
<p >Hello</p>
3+
<p>Hi</p>
4+
<!-- prettier-ignore -->
5+
<div />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
[
2+
{
3+
"message": "Expected no space before '>', but found.",
4+
"line": 2,
5+
"column": 3
6+
},
7+
{
8+
"message": "Expected no space before '>', but found.",
9+
"line": 2,
10+
"column": 14
11+
},
12+
{
13+
"message": "Expected space before '>', but not found.",
14+
"line": 4,
15+
"column": 5
16+
}
17+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- prettier-ignore -->
2+
<p >Hello</p >
3+
<!-- prettier-ignore -->
4+
<div/>
5+
<!-- prettier-ignore -->
6+
<div
7+
>
8+
</div
9+
10+
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<!-- prettier-ignore -->
2+
<p>Hello</p>
3+
<!-- prettier-ignore -->
4+
<div />
5+
<!-- prettier-ignore -->
6+
<div
7+
>
8+
</div
9+
10+
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<p>Hello</p>
2+
<div />
3+
<!-- prettier-ignore -->
4+
<div
5+
/>
6+
<!-- prettier-ignore -->
7+
<div
8+
>
9+
</div
10+
11+
>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { RuleTester } from "eslint"
2+
import rule from "../../../src/rules/html-closing-bracket-spacing"
3+
import { loadTestCases } from "../../utils/utils"
4+
5+
const tester = new RuleTester({
6+
parserOptions: {
7+
ecmaVersion: 2020,
8+
sourceType: "module",
9+
},
10+
})
11+
12+
tester.run(
13+
"html-closing-bracket-spacing",
14+
rule as any,
15+
loadTestCases("html-closing-bracket-spacing"),
16+
)

0 commit comments

Comments
 (0)