diff --git a/README.md b/README.md
index 4ec1e4c..b0fda39 100644
--- a/README.md
+++ b/README.md
@@ -51,6 +51,7 @@ Below are the options (from [`src/plugin.js`](src/plugin.js)) that `@prettier/pl
| `tabWidth` | `--tab-width` | `2` | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)). |
| `xmlQuoteAttributes` | `--xml-quote-attributes` | `"preserve"` | Options are `"preserve"`, `"single"`, and `"double"` |
| `xmlSelfClosingSpace` | `--xml-self-closing-space` | `true` | Adds a space before self-closing tags. |
+| `xmlSelfClosingTags` | `--xml-self-closing-tags` | `"always"` | Controls how empty XML tags are formatted. Options are `"always"`, `"preserve"`, and `"never"`. [See below](#self-closing-tags). |
| `xmlSortAttributesByKey` | `--xml-sort-attributes-by-key` | `false` | Orders XML attributes by key alphabetically while prioritizing xmlns attributes. |
| `xmlWhitespaceSensitivity` | `--xml-whitespace-sensitivity` | `"strict"` | Options are `"strict"`, `"preserve"`, and `"ignore"`. You may want `"ignore"` or `"preserve"`, [see below](#whitespace). |
@@ -69,6 +70,48 @@ Or, they can be passed to `prettier` as arguments:
prettier --plugin=@prettier/plugin-xml --tab-width 4 --write '**/*.xml'
```
+### Self-Closing Tags
+
+The `xmlSelfClosingTags` option controls how empty XML tags (tags with no content) are formatted. By default (`"always"`), the plugin will convert all empty tags to self-closing format:
+
+```xml
+
+
+
+
+
+```
+
+If you want to preserve the original formatting from your source files, use `"preserve"`:
+
+```xml
+
+
+
+
+
+
+
+```
+
+If you want to prevent self-closing tags entirely and always use open/close format, use `"never"`:
+
+```xml
+
+
+
+
+
+```
+
+This is particularly useful for certain XML formats (like Salesforce metadata) that require specific tag formats. To use this option:
+
+```json
+{
+ "xmlSelfClosingTags": "never"
+}
+```
+
### Whitespace
In XML, by default, all whitespace inside elements has semantic meaning. For prettier to maintain its contract of not changing the semantic meaning of your program, this means the default for `xmlWhitespaceSensitivity` is `"strict"`. When running in this mode, prettier's ability to rearrange your markup is somewhat limited, as it has to maintain the exact amount of whitespace that you input within elements.
diff --git a/src/plugin.js b/src/plugin.js
index eb88416..68f4395 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -69,6 +69,27 @@ const plugin = {
"Quotes in attribute values will be converted to consistent double quotes and other quotes in the string will be escaped."
}
]
+ },
+ xmlSelfClosingTags: {
+ type: "choice",
+ category: "XML",
+ default: "always",
+ description: "Controls how empty XML tags are formatted.",
+ choices: [
+ {
+ value: "always",
+ description: "Convert empty tags to self-closing format."
+ },
+ {
+ value: "preserve",
+ description: "Preserve tags as written in source."
+ },
+ {
+ value: "never",
+ description: "Convert self-closing tags to empty open/close format."
+ }
+ ],
+ since: "3.5.0"
}
},
defaultOptions: {
diff --git a/src/printer.js b/src/printer.js
index 753ba61..e940348 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -427,28 +427,56 @@ function printElement(path, opts, print) {
space = opts.xmlSelfClosingSpace ? line : softline;
}
+ // Helper to create open/close tag pair
+ const createOpenCloseTags = () => {
+ const openTag = group([
+ ...parts,
+ opts.bracketSameLine ? "" : softline,
+ START_CLOSE || ">"
+ ]);
+ // For self-closing tags converted to open/close, use Name from opening tag
+ const closeName = END_NAME || Name;
+ const closeTag = group([SLASH_OPEN || "", closeName, END || ">"]);
+ return { openTag, closeTag };
+ };
+
+ // Handle already self-closing tags from source
if (SLASH_CLOSE) {
+ if (opts.xmlSelfClosingTags === "never") {
+ // Convert to open/close format
+ const { openTag, closeTag } = createOpenCloseTags();
+ return group([openTag, closeTag]);
+ }
+ // For "always" or "preserve", keep as self-closing
return group([...parts, space, SLASH_CLOSE]);
}
- if (
+ // Check if element is empty (no content at all)
+ const isEmpty =
content.chardata.length === 0 &&
content.CData.length === 0 &&
content.Comment.length === 0 &&
content.element.length === 0 &&
content.PROCESSING_INSTRUCTION.length === 0 &&
- content.reference.length === 0
- ) {
- return group([...parts, space, "/>"]);
+ content.reference.length === 0;
+
+ // Handle empty elements (not self-closing in source)
+ if (isEmpty) {
+ if (opts.xmlSelfClosingTags === "never") {
+ // Use open/close format
+ const { openTag, closeTag } = createOpenCloseTags();
+ return group([openTag, closeTag]);
+ } else if (opts.xmlSelfClosingTags === "preserve") {
+ // Preserve as open/close (since it wasn't self-closing in source)
+ const { openTag, closeTag } = createOpenCloseTags();
+ return group([openTag, closeTag]);
+ } else {
+ // "always" - convert to self-closing (current behavior)
+ return group([...parts, space, "/>"]);
+ }
}
- const openTag = group([
- ...parts,
- opts.bracketSameLine ? "" : softline,
- START_CLOSE
- ]);
-
- const closeTag = group([SLASH_OPEN, END_NAME, END]);
+ const { openTag, closeTag } = createOpenCloseTags();
if (isWhitespaceIgnorable(opts, Name, attribute, content)) {
const fragments = path.call(
@@ -470,7 +498,16 @@ function printElement(path, opts, print) {
}
if (fragments.length === 0) {
- return group([...parts, space, "/>"]);
+ // Element became empty after whitespace processing
+ if (opts.xmlSelfClosingTags === "never") {
+ return group([openTag, closeTag]);
+ } else if (opts.xmlSelfClosingTags === "preserve") {
+ // Was open/close in source (no SLASH_CLOSE), keep as open/close
+ return group([openTag, closeTag]);
+ } else {
+ // "always" - convert to self-closing
+ return group([...parts, space, "/>"]);
+ }
}
// If the only content of this tag is chardata, then use a softline so
diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap
index 5a89e2a..f730edc 100644
--- a/test/__snapshots__/format.test.js.snap
+++ b/test/__snapshots__/format.test.js.snap
@@ -22,6 +22,7 @@ exports[`bracketSameLine => true 1`] = `
+
+
+
+
+- 1
+ - 2
+- 3
+
+
+
+
+
+
+
+
+
+
+
+ < ignored />
+
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed at est eget
+ enim consectetur accumsan. Aliquam pretium sodales ipsum quis dignissim. Sed
+ id sem vel diam luctus fringilla. Aliquam quis egestas magna. Curabitur
+ molestie lorem et odio porta, et molestie libero laoreet. Morbi rhoncus
+ sagittis cursus. Nullam vehicula pretium consequat. Praesent porta ante at
+ posuere sollicitudin. Nullam commodo tempor arcu, at condimentum neque
+ elementum ut.
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed at est eget enim consectetur accumsan. Aliquam pretium sodales ipsum quis dignissim. Sed id sem vel diam luctus fringilla. Aliquam quis egestas magna. Curabitur molestie lorem et odio porta, et molestie libero laoreet. Morbi rhoncus sagittis cursus. Nullam vehicula pretium consequat. Praesent porta ante at posuere sollicitudin. Nullam commodo tempor arcu, at condimentum neque elementum ut.
+