Repository: prettier/plugin-xml
Pull request #45 (2020-01-25 19:52:05)

Support ignoring whitespace


Patch:
diff --git a/README.md b/README.md
index 1051d64..5fcaae8 100644
--- a/README.md
+++ b/README.md
@@ -46,11 +46,12 @@ The `prettier` executable is now installed and ready for use:
 
 Below are the options (from [`src/plugin.js`](src/plugin.js)) that `@prettier/plugin-xml` currently supports:
 
-| Name                  | Default | Description                                                                                      |
-| --------------------- | :-----: | ------------------------------------------------------------------------------------------------ |
-| `printWidth`          |  `80`   | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#print-width)). |
-| `tabWidth`            |   `2`   | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)).   |
-| `xmlSelfClosingSpace` | `true`  | Adds a space before self-closing tags.                                                           |
+| Name                       |  Default   | Description                                                                                      |
+| -------------------------- | :--------: | ------------------------------------------------------------------------------------------------ |
+| `printWidth`               |    `80`    | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#print-width)). |
+| `tabWidth`                 |    `2`     | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)).   |
+| `xmlSelfClosingSpace`      |   `true`   | Adds a space before self-closing tags.                                                           |
+| `xmlWhitespaceSensitivity` | `"strict"` | How to handle whitespaces. Options are `"strict"` and `"ignore"`.                                |
 
 Any of these can be added to your existing [prettier configuration
 file](https://prettier.io/docs/en/configuration.html). For example:
diff --git a/src/plugin.js b/src/plugin.js
index e3bf872..f447281 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -43,6 +43,22 @@ const plugin = {
       category: "Global",
       default: true,
       description: "Adds a space before self-closing tags."
+    },
+    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."
+        }
+      ]
     }
   },
   defaultOptions: {
diff --git a/src/print.js b/src/print.js
index 34f62ab..2d11b9b 100644
--- a/src/print.js
+++ b/src/print.js
@@ -9,6 +9,43 @@ const {
   softline
 } = require("prettier").doc.builders;
 
+const elementOnly = node => {
+  const { CData, Comment, chardata, element, reference } = node.children;
+
+  return (
+    !CData &&
+    !Comment &&
+    (!chardata || chardata.every(datum => !datum.children.TEXT)) &&
+    element &&
+    !reference
+  );
+};
+
+const printCData = node => ({
+  offset: node.startOffset,
+  printed: node.image
+});
+
+const printComment = node => ({
+  offset: node.startOffset,
+  printed: node.image
+});
+
+const printCharData = (path, print) => (node, index) => ({
+  offset: node.location.startOffset,
+  printed: path.call(print, "children", "chardata", index)
+});
+
+const printElement = (path, print) => (node, index) => ({
+  offset: node.location.startOffset,
+  printed: path.call(print, "children", "element", index)
+});
+
+const printReference = node => ({
+  offset: node.location.startOffset,
+  printed: (node.children.CharRef || node.children.EntityRef)[0].image
+});
+
 const nodes = {
   attribute: (path, _opts, _print) => {
     const { Name, EQUALS, STRING } = path.getValue().children;
@@ -27,58 +64,25 @@ const nodes = {
   },
   content: (path, opts, print) => {
     const {
-      CData,
-      Comment,
-      chardata,
-      element,
-      reference
+      CData = [],
+      Comment = [],
+      chardata = [],
+      element = [],
+      reference = []
     } = path.getValue().children;
 
-    let parts = [];
-
-    if (CData) {
-      CData.forEach(node => {
-        parts.push({ offset: node.startOffset, printed: node.image });
-      });
-    }
-
-    if (Comment) {
-      Comment.forEach(node => {
-        parts.push({ offset: node.startOffset, printed: node.image });
-      });
-    }
-
-    if (chardata) {
-      chardata.forEach((node, index) => {
-        parts.push({
-          offset: node.location.startOffset,
-          printed: path.call(print, "children", "chardata", index)
-        });
-      });
-    }
-
-    if (element) {
-      element.forEach((node, index) => {
-        parts.push({
-          offset: node.location.startOffset,
-          printed: path.call(print, "children", "element", index)
-        });
-      });
-    }
-
-    if (reference) {
-      reference.forEach(node => {
-        parts.push({
-          offset: node.location.startOffset,
-          printed: (node.children.CharRef || node.children.EntityRef)[0].image
-        });
-      });
-    }
-
-    parts.sort((left, right) => left.offset - right.offset);
-    parts = parts.map(({ printed }) => printed);
-
-    return group(concat(parts));
+    return group(
+      concat(
+        []
+          .concat(CData.map(printCData))
+          .concat(Comment.map(printComment))
+          .concat(chardata.map(printCharData(path, print)))
+          .concat(element.map(printElement(path, print)))
+          .concat(reference.map(printReference))
+          .sort((left, right) => left.offset - right.offset)
+          .map(({ printed }) => printed)
+      )
+    );
   },
   docTypeDecl: (path, opts, print) => {
     const { DocType, Name, externalID, CLOSE } = path.getValue().children;
@@ -169,11 +173,37 @@ const nodes = {
       return group(concat(parts.concat(space, "/>")));
     }
 
+    const openTag = group(concat(parts.concat(softline, START_CLOSE[0].image)));
+    const closeTag = group(
+      concat([SLASH_OPEN[0].image, END_NAME[0].image, END[0].image])
+    );
+
+    // If we're ignoring whitespace and we're printing a node that only contains
+    // other elements, then we can just print out the child elements directly.
+    if (opts.xmlWhitespaceSensitivity === "ignore" && elementOnly(content[0])) {
+      return group(
+        concat([
+          openTag,
+          indent(
+            concat([
+              hardline,
+              join(
+                hardline,
+                path.map(print, "children", "content", 0, "children", "element")
+              )
+            ])
+          ),
+          hardline,
+          closeTag
+        ])
+      );
+    }
+
     return group(
       concat([
-        group(concat(parts.concat(softline, START_CLOSE[0].image))),
+        openTag,
         indent(path.call(print, "children", "content", 0)),
-        group(concat([SLASH_OPEN[0].image, END_NAME[0].image, END[0].image]))
+        closeTag
       ])
     );
   },


Test patch:
diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap
index f564f01..985e92b 100644
--- a/test/__snapshots__/format.test.js.snap
+++ b/test/__snapshots__/format.test.js.snap
@@ -1,6 +1,41 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`fixture 1`] = `
+exports[`defaults 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>
+    &anp; &#12345;
+    <![CDATA[foo]]>
+    bar
+  </desc>
+  <style />
+  <style type=\\"text/css\\">
+    circle          { stroke-opacity: 0.7; }
+    .special circle { stroke: green; }
+    use             { stroke: purple;
+                      fill: orange; }
+  </style>
+    <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\\" />
+</svg>
+<!-- bar -->
+"
+`;
+
+exports[`xmlWhitespaceSensitivity => ignore 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\\">
diff --git a/test/fixture.xml b/test/fixture.xml
index 69dba7f..67913f1 100644
--- a/test/fixture.xml
+++ b/test/fixture.xml
@@ -23,9 +23,9 @@
     use             { stroke: purple;
                       fill: orange; }
   </style>
-  <g class="special" style="fill: blue">
-    <circle id="c" cy="50" cx="50" r="40" stroke-width="20" />
-  </g>
+    <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" />
 </svg>
 <!-- bar -->
diff --git a/test/format.test.js b/test/format.test.js
index 7093df4..83d1e9c 100644
--- a/test/format.test.js
+++ b/test/format.test.js
@@ -2,10 +2,19 @@ const fs = require("fs");
 const path = require("path");
 const prettier = require("prettier");
 
-test("fixture", () => {
-  const filepath = path.join(__dirname, "./fixture.xml");
-  const content = fs.readFileSync(filepath, "utf-8");
+const fixture = fs.readFileSync(path.join(__dirname, "./fixture.xml"), "utf-8");
+const format = (content, opts = {}) => prettier.format(content, {
+  ...opts,
+  parser: "xml",
+  plugins: ["."]
+});
+
+test("defaults", () => {
+  const formatted = format(fixture);
+  expect(formatted).toMatchSnapshot();
+});
 
-  const formatted = prettier.format(content, { parser: "xml", plugins: ["."] });
+test("xmlWhitespaceSensitivity => ignore", () => {
+  const formatted = format(fixture, { xmlWhitespaceSensitivity: "ignore" });
   expect(formatted).toMatchSnapshot();
 });