Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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). |

Expand All @@ -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
<!-- Input -->
<searchLayouts></searchLayouts>

<!-- Output (default: "always") -->
<searchLayouts />
```

If you want to preserve the original formatting from your source files, use `"preserve"`:

```xml
<!-- Input -->
<searchLayouts></searchLayouts>
<otherTag />

<!-- Output (preserve) -->
<searchLayouts></searchLayouts>
<otherTag />
```

If you want to prevent self-closing tags entirely and always use open/close format, use `"never"`:

```xml
<!-- Input -->
<searchLayouts />

<!-- Output (never) -->
<searchLayouts></searchLayouts>
```

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.
Expand Down
21 changes: 21 additions & 0 deletions src/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
61 changes: 49 additions & 12 deletions src/printer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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
Expand Down
Loading