Skip to content

feat: add svelte/html-self-closing #190

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
|:--------|:------------|:---|
| [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-quotes](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-quotes/) | enforce quotes style of HTML attributes | :wrench: |
| [svelte/html-self-closing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/html-self-closing/) | enforce self-closing style | :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: |
| [svelte/mustache-spacing](https://ota-meshi.github.io/eslint-plugin-svelte/rules/mustache-spacing/) | enforce unified spacing in mustache | :wrench: |
Expand Down
1 change: 1 addition & 0 deletions docs/rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ These rules relate to style guidelines, and are therefore quite subjective:
|:--------|:------------|:---|
| [svelte/first-attribute-linebreak](./rules/first-attribute-linebreak.md) | enforce the location of first attribute | :wrench: |
| [svelte/html-quotes](./rules/html-quotes.md) | enforce quotes style of HTML attributes | :wrench: |
| [svelte/html-self-closing](./rules/html-self-closing.md) | enforce self-closing style | :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: |
| [svelte/mustache-spacing](./rules/mustache-spacing.md) | enforce unified spacing in mustache | :wrench: |
Expand Down
78 changes: 78 additions & 0 deletions docs/rules/html-self-closing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
pageClass: "rule-details"
sidebarDepth: 0
title: "svelte/html-self-closing"
description: "enforce self-closing style"
---

# svelte/html-self-closing

> enforce self-closing style

- :exclamation: <badge text="This rule has not been released yet." vertical="middle" type="error"> **_This rule has not been released yet._** </badge>
- :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 elements without content

- always: `<div />`
- never: `<div></div>`

<ESLintCodeBlock fix>

<!-- prettier-ignore-start -->
<!--eslint-skip-->

```svelte
<script>
/* eslint svelte/html-self-closing: "error" */
</script>

<!-- ✓ GOOD -->
<div />
Copy link
Collaborator

@JounQin JounQin Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes the template less html compliant, div is not self-closing in HTML, we should have three options: 'all' | 'html-compliant' | 'none', and 'html-compliant' should be default IMO.

Of course the options naming can be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

div is not self-closing in HTML
'html-compliant' should be default IMO.

I think we should make the default of prettier-plugin-svelte the default behavior rather than the HTML standard. So I think all should be the default.
prettier-plugin-svelte has long been the default style for the community.
Turn off these rules when users use plugins with prettier-plugin-svelte, but I think it makes sense to adopt the same style as prettier-plugin-svelte by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should have three options: 'all' | 'html-compliant' | 'none'

I think the current options can already do the same. Why do you think we need three options?

{
  "svelte/html-self-closing": [
    "error",
    {
      "void": "always", // or "always" or "ignore"
      "normal": "always", // or "never" or "ignore"
      "component": "always", // or "never" or "ignore"
      "svelte": "always" // or "never" or "ignore"
    }
  ]
}
  • void ("always" by default)... Style of HTML void elements
  • component ("always" by default)... Style of svelte components
  • svelte ("always" by default)... Style of svelte special elements (<svelte:head>, <svelte:self>)
  • normal ("always" by default)... Style of other elements

Every option can be set to

  • "always" (<div />)
  • "never" (<div></div>)
  • "ignore" (either <div /> or <div></div>)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh. I didn't know that.
Hmm. The maintainers don't seem to be involved in the discussion, so I'm still not sure if they're thinking of doing so by default.

Copy link
Collaborator

@JounQin JounQin Jul 28, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The linked option is not related to tag closing, it's related to whitespace and prettier-plugin-svelte already adheres to that option.

I know, I'm just saying we can have such single option, maybe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know, I'm just saying we can have such single option, maybe.

That's a good idea, but I'd also keep the current configuration in overrides section, because there could be someone who only wants self closing on tags in specific category, and it would be waste to just throw away the option to set that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@marekvospel Do you currently have time to work on this PR?

I think the new single option is that we can add features later, so if you don't have time now, after merging this PR, we can work on a new PR when you/we have time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ota-meshi no, I'll have time in 2 weeks or later. I'll create a new PR when I have time

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay! I will merge this PR. Thank you for the great job!

<p>Hello</p>
<div><div /></div>
<img>

<!-- ✗ BAD -->
<div></div>
<p> </p>
<div><div></div></div>
<img />
```

<!-- prettier-ignore-end -->

</ESLintCodeBlock>

## :wrench: Options

```json
{
"svelte/html-self-closing": [
"error",
{
"html": {
"void": "always", // or "always" or "ignore"
"normal": "always", // or "never" or "ignore"
"component": "always" // or "never" or "ignore"
}
}
]
}
```

- `html.void` (`"always"` by default)... Style of HTML void elements
- `html.component` (`"always"` by default)... Style of svelte components
- `html.normal` (`"always"` by default)... Style of other elements

Every option can be set to
- "always" (`<div />`)
- "never" (`<div></div>`)
- "ignore" (either `<div />` or `<div></div>`)

## :mag: Implementation

- [Rule source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/src/rules/html-self-closing.ts)
- [Test source](https://github.com/ota-meshi/eslint-plugin-svelte/blob/main/tests/src/rules/html-self-closing.ts)
1 change: 1 addition & 0 deletions src/configs/prettier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export = {
// eslint-plugin-svelte rules
"svelte/first-attribute-linebreak": "off",
"svelte/html-quotes": "off",
"svelte/html-self-closing": "off",
"svelte/indent": "off",
"svelte/max-attributes-per-line": "off",
"svelte/mustache-spacing": "off",
Expand Down
146 changes: 146 additions & 0 deletions src/rules/html-self-closing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import type { AST } from "svelte-eslint-parser"
import { createRule } from "../utils"
import {
getNodeName,
isCustomComponent,
isVoidHtmlElement,
} from "../utils/template-utils"

const TYPE_MESSAGES = {
normal: "HTML elements",
void: "HTML void elements",
component: "Svelte custom components",
}

type ElementTypes = "normal" | "void" | "component"

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: {
html: {
type: "object",
properties: {
void: {
enum: ["never", "always", "ignore"],
},
normal: {
enum: ["never", "always", "ignore"],
},
component: {
enum: ["never", "always", "ignore"],
},
},
additionalProperties: false,
},
},
additionalProperties: false,
},
],
},
create(ctx) {
const options = {
html: {
void: "always",
normal: "always",
component: "always",
},
...ctx.options?.[0],
}

/**
* Get SvelteElement type.
* If element is custom component "component" is returned
* If element is void element "void" is returned
* otherwise "normal" is returned
*/
function getElementType(node: AST.SvelteElement): ElementTypes {
if (isCustomComponent(node)) return "component"
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.html[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)
}
},
}
},
})
2 changes: 2 additions & 0 deletions src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import buttonHasType from "../rules/button-has-type"
import commentDirective from "../rules/comment-directive"
import firstAttributeLinebreak from "../rules/first-attribute-linebreak"
import htmlQuotes from "../rules/html-quotes"
import htmlSelfClosing from "../rules/html-self-closing"
import indent from "../rules/indent"
import maxAttributesPerLine from "../rules/max-attributes-per-line"
import mustacheSpacing from "../rules/mustache-spacing"
Expand Down Expand Up @@ -33,6 +34,7 @@ export const rules = [
commentDirective,
firstAttributeLinebreak,
htmlQuotes,
htmlSelfClosing,
indent,
maxAttributesPerLine,
mustacheSpacing,
Expand Down
24 changes: 24 additions & 0 deletions src/utils/template-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { AST } from "svelte-eslint-parser"
import voidElements from "./void-elements"

/**
* Returns name of SvelteElement
*/
export function getNodeName(node: AST.SvelteElement): string {
return "name" in node.name ? node.name.name : node.name.property.name
}

/**
* Returns true if Element is custom component
*/
export function isCustomComponent(node: AST.SvelteElement): boolean {
return node.kind === "component"
}

/**
* Returns true if element is known void element
* {@link https://developer.mozilla.org/en-US/docs/Glossary/Empty_element}
*/
export function isVoidHtmlElement(node: AST.SvelteElement): boolean {
return voidElements.includes(getNodeName(node))
}
20 changes: 20 additions & 0 deletions src/utils/void-elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const voidElements = [
"area",
"base",
"br",
"col",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"menuitem",
"meta",
"param",
"source",
"track",
"wbr",
]

export default voidElements
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"options": [
{
"html": {
"component": "never"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Disallow self-closing on Svelte custom components.",
"line": 3,
"column": 3
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- prettier-ignore -->
<div>
<CustomElement />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- prettier-ignore -->
<div>
<CustomElement ></CustomElement>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"options": [
{
"html": {
"normal": "ignore"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Disallow self-closing on HTML void elements.",
"line": 5,
"column": 3
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!-- prettier-ignore -->
<div>
<div />
<div></div>
<img />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<!-- prettier-ignore -->
<div>
<div />
<div></div>
<img >
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"options": [
{
"html": {
"normal": "never"
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"message": "Disallow self-closing on HTML elements.",
"line": 3,
"column": 3
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- prettier-ignore -->
<div>
<div />
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!-- prettier-ignore -->
<div>
<div ></div>
</div>
Loading