Repository: prettier/plugin-xml (js)

Issue:
Unstable attributes sorting
The `xmlSortAttributesByKey` option is unstable when comparing an `xmlns` attribute with another prefixed attribute. For example, consider the following XML element:

```xml
<foo xmlns:foo="http://www.example.org" foo:foo="1" />
```

Formatting it with `prettier --plugin=@prettier/plugin-xml --write --xml-sort-attributes-by-key` swaps attributes:

```xml
<foo foo:foo="1" xmlns:foo="http://www.example.org" />
```

However, running the same command again, swaps the attributes once more, changing the code back to its original form.

The possible culprit of this behavior may be the following line:

https://github.com/prettier/plugin-xml/blob/b0a1dd7c1b99c9f70320f85f97044dd771db3476/src/printer.js#L431

There's a typo in the `if` statement—instead of `xmlns`, it's `xlmns`.

Pull request description:
Fix up stable sorting for xmlns attributes
Fixes #732

Patch:
diff --git a/src/printer.js b/src/printer.js
index 3e227f6..6fcff86 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -428,7 +428,7 @@ function printElement(path, opts, print) {
 
           // Handle the 1 but not both being an xmlns
           if (leftNS === "xmlns") return -1;
-          if (rightNS === "xlmns") return 1;
+          if (rightNS === "xmlns") return 1;
 
           return leftNS.localeCompare(rightNS);
         }


Test patch:
diff --git a/test/__snapshots__/format.test.js.snap b/test/__snapshots__/format.test.js.snap
index 90b1f97..0047020 100644
--- a/test/__snapshots__/format.test.js.snap
+++ b/test/__snapshots__/format.test.js.snap
@@ -114,6 +114,8 @@ use {
         otherTools:baz="bing"
         ios:a="a" />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -241,6 +243,8 @@ use {
         otherTools:baz="bing"
         ios:a="a"/>
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo"/>
+    <foo foo:foo="foo" xmlns:foo="foo"/>
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell"/>
 
@@ -364,6 +368,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -518,6 +524,14 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo
+      xmlns:foo="foo"
+      foo:foo="foo"
+    />
+    <foo
+      foo:foo="foo"
+      xmlns:foo="foo"
+    />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -641,6 +655,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -764,6 +780,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -887,6 +905,8 @@ use {
         ios:a='a'
       />
     </foo>
+    <foo xmlns:foo='foo' foo:foo='foo' />
+    <foo foo:foo='foo' xmlns:foo='foo' />
   </container>
   <EscapeNeeded name='Pete &quot;Maverick&quot; Mitchell' />
 
@@ -1019,6 +1039,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo"/>
+    <foo foo:foo="foo" xmlns:foo="foo"/>
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell"/>
 
@@ -1032,6 +1054,131 @@ use {
 "
 `;
 
+exports[`xmlSortAttributesByKey => 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"
+  height="100"
+  viewBox="0 0 200 100"
+  width="200"
+>
+  <title>Style inheritance and the use element</title>
+  <desc _attr="attr">
+    &anp; &#12345;
+    <![CDATA[
+      foo
+    ]]>
+    bar
+  </desc>
+  <?pagebreak?>
+
+  <style />
+  <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 cx="50" cy="50" id="c" 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 />
+  </div>
+
+  <div xml:space="preserve">
+      <content />
+  </div>
+
+  <div
+    anotherverylongattribute="anotherverylongvalue"
+    verylongattribute="verylongvalue"
+  />
+
+  <div att2="doubleQuotes" attr1='singleQuotes' />
+
+   <!-- Basic sorting of xml attributes -->
+  <div x="first" y="second" z="third" />
+
+  <container xmlns:android="androidURI" xmlns:ios="iosURI">
+    <foo
+      xmlns="highest priority"
+      xmlns:tools="toolsURI"
+      android:a="a"
+      android:b="b"
+      android:c="c"
+      android:d="d"
+      tools:other="thing"
+      here="!"
+      some="non-ns-ed-attributes"
+    > 
+      <bar
+        xmlns:otherTools="otherToolsURI"
+        android:b="b"
+        ios:a="a"
+        ios:c="c"
+        ios:d="d"
+        otherTools:baz="bing"
+        tools:other="thing"
+        another="one"
+      />
+    </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo xmlns:foo="foo" foo:foo="foo" />
+  </container>
+  <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
+
+  <NoEscapeNeeded>He said, "Don't quote me."</NoEscapeNeeded>
+
+  <NoEscapeNeeded name='Pete "Maverick" Mitchell' />
+
+  <NoEscapeNeeded name="Pete 'Maverick' Mitchell" />
+</svg>
+<!-- bar -->
+"
+`;
+
 exports[`xmlWhitespaceSensitivity => ignore 1`] = `
 "<?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE module PUBLIC "-//Puppy Crawl//DTD Check Configuration 1.3//EN"
@@ -1151,6 +1298,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
@@ -1276,6 +1425,8 @@ use {
         ios:a="a"
       />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell" />
 
diff --git a/test/fixture.xml b/test/fixture.xml
index 8f2142c..2c52525 100644
--- a/test/fixture.xml
+++ b/test/fixture.xml
@@ -94,6 +94,8 @@
         otherTools:baz="bing"
         ios:a="a" />
     </foo>
+    <foo xmlns:foo="foo" foo:foo="foo" />
+    <foo foo:foo="foo" xmlns:foo="foo" />
   </container>
   <EscapeNeeded name="Pete &quot;Maverick&quot; Mitchell"/>
 
diff --git a/test/format.test.js b/test/format.test.js
index ebaa16d..5448dc2 100644
--- a/test/format.test.js
+++ b/test/format.test.js
@@ -24,6 +24,7 @@ test("xmlWhitespaceSensitivity => ignore", async () => {
   const formatted = await format(fixture, {
     xmlWhitespaceSensitivity: "ignore"
   });
+
   expect(formatted).toMatchSnapshot();
 });
 
@@ -76,6 +77,8 @@ test("xmlSortAttributesByKey => true", async () => {
   const formatted = await format(fixture, {
     xmlSortAttributesByKey: true
   });
+
+  expect(formatted).toMatchSnapshot();
 });
 
 test("xmlQuoteAttributes => preserve", async () => {