diff --git a/README.md b/README.md
index b2e746eeb..f92b2633e 100644
--- a/README.md
+++ b/README.md
@@ -292,6 +292,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| Rule ID | Description | |
|:--------|:------------|:---|
| [svelte/first-attribute-linebreak](https://ota-meshi.github.io/eslint-plugin-svelte/rules/first-attribute-linebreak/) | enforce the location of first attribute | :wrench: |
+| [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: |
| [svelte/html-quotes](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-quotes/) | enforce quotes style of HTML attributes | :wrench: |
| [svelte/indent](https://ota-meshi.github.io/eslint-plugin-svelte/rules/indent/) | enforce consistent indentation | :wrench: |
| [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: |
diff --git a/docs/rules.md b/docs/rules.md
index 70a940c90..85077d75e 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -52,6 +52,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
| Rule ID | Description | |
|:--------|:------------|:---|
| [svelte/first-attribute-linebreak](./rules/first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
+| [svelte/html-closing-bracket-spacing](./rules/html-closing-bracket-spacing.md) | require or disallow a space before tag's closing brackets | :wrench: |
| [svelte/html-quotes](./rules/html-quotes.md) | enforce quotes style of HTML attributes | :wrench: |
| [svelte/indent](./rules/indent.md) | enforce consistent indentation | :wrench: |
| [svelte/max-attributes-per-line](./rules/max-attributes-per-line.md) | enforce the maximum number of attributes per line | :wrench: |
diff --git a/docs/rules/html-closing-bracket-spacing.md b/docs/rules/html-closing-bracket-spacing.md
new file mode 100644
index 000000000..b514cdc05
--- /dev/null
+++ b/docs/rules/html-closing-bracket-spacing.md
@@ -0,0 +1,77 @@
+---
+pageClass: "rule-details"
+sidebarDepth: 0
+title: "svelte/html-closing-bracket-spacing"
+description: "require or disallow a space before tag's closing brackets"
+---
+
+# svelte/html-closing-bracket-spacing
+
+> require or disallow a space before tag's closing brackets
+
+- :exclamation: **_This rule has not been released yet._**
+- :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.
+
+## :book: Rule Details
+
+You can choose either two styles for spacing before closing bracket
+
+- always: `
`
+- never: ``
+
+
+
+
+
+
+```svelte
+
+
+
+
+Hello
+
+
+
+
+
+Hello
+
+
+```
+
+
+
+
+
+## :wrench: Options
+
+```json
+{
+ "svelte/html-closing-bracket-spacing": [
+ "error",
+ {
+ "startTag": "never", // or "always" or "ignore"
+ "endTag": "never", // or "always" or "ignore"
+ "selfClosingTag": "always" // or "never" or "ignore"
+ }
+ ]
+}
+```
+
+- `startTag` (`"never"` by default)... Spacing in start tags
+- `endTag` (`"never"` by default)... Spacing in end tags
+- `selfClosingTag` (`"always"` by default)... Spacing in self closing tags
+
+Every option can be set to
+- "always" (``)
+- "never" (``)
+- "ignore" (either `` or ``)
+
+## :mag: Implementation
+
+- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/html-closing-bracket-spacing.ts)
+- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/html-closing-bracket-spacing.ts)
diff --git a/src/configs/prettier.ts b/src/configs/prettier.ts
index 6c0eba387..fa92608da 100644
--- a/src/configs/prettier.ts
+++ b/src/configs/prettier.ts
@@ -7,6 +7,7 @@ export = {
rules: {
// eslint-plugin-svelte rules
"svelte/first-attribute-linebreak": "off",
+ "svelte/html-closing-bracket-spacing": "off",
"svelte/html-quotes": "off",
"svelte/indent": "off",
"svelte/max-attributes-per-line": "off",
diff --git a/src/rules/html-closing-bracket-spacing.ts b/src/rules/html-closing-bracket-spacing.ts
new file mode 100644
index 000000000..a69b16334
--- /dev/null
+++ b/src/rules/html-closing-bracket-spacing.ts
@@ -0,0 +1,109 @@
+import { createRule } from "../utils"
+import type { AST } from "svelte-eslint-parser"
+
+export default createRule("html-closing-bracket-spacing", {
+ meta: {
+ docs: {
+ description: "require or disallow a space before tag's closing brackets",
+ category: "Stylistic Issues",
+ conflictWithPrettier: true,
+ recommended: false,
+ },
+ schema: [
+ {
+ type: "object",
+ properties: {
+ startTag: {
+ enum: ["always", "never", "ignore"],
+ },
+ endTag: {
+ enum: ["always", "never", "ignore"],
+ },
+ selfClosingTag: {
+ enum: ["always", "never", "ignore"],
+ },
+ },
+ additionalProperties: false,
+ },
+ ],
+ messages: {
+ expectedSpace: "Expected space before '>', but not found.",
+ unexpectedSpace: "Expected no space before '>', but found.",
+ },
+ fixable: "whitespace",
+ type: "layout",
+ },
+ create(ctx) {
+ const options = {
+ startTag: "never",
+ endTag: "never",
+ selfClosingTag: "always",
+ ...ctx.options[0],
+ }
+ const src = ctx.getSourceCode()
+
+ /**
+ * Returns true if string contains newline characters
+ */
+ function containsNewline(string: string): boolean {
+ return string.includes("\n")
+ }
+
+ /**
+ * Report
+ */
+ function report(
+ node: AST.SvelteStartTag | AST.SvelteEndTag,
+ shouldHave: boolean,
+ ) {
+ const tagSrc = src.getText(node)
+ const match = /(\s*)\/?>$/.exec(tagSrc)
+
+ const end = node.range[1]
+ const start = node.range[1] - match![0].length
+ const loc = {
+ start: src.getLocFromIndex(start),
+ end: src.getLocFromIndex(end),
+ }
+
+ ctx.report({
+ loc,
+ messageId: shouldHave ? "expectedSpace" : "unexpectedSpace",
+ *fix(fixer) {
+ if (shouldHave) {
+ yield fixer.insertTextBeforeRange([start, end], " ")
+ } else {
+ const spaces = match![1]
+
+ yield fixer.removeRange([start, start + spaces.length])
+ }
+ },
+ })
+ }
+
+ return {
+ "SvelteStartTag, SvelteEndTag"(
+ node: AST.SvelteStartTag | AST.SvelteEndTag,
+ ) {
+ const tagType =
+ node.type === "SvelteEndTag"
+ ? "endTag"
+ : node.selfClosing
+ ? "selfClosingTag"
+ : "startTag"
+
+ if (options[tagType] === "ignore") return
+
+ const tagSrc = src.getText(node)
+ const match = /(\s*)\/?>$/.exec(tagSrc)
+ if (containsNewline(match![1])) return
+
+ if (options[tagType] === "always" && !match![1]) {
+ report(node, true)
+ } else if (options[tagType] === "never" && match![1]) {
+ report(node, false)
+ }
+ },
+ }
+ },
+})
diff --git a/src/utils/rules.ts b/src/utils/rules.ts
index 19fe075fd..5bff9ab49 100644
--- a/src/utils/rules.ts
+++ b/src/utils/rules.ts
@@ -2,6 +2,7 @@ import type { RuleModule } from "../types"
import buttonHasType from "../rules/button-has-type"
import commentDirective from "../rules/comment-directive"
import firstAttributeLinebreak from "../rules/first-attribute-linebreak"
+import htmlClosingBracketSpacing from "../rules/html-closing-bracket-spacing"
import htmlQuotes from "../rules/html-quotes"
import indent from "../rules/indent"
import maxAttributesPerLine from "../rules/max-attributes-per-line"
@@ -32,6 +33,7 @@ export const rules = [
buttonHasType,
commentDirective,
firstAttributeLinebreak,
+ htmlClosingBracketSpacing,
htmlQuotes,
indent,
maxAttributesPerLine,
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json
new file mode 100644
index 000000000..e5e1e4d84
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/_config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "selfClosingTag": "ignore"
+ }
+ ]
+}
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json
new file mode 100644
index 000000000..8e0d94be0
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 3
+ },
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 14
+ }
+]
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte
new file mode 100644
index 000000000..0aa1e17af
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-input.svelte
@@ -0,0 +1,5 @@
+
+Hello
+
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte
new file mode 100644
index 000000000..ee4523394
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/closing-ignore/closing-ignore-output.svelte
@@ -0,0 +1,5 @@
+
+Hello
+
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json
new file mode 100644
index 000000000..c2b607b39
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/_config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "endTag": "ignore"
+ }
+ ]
+}
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json
new file mode 100644
index 000000000..130ab5d28
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 3
+ },
+ {
+ "message": "Expected space before '>', but not found.",
+ "line": 5,
+ "column": 5
+ }
+]
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte
new file mode 100644
index 000000000..97408e4d3
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-input.svelte
@@ -0,0 +1,5 @@
+
+Hello
+Hi
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte
new file mode 100644
index 000000000..8b8916c1b
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/end-ignore/end-ignore-output.svelte
@@ -0,0 +1,5 @@
+
+Hello
+Hi
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json
new file mode 100644
index 000000000..e0a9668b5
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/_config.json
@@ -0,0 +1,7 @@
+{
+ "options": [
+ {
+ "startTag": "ignore"
+ }
+ ]
+}
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json
new file mode 100644
index 000000000..bb1c24ac3
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-errors.json
@@ -0,0 +1,12 @@
+[
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 14
+ },
+ {
+ "message": "Expected space before '>', but not found.",
+ "line": 5,
+ "column": 5
+ }
+]
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte
new file mode 100644
index 000000000..97408e4d3
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-input.svelte
@@ -0,0 +1,5 @@
+
+Hello
+Hi
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte
new file mode 100644
index 000000000..644df5de0
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/start-ignore/start-ignore-output.svelte
@@ -0,0 +1,5 @@
+
+Hello
+Hi
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json
new file mode 100644
index 000000000..bababc39e
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-errors.json
@@ -0,0 +1,17 @@
+[
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 3
+ },
+ {
+ "message": "Expected no space before '>', but found.",
+ "line": 2,
+ "column": 14
+ },
+ {
+ "message": "Expected space before '>', but not found.",
+ "line": 4,
+ "column": 5
+ }
+]
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte
new file mode 100644
index 000000000..7b0b7117d
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-input.svelte
@@ -0,0 +1,10 @@
+
+Hello
+
+
+
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte
new file mode 100644
index 000000000..2cbd69667
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/invalid/test-01-output.svelte
@@ -0,0 +1,10 @@
+
+Hello
+
+
+
+
+
diff --git a/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte b/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte
new file mode 100644
index 000000000..e40adb0e0
--- /dev/null
+++ b/tests/fixtures/rules/html-closing-bracket-spacing/valid/test-01-input.svelte
@@ -0,0 +1,11 @@
+Hello
+
+
+
+
+
+
diff --git a/tests/src/rules/html-closing-bracket-spacing.ts b/tests/src/rules/html-closing-bracket-spacing.ts
new file mode 100644
index 000000000..80cbe4562
--- /dev/null
+++ b/tests/src/rules/html-closing-bracket-spacing.ts
@@ -0,0 +1,16 @@
+import { RuleTester } from "eslint"
+import rule from "../../../src/rules/html-closing-bracket-spacing"
+import { loadTestCases } from "../../utils/utils"
+
+const tester = new RuleTester({
+ parserOptions: {
+ ecmaVersion: 2020,
+ sourceType: "module",
+ },
+})
+
+tester.run(
+ "html-closing-bracket-spacing",
+ rule as any,
+ loadTestCases("html-closing-bracket-spacing"),
+)