Repository: prettier/plugin-xml (js)

Issue:
Option for closing bracket to be on the same line
## Metadata

- `@prettier/plugin-xml` version: '0.13.0
- Options:

Our gradle spotless config:

```
        format 'xml', {
            target '**/*.xml'
            prettier(['prettier': '2.0.5', '@prettier/plugin-xml': '0.13.0']).config(['parser': 'xml', 'tabWidth': 4])
        }
```
  - `xmlSelfClosingSpace`: true
  - `xmlWhitespaceSensitivity`: `"strict"` (be 100% sure changing this to `"ignore"` doesn't fix your issue)
      - changing it to ignore actually throws an error: `(Error while formatting: TypeError: Cannot read property 'startLine' of undefined)`

## Input

```xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:orientation="horizontal">
```

## Current output

```xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:orientation="horizontal"
>
```

## Expected output

```xml
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:orientation="horizontal" >
```


Pull request description:
Support the bracketSameLine option
Closes #171

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index d3a7c03..3591d7a 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -10,25 +10,18 @@ jobs:
       CI: true
     steps:
     - uses: actions/checkout@master
-    - uses: actions/setup-node@v2-beta
+    - uses: actions/setup-node@v2
       with:
         node-version: 14.x
-    - id: yarn-cache
-      run: echo "::set-output name=directory::$(yarn cache dir)"
-    - uses: actions/cache@v1
-      with:
-        path: ${{ steps.yarn-cache.outputs.directory }}
-        key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
-        restore-keys: |
-          ${{ runner.os }}-yarn-
-    - name: Install dependencies
-      run: yarn install --frozen-lockfile
+        cache: yarn
     - name: Lint and test
       run: |
+        yarn install --frozen-lockfile
         yarn prepublishOnly
         yarn lint
-        yarn print --check '**/*'
+        yarn prettier --plugin . --check '**/*'
         yarn test
+
   automerge:
     name: AutoMerge
     needs: ci
diff --git a/src/embed.ts b/src/embed.ts
index 2e06173..d52f9a1 100644
--- a/src/embed.ts
+++ b/src/embed.ts
@@ -2,7 +2,7 @@ import { ContentCtx, ElementCstNode } from "@xml-tools/parser";
 import type { AstPath, Doc, ParserOptions, Printer } from "prettier";
 import { builders, utils } from "prettier/doc";
 
-import { XMLAst } from "./types";
+import { XMLAst, XMLOptions } from "./types";
 
 const {
   dedentToRoot,
@@ -28,6 +28,7 @@ function replaceNewlines(doc: Doc) {
 // Get the start and end element tags from the current node on the tree
 function getElementTags(
   path: AstPath<XMLAst>,
+  opts: XMLOptions,
   print: (path: AstPath<XMLAst>) => Doc
 ) {
   const node = path.getValue() as ElementCstNode;
@@ -42,8 +43,12 @@ function getElementTags(
     );
   }
 
+  if (!opts.bracketSameLine) {
+    parts.push(softline);
+  }
+
   return {
-    openTag: group([...parts, softline, START_CLOSE[0].image]),
+    openTag: group([...parts, START_CLOSE[0].image]),
     closeTag: group([SLASH_OPEN[0].image, END_NAME[0].image, END[0].image])
   };
 }
@@ -133,7 +138,7 @@ const embed: Printer<XMLAst>["embed"] = (path, print, textToDoc, opts) => {
 
   // Get the open and close tags of this element, then return the properly
   // formatted content enclosed within them
-  const { openTag, closeTag } = getElementTags(path, print);
+  const { openTag, closeTag } = getElementTags(path, opts as XMLOptions, print);
 
   return group([
     openTag,
diff --git a/src/plugin.ts b/src/plugin.ts
index b319f71..6b91aeb 100644
--- a/src/plugin.ts
+++ b/src/plugin.ts
@@ -1,9 +1,56 @@
 import type { Plugin } from "prettier";
+import prettier from "prettier";
 
 import type { XMLAst } from "./types";
 import parser from "./parser";
 import printer from "./printer";
 
+// These are the extra options defined by this plugin.
+const options: Plugin<XMLAst>["options"] = {
+  xmlSelfClosingSpace: {
+    type: "boolean",
+    category: "XML",
+    default: true,
+    description: "Adds a space before self-closing tags.",
+    since: "0.4.0"
+  },
+  xmlWhitespaceSensitivity: {
+    type: "choice",
+    category: "XML",
+    default: "strict",
+    description: "How to handle whitespaces in XML.",
+    choices: [
+      {
+        value: "strict",
+        description: "Whitespaces are considered sensitive in all elements."
+      },
+      {
+        value: "ignore",
+        description: "Whitespaces are considered insensitive in all elements."
+      }
+    ],
+    since: "0.6.0"
+  }
+};
+
+// We're going to be using the bracketSameLine option, but since it wasn't
+// introduced until prettier 2.4.0, we need to add it to our list of options if
+// it's not present so that it gets respected.
+if (
+  !prettier
+    .getSupportInfo()
+    .options.some((opt) => opt.name === "bracketSameLine")
+) {
+  options.bracketSameLine = {
+    type: "boolean",
+    category: "Global",
+    default: false,
+    description:
+      "Put > of opening tags on the last line instead of on a new line.",
+    since: "1.0.0"
+  };
+}
+
 const plugin: Plugin<XMLAst> = {
   languages: [
     {
@@ -138,32 +185,7 @@ const plugin: Plugin<XMLAst> = {
   printers: {
     xml: printer
   },
-  options: {
-    xmlSelfClosingSpace: {
-      type: "boolean",
-      category: "Global",
-      default: true,
-      description: "Adds a space before self-closing tags.",
-      since: "0.4.0"
-    },
-    xmlWhitespaceSensitivity: {
-      type: "choice",
-      category: "Global",
-      default: "strict",
-      description: "How to handle whitespaces in XML.",
-      choices: [
-        {
-          value: "strict",
-          description: "Whitespaces are considered sensitive in all elements."
-        },
-        {
-          value: "ignore",
-          description: "Whitespaces are considered insensitive in all elements."
-        }
-      ],
-      since: "0.6.0"
-    }
-  },
+  options,
   defaultOptions: {
     printWidth: 80,
     tabWidth: 2
diff --git a/src/printer.ts b/src/printer.ts
index f23e124..b5cf150 100644
--- a/src/printer.ts
+++ b/src/printer.ts
@@ -273,17 +273,32 @@ const printer: Printer<XMLAst> = {
           );
         }
 
+        // Determine the value that will go between the <, name, and attributes
+        // of an element and the /> of an element.
+        let space: Doc = "";
+
+        if (opts.bracketSameLine) {
+          space = " ";
+        } else if (opts.xmlSelfClosingSpace) {
+          space = line;
+        } else {
+          space = softline;
+        }
+
         if (SLASH_CLOSE) {
-          const space = opts.xmlSelfClosingSpace ? line : softline;
           return group([...parts, space, SLASH_CLOSE[0].image]);
         }
 
         if (Object.keys(content[0].children).length === 0) {
-          const space = opts.xmlSelfClosingSpace ? line : softline;
           return group([...parts, space, "/>"]);
         }
 
-        const openTag = group([...parts, softline, START_CLOSE[0].image]);
+        const openTag = group([
+          ...parts,
+          opts.bracketSameLine ? "" : softline,
+          START_CLOSE[0].image
+        ]);
+
         const closeTag = group([
           SLASH_OPEN[0].image,
           END_NAME[0].image,
@@ -391,7 +406,6 @@ const printer: Printer<XMLAst> = {
           }
 
           if (fragments.length === 0) {
-            const space = opts.xmlSelfClosingSpace ? line : softline;
             return group([...parts, space, "/>"]);
           }
 
diff --git a/src/types.ts b/src/types.ts
index 08d081c..802c07a 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -34,6 +34,7 @@ export type XMLAst =
   | PrologCstNode;
 
 export interface XMLOptions extends ParserOptions<XMLAst> {
+  bracketSameLine: boolean;
   xmlSelfClosingSpace: boolean;
   xmlWhitespaceSensitivity: "ignore" | "strict";
 }


Test patch:
diff --git a/test/__snapshots__/format.test.ts.snap b/test/__snapshots__/format.test.ts.snap
index 1d406f8..8e3025f 100644
--- a/test/__snapshots__/format.test.ts.snap
+++ b/test/__snapshots__/format.test.ts.snap
@@ -1,5 +1,79 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
+exports[`bracketSameLine => true 1`] = `
+"<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" ?>
+<!DOCTYPE module PUBLIC \\"-//Puppy Crawl//DTD Check Configuration 1.3//EN\\"
+  \\"https://www.puppycrawl.com/dtds/configuration_1_3.dtd\\">
+<?xml-model href=\\"project.rnc\\" type=\\"application/relax-ng-compact-syntax\\"?>
+<!-- foo -->
+<svg
+  xmlns=\\"http://www.w3.org/2000/svg\\"
+  xmlns:xlink=\\"http://www.w3.org/1999/xlink\\"
+  width=\\"200\\"
+  height=\\"100\\"
+  viewBox=\\"0 0 200 100\\">
+  <title>Style inheritance and the use element</title>
+  <desc _attr=\\"attr\\">
+    &anp; &#12345;
+    <![CDATA[
+      foo
+    ]]>
+    bar
+  </desc>
+  <?pagebreak?>
+
+  <style />
+  <style />
+  <style type=\\"text/css\\">
+circle {
+  stroke-opacity: 0.7;
+}
+.special circle {
+  stroke: green;
+}
+use {
+  stroke: purple;
+  fill: orange;
+}
+  </style>
+
+  <yaml
+    myveryveryveryverylongattributename=\\"myveryveryveryverylongattributevalue\\">
+    - 1
+    - 2
+    - 3
+  </style>
+
+  <?pagebreak?>
+  <g class=\\"special\\" style=\\"fill: blue\\">
+    <circle id=\\"c\\" cy=\\"50\\" cx=\\"50\\" r=\\"40\\" stroke-width=\\"20\\" />
+  </g>
+  <use xlink:href=\\"#c\\" x=\\"100\\" />
+  <ignored>
+    <!-- prettier-ignore-start -->
+      <   ignored   />
+    <!-- prettier-ignore-end -->
+  </ignored>
+  <p>
+    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.
+  </p>
+  <span>content</span>
+
+  <div>
+    even more
+    <content />
+  </div>
+</svg>
+<!-- bar -->
+"
+`;
+
 exports[`defaults 1`] = `
 "<?xml version=\\"1.0\\" encoding=\\"UTF-8\\" ?>
 <!DOCTYPE module PUBLIC \\"-//Puppy Crawl//DTD Check Configuration 1.3//EN\\"
@@ -38,6 +112,14 @@ use {
 }
   </style>
 
+  <yaml
+    myveryveryveryverylongattributename=\\"myveryveryveryverylongattributevalue\\"
+  >
+      - 1
+    - 2
+    - 3
+  </style>
+
   <?pagebreak?>
     <g class=\\"special\\" style=\\"fill: blue\\">
         <circle id=\\"c\\" cy=\\"50\\" cx=\\"50\\" r=\\"40\\" stroke-width=\\"20\\" />
@@ -103,6 +185,14 @@ use {
 }
   </style>
 
+  <yaml
+    myveryveryveryverylongattributename=\\"myveryveryveryverylongattributevalue\\"
+  >
+    - 1
+    - 2
+    - 3
+  </style>
+
   <?pagebreak?>
   <g class=\\"special\\" style=\\"fill: blue\\">
     <circle id=\\"c\\" cy=\\"50\\" cx=\\"50\\" r=\\"40\\" stroke-width=\\"20\\" />
diff --git a/test/fixture.xml b/test/fixture.xml
index c3594be..58f48cd 100644
--- a/test/fixture.xml
+++ b/test/fixture.xml
@@ -29,6 +29,12 @@
                       fill: orange; }
   </style>
 
+  <yaml myveryveryveryverylongattributename="myveryveryveryverylongattributevalue">
+      - 1
+    - 2
+    - 3
+  </style>
+
   <?pagebreak?>
     <g class="special" style="fill: blue">
         <circle id="c" cy="50" cx="50" r="40" stroke-width="20" />
diff --git a/test/format.test.ts b/test/format.test.ts
index 5d48d88..ee8bb08 100644
--- a/test/format.test.ts
+++ b/test/format.test.ts
@@ -24,3 +24,12 @@ test("xmlWhitespaceSensitivity => ignore", () => {
   const formatted = format(fixture, { xmlWhitespaceSensitivity: "ignore" });
   expect(formatted).toMatchSnapshot();
 });
+
+test("bracketSameLine => true", () => {
+  const formatted = format(fixture, {
+    bracketSameLine: true,
+    xmlWhitespaceSensitivity: "ignore"
+  });
+
+  expect(formatted).toMatchSnapshot();
+});