Repository: prettier/plugin-xml
Pull request #635 (2022-12-13 19:29:38)

feat: implement xmlWhitespaceSensitivity: 'preserve'
Fixes [#478](https://github.com/prettier/plugin-xml/issues/478)

Add new suboption `preserve` to `xmlWhitespaceSensitivity`, to preserve significant whitespace within XML elements.  This PR should address all the cases where significant white space exists.

Specifically:

- spaces within string are preserved:
  ```
  // before and after prettier identical
  <v>some string    extra space</v>
  ```

- string with space(s) at the beginning and/or end:
  ```
  // before and after prettier identical
  <v>
    some  string
  </v>
   ```
   
- mixing elements within string literal is also supported, space(s) surrounding and within string literal will be preserved:
   ```
   before: <v> some  string<abc/>more string </v>
    after: <v> some  string<abc />more string </v>
   ```

See PR https://github.com/prettier/plugin-xml/pull/479 for more detail 


Patch:
diff --git a/.github/ISSUE_TEMPLATE/formatting.yml b/.github/ISSUE_TEMPLATE/formatting.yml
index a376f56..88eb5b9 100644
--- a/.github/ISSUE_TEMPLATE/formatting.yml
+++ b/.github/ISSUE_TEMPLATE/formatting.yml
@@ -24,7 +24,8 @@ body:
       description: What value do you have the `xmlWhitespaceSensitivity` option set to? (Defaults to `"strict"`.) Be 100% sure changing this to `"ignore"` doesn't fix your issue!
       options:
         - "strict"
-        - "ignore"
+        - "preserve"
+        - "ignore"        
     validations:
       required: true
   - type: dropdown
diff --git a/README.md b/README.md
index 547e3b5..69e9998 100644
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ Below are the options (from [`src/plugin.js`](src/plugin.js)) that `@prettier/pl
 | `singleAttributePerLine`   | `--single-attribute-per-line`  |  `false`   | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#single-attribute-per-line)) |
 | `tabWidth`                 | `--tab-width`                  |    `2`     | Same as in Prettier ([see prettier docs](https://prettier.io/docs/en/options.html#tab-width)).                |
 | `xmlSelfClosingSpace`      | `--xml-self-closing-space`     |   `true`   | Adds a space before self-closing tags.                                                                        |
-| `xmlWhitespaceSensitivity` | `--xml-whitespace-sensitivity` | `"strict"` | Options are `"strict"` and `"ignore"`. You may want `"ignore"`, [see below](#whitespace).                     |
+| `xmlWhitespaceSensitivity` | `--xml-whitespace-sensitivity` | `"strict"` | Options are `"strict"`， `"preserve"` and `"ignore"`. You may want `"preserve"`, [see below](#whitespace).                     |
 
 Any of these can be added to your existing [prettier configuration
 file](https://prettier.io/docs/en/configuration.html). For example:
@@ -74,7 +74,9 @@ prettier --tab-width 4 --write '**/*.xml'
 
 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.
 
-If you're sure that the XML files that you're formatting do not require whitespace sensitivity, you can use the `"ignore"` option, as this will produce a standardized amount of whitespace. This will fix any indentation issues, and collapse excess blank lines (max of 1 blank line). For most folks most of the time, this is probably the option that you want.
+You can use the `"preserve"` option, if you want to preserve the whitespaces of the text node within XML elements and attributes. (see [Issue #478](https://github.com/prettier/plugin-xml/issues/478)).  For most folks most of the time, this is probably the option that you want.
+
+If you're sure that the XML files that you're formatting do not require whitespace sensitivity, you can use the `"ignore"` option, as this will produce a standardized amount of whitespace. This will fix any indentation issues, and collapse excess blank lines (max of 1 blank line).
 
 ### Ignore ranges
 
diff --git a/src/plugin.js b/src/plugin.js
index a12e45e..259f466 100644
--- a/src/plugin.js
+++ b/src/plugin.js
@@ -28,6 +28,10 @@ const plugin = {
           value: "strict",
           description: "Whitespaces are considered sensitive in all elements."
         },
+        {
+          value: "preserve",
+          description: "Whitespaces within text nodes in XML elements and attributes are considered sensitive."
+        },
         {
           value: "ignore",
           description: "Whitespaces are considered insensitive in all elements."
diff --git a/src/printer.js b/src/printer.js
index b961da4..ad9868f 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -282,7 +282,7 @@ const printer = {
         ]);
 
         if (
-          opts.xmlWhitespaceSensitivity === "ignore" &&
+          opts.xmlWhitespaceSensitivity !== "strict" &&
           isWhitespaceIgnorable(content[0])
         ) {
           const fragments = path.call(
@@ -297,37 +297,72 @@ const printer = {
               }
 
               if (children.chardata) {
-                childrenPath.each((charDataPath) => {
-                  const chardata = charDataPath.getValue();
-                  if (!chardata.children.TEXT) {
-                    return;
-                  }
-
-                  const content = chardata.children.TEXT[0].image.trim();
-                  const printed = group(
-                    content.split(/(\n)/g).map((value) => {
-                      if (value === "\n") {
-                        return literalline;
-                      }
-
-                      return fill(
-                        value
-                          .split(/\b( +)\b/g)
-                          .map((segment, index) =>
-                            index % 2 === 0 ? segment : line
-                          )
-                      );
-                    })
-                  );
-
-                  const location = chardata.location;
-                  response.push({
-                    offset: location.startOffset,
-                    startLine: location.startLine,
-                    endLine: location.endLine,
-                    printed
-                  });
-                }, "chardata");
+                // Does this chardata node have any non-whitespace text?
+                const containsText = children.chardata.some(({children}) => !!children.TEXT);                
+
+                if (containsText && opts.xmlWhitespaceSensitivity === "preserve") {
+                  let prevLocation;
+                  childrenPath.each((charDataPath) => {
+                    const chardata = charDataPath.getValue();
+                    const location = chardata.location;
+                    const content = print(charDataPath);
+
+                    if (prevLocation &&
+                        location.startColumn &&
+                        prevLocation.endColumn &&
+                        location.startLine === prevLocation.endLine &&
+                        location.startColumn === prevLocation.endColumn + 1) {
+                      // continuation of previous fragment                      
+                      const prevFragment = response[response.length - 1];
+                      prevFragment.endLine = location.endLine;
+                      prevFragment.printed = group([
+                        prevFragment.printed,
+                        content
+                      ]);
+                    } else {
+                      response.push({
+                        offset: location.startOffset,
+                        startLine: location.startLine,
+                        endLine: location.endLine,
+                        printed: content,
+                        hasSignificantWhitespace: true,
+                      })
+                    }
+                    prevLocation = location;
+                  }, 'chardata');
+                } else {
+                  childrenPath.each((charDataPath) => {
+                    const chardata = charDataPath.getValue();
+                    if (!chardata.children.TEXT) {
+                      return;
+                    }
+  
+                    const content = chardata.children.TEXT[0].image.trim();
+                    const printed = group(
+                      content.split(/(\n)/g).map((value) => {
+                        if (value === "\n") {
+                          return literalline;
+                        }
+  
+                        return fill(
+                          value
+                            .split(/\b( +)\b/g)
+                            .map((segment, index) =>
+                              index % 2 === 0 ? segment : line
+                            )
+                        );
+                      })
+                    );
+  
+                    const location = chardata.location;
+                    response.push({
+                      offset: location.startOffset,
+                      startLine: location.startLine,
+                      endLine: location.endLine,
+                      printed
+                    });
+                  }, "chardata");
+                }
               }
 
               if (children.element) {
@@ -361,6 +396,20 @@ const printer = {
 
           fragments.sort((left, right) => left.offset - right.offset);
 
+          if (opts.xmlWhitespaceSensitivity === "preserve") {
+            const hasSignificantWhitespace = fragments.some(
+              ({ hasSignificantWhitespace }) => !!hasSignificantWhitespace
+            );
+            if (hasSignificantWhitespace) {
+              // Return fragments unaltered
+              return group([
+                openTag,
+                fragments.map(({ printed }) => printed),
+                closeTag
+              ])
+            }            
+          }
+
           // If the only content of this tag is chardata, then use a softline so
           // that we won't necessarily break (to allow <foo>bar</foo>).
           if (


Test patch:
diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap
index 0bf47dd..dc8a2a8 100644
--- a/test/__snapshots__/format.test.js.snap
+++ b/test/__snapshots__/format.test.js.snap
@@ -68,6 +68,13 @@ use {
   </p>
   <span>content</span>
 
+  <div>
+    text with space
+    <div>
+      <hr />
+    </div>
+  </div>
+
   <div>
     even more
     <content />
@@ -145,6 +152,13 @@ use {
   </p>
   <span>content</span>
 
+  <div>
+    text with space
+    <div>
+      <hr/>
+    </div>
+  </div>
+
   <div>
     even more
     <content/>
@@ -220,6 +234,7 @@ use {
     content
   </span>
 
+  <div> text   with space<div><hr /></div>   </div>
 
   <div>
     even more
@@ -312,6 +327,13 @@ use {
   </p>
   <span>content</span>
 
+  <div>
+    text with space
+    <div>
+      <hr />
+    </div>
+  </div>
+
   <div>
     even more
     <content />
@@ -391,6 +413,13 @@ use {
   </p>
   <span>content</span>
 
+  <div>
+    text with space
+    <div>
+      <hr/>
+    </div>
+  </div>
+
   <div>
     even more
     <content/>
@@ -470,6 +499,92 @@ use {
   </p>
   <span>content</span>
 
+  <div>
+    text with space
+    <div>
+      <hr />
+    </div>
+  </div>
+
+  <div>
+    even more
+    <content />
+  </div>
+</svg>
+<!-- bar -->
+"
+`;
+
+exports[`xmlWhitespaceSensitivity => preserve 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>
+  <script value="lint" />
+
+  <yaml
+    myveryveryveryverylongattributename="myveryveryveryverylongattributevalue"
+  >
+- 1
+  - 2
+- 3
+  </yaml>
+
+  <!-- inner comment -->
+
+  <?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> text   with space<div>
+    <hr />
+  </div>   </div>
+
   <div>
     even more
     <content />
diff --git a/test/fixture.xml b/test/fixture.xml
index 069402c..db4416e 100644
--- a/test/fixture.xml
+++ b/test/fixture.xml
@@ -55,6 +55,7 @@
     content
   </span>
 
+  <div> text   with space<div><hr/></div>   </div>
 
   <div>
     even more
diff --git a/test/format.test.js b/test/format.test.js
index 9646769..4e45dac 100644
--- a/test/format.test.js
+++ b/test/format.test.js
@@ -63,3 +63,12 @@ test("singleAttributePerLine => true", async () => {
 
   expect(formatted).toMatchSnapshot();
 });
+
+test("xmlWhitespaceSensitivity => preserve", async () => {
+  const formatted = await format(fixture, {
+    xmlWhitespaceSensitivity: "preserve"
+  });
+  console.log({formatted});
+
+  expect(formatted).toMatchSnapshot();
+});
\ No newline at end of file