Repository: unified-font-object/ufoNormalizer

Issue:
Trouble normalizing the indention of new lines in Glyph Notes
Hi, 

We've been noticing that if a glyph has line returns in a note, the Normalizer has some trouble formatting it.

For instance, here is how a note started out after it was added, and its first Normalization: 
```xml
<note>
  	Line1
  	Line2
</note>
```
and with each round of normalization, all the lines after the first get indented an extra level

Once:
```xml
<note>
  	Line1
  		  	Line2
</note>
```

Twice:
```xml
<note>
  	Line1
  		  		  	Line2
</note>
```


Thrice:
```xml
<note>
  	Line1
  		  		  		  	Line2
</note>
```

etc.

Discussion:

Patch:
diff --git a/src/ufonormalizer.py b/src/ufonormalizer.py
index ff76f91..796bb37 100644
--- a/src/ufonormalizer.py
+++ b/src/ufonormalizer.py
@@ -4,6 +4,7 @@ from __future__ import print_function, unicode_literals
 
 import time
 import os
+import re
 import shutil
 from xml.etree import cElementTree as ET
 import plistlib
@@ -158,11 +159,9 @@ else:
 class UFONormalizerError(Exception):
     pass
 
-
 DEFAULT_FLOAT_PRECISION = 10
 FLOAT_FORMAT = "%%.%df" % DEFAULT_FLOAT_PRECISION
 
-
 def normalizeUFO(ufoPath, outputPath=None, onlyModified=True,
                  floatPrecision=DEFAULT_FLOAT_PRECISION, writeModTimes=True):
     global FLOAT_FORMAT
@@ -1178,6 +1177,8 @@ class XMLWriter(object):
         self.raw(line)
 
     def text(self, text):
+        text = text.strip("\n")
+        text = dedent_tabs(text)
         text = text.strip()
         text = xmlEscapeText(text)
         paragraphs = []
@@ -1358,6 +1359,64 @@ def xmlConvertFloat(value):
 def xmlConvertInt(value):
     return str(value)
 
+
+# ---------------
+# Text Operations
+# ---------------
+
+WHITESPACE_ONLY_RE = re.compile('^[\s\t]+$', re.MULTILINE)
+LEADING_WHITESPACE_RE = re.compile('(^(?:\s{4}|\t)*)(?:[^\t\n])', re.MULTILINE)
+
+def dedent_tabs(text):
+    """
+    Based on `textwrap.dedent`, but modified to only work on tabs and 4-space indents
+
+    Remove any common leading tabs from every line in `text`.
+    This can be used to make triple-quoted strings line up with the left
+    edge of the display, while still presenting them in the source code
+    in indented form.
+
+    Entirely blank lines are normalized to a newline character.
+    """
+    # Look for the longest leading string of spaces and tabs common to
+    # all lines.
+    margin = None
+    text = WHITESPACE_ONLY_RE.sub('', text)
+    indents = LEADING_WHITESPACE_RE.findall(text)
+    for indent in indents:
+        if margin is None:
+            margin = indent
+
+        # Current line more deeply indented than previous winner:
+        # no change (previous winner is still on top).
+        elif indent.startswith(margin):
+            pass
+
+        # Current line consistent with and no deeper than previous winner:
+        # it's the new winner.
+        elif margin.startswith(indent):
+            margin = indent
+
+        # Find the largest common whitespace between current line and previous
+        # winner.
+        else:
+            for i, (x, y) in enumerate(zip(margin, indent)):
+                if x != y:
+                    margin = margin[:i]
+                    break
+
+    # sanity check (testing/debugging only)
+    if 0 and margin:
+        for line in text.split("\n"):
+            assert not line or line.startswith(margin), \
+                   "line = %r, margin = %r" % (line, margin)
+
+    if margin:
+        text = re.sub(r'(?m)^' + margin, '', text)
+    return text
+
+
+
 # ---------------
 # Path Operations
 # ---------------


Test patch:
diff --git a/tests/test_ufonormalizer.py b/tests/test_ufonormalizer.py
index 910d5ae..cde30e0 100644
--- a/tests/test_ufonormalizer.py
+++ b/tests/test_ufonormalizer.py
@@ -836,6 +836,53 @@ class UFONormalizerTest(unittest.TestCase):
             writer.getText(),
             "<note>\n\tLine1\n\t\n\t    Line3\n</note>")
 
+        # Normalizer should not indent Line2 and Line3 more than already indented
+        element = ET.fromstring(
+            "<note>\n\tLine1\n\tLine2\n\tLine3\n</note>")
+        writer = XMLWriter(declaration=None)
+        _normalizeGlifNote(element, writer)
+        self.assertEqual(
+            writer.getText(),
+            "<note>\n\tLine1\n\tLine2\n\tLine3\n</note>")
+
+        # Normalizer should keep the extra tab in line 2
+        element = ET.fromstring(
+            "<note>\n\tLine1\n\t\tLine2\n\tLine3\n</note>")
+        writer = XMLWriter(declaration=None)
+        _normalizeGlifNote(element, writer)
+        self.assertEqual(
+            writer.getText(),
+            "<note>\n\tLine1\n\t\tLine2\n\tLine3\n</note>")
+
+        # Normalizer should keep the extra spaces on line 2
+        element = ET.fromstring(
+            "<note>\n\tLine1\n\t    Line2\n\tLine3\n</note>")
+        writer = XMLWriter(declaration=None)
+        _normalizeGlifNote(element, writer)
+        self.assertEqual(
+            writer.getText(),
+            "<note>\n\tLine1\n\t    Line2\n\tLine3\n</note>")
+
+        # Normalizer should remove the extra tab all lines have in common,
+        # but leave the additional tab on line 2
+        element = ET.fromstring(
+            "<note>\n\t\tLine1\n\t\t\tLine2\n\t\tLine3\n</note>")
+        writer = XMLWriter(declaration=None)
+        _normalizeGlifNote(element, writer)
+        self.assertEqual(
+            writer.getText(),
+            "<note>\n\tLine1\n\t\tLine2\n\tLine3\n</note>")
+
+        # Normalizer should remove the extra 4-space all lines have in common,
+        # but leave the additional 4-space on line 2
+        element = ET.fromstring(
+            "<note>\n        Line1\n            Line2\n        Line3\n</note>")
+        writer = XMLWriter(declaration=None)
+        _normalizeGlifNote(element, writer)
+        self.assertEqual(
+            writer.getText(),
+            "<note>\n\tLine1\n\t    Line2\n\tLine3\n</note>")
+
     def test_normalizeGLIF_note_undefined(self):
         element = ET.fromstring("<note></note>")
         writer = XMLWriter(declaration=None)