Repository: collective/zpretty

Issue:
Broken reformatting at plone.schemaeditor
👋🏾 somehow when trying to format `plone.schemaeditor`'s [`schema_listing.pt`](https://github.com/plone/plone.schemaeditor/blob/master/plone/schemaeditor/browser/schema/schema_listing.pt) `zpretty` trips and produces invalid code.

Please test it with these changes: https://github.com/plone/plone.schemaeditor/pull/99

So one can run `tox -e test` to ensure that tests do run fine, then run `tox -e format` to apply `zpretty` and finally run `tox -e test` again to see that the reformatting of the template mentioned above breaks the tests.

The diff looks like this:

```diff
                 <tal:field tal:replace="structure widget/@@ploneform-render-widget" />
               </div>
             </div>
-            </tal:block>
+          </a></fieldset></tal:block>
 
-          </tal:widgets>
+ 
 
-        </fieldset>
-      </tal:block>
+ 
+ 
     </metal:fields-slot>
   </metal:form>
 </div>
```

Somehow `zpretty` decides to replace `</tal:block>` and `<tal:widgets>` by a single `</a>`

@ale-rt any idea? 🍀 

Comments:

Patch:
diff --git a/HISTORY.md b/HISTORY.md
index f84ba58..bd9d8e6 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -3,8 +3,9 @@
 3.0.4 (unreleased)
 ------------------
 
-- Nothing changed yet.
-
+- Fix bogus ampersands in attributes
+  (Fixes #116)
+  [ale-rt]
 
 3.0.3 (2023-03-26)
 
diff --git a/zpretty/prettifier.py b/zpretty/prettifier.py
index 4d7c4e5..e28e7f7 100644
--- a/zpretty/prettifier.py
+++ b/zpretty/prettifier.py
@@ -43,7 +43,17 @@ class ZPrettifier(object):
             text = text.decode(self.encoding)
         self.original_text = text
         self.text = self._prepare_text()
-        self.soup = self.get_soup(self.text)
+        soup = self.get_soup(self.text)
+        # Workaround for https://github.com/collective/zpretty/issues/116
+        # restore the ampersands
+        # in the attributes so that bogus ones can be escaped
+        for el in soup.descendants:
+            attrs = getattr(el, "attrs", {})
+            for key, value in attrs.items():
+                if self._ampersand_marker in value:
+                    attrs[key] = value.replace(self._ampersand_marker, "&")
+
+        self.soup = soup
 
         # Cleanup all spurious self._newlines_marker attributes, see #35
         key = self._newlines_marker.partition("=")[0]


Test patch:
diff --git a/zpretty/tests/test_zpretty.py b/zpretty/tests/test_zpretty.py
index 6b3c499..016b847 100644
--- a/zpretty/tests/test_zpretty.py
+++ b/zpretty/tests/test_zpretty.py
@@ -171,6 +171,20 @@ class TestZpretty(TestCase):
     def test_single_quotes_in_attrs(self):
         self.assertPrettified('<root a="\'" />', '<root a="\'"></root>\n')
 
+    def test_ampersand_in_attrs(self):
+        self.assertPrettified('<root a="&" />', '<root a="&amp;"></root>\n')
+        self.assertPrettified('<root a="foo &" />', '<root a="foo &amp;"></root>\n')
+        self.assertPrettified('<root a="& bar" />', '<root a="&amp; bar"></root>\n')
+        self.assertPrettified('<root a=";&" />', '<root a=";&amp;"></root>\n')
+        self.assertPrettified('<root a="&;" />', '<root a="&amp;;"></root>\n')
+
+    def test_escaped_ampersand_in_attrs(self):
+        self.assertPrettified('<root a="&amp;" />', '<root a="&amp;"></root>\n')
+        self.assertPrettified('<root a="foo &amp;" />', '<root a="foo &amp;"></root>\n')
+        self.assertPrettified('<root a="&amp; bar" />', '<root a="&amp; bar"></root>\n')
+        self.assertPrettified('<root a=";&amp;" />', '<root a=";&amp;"></root>\n')
+        self.assertPrettified('<root a="&amp;;" />', '<root a="&amp;;"></root>\n')
+
     def test_sample_html(self):
         self.prettify("sample_html.html")