[PATCH 4/9] Add logical_rslice() function

"Jarret \"Jax\" Renker via Docutils-develop" <[email protected]>
Newsgroups gmane.text.docutils.devel
Message-ID <vmE9kGYSi9zf3Mg7wp-OEbx4fBAHMPvY6hUHrRAlrFcP9IUMT-5Df8ML65ASY-_I6jzZbqyItE-4IR4XelPdiPJZs1qzGeHhWa00cyE5FZs=@proton.me>
Add a function which works like ``string[start:]`` except that tabs are
not counted as a single character but as `tab_width` characters wide.
---
 docutils/docutils/statemachine.py  | 66 ++++++++++++++++++++++++++++++
 docutils/test/test_statemachine.py | 16 ++++++++
 2 files changed, 82 insertions(+)

diff --git a/docutils/docutils/statemachine.py b/docutils/docutils/statemachine.py
index 890572564..167cb7744 100644
--- a/docutils/docutils/statemachine.py
+++ b/docutils/docutils/statemachine.py
@@ -1496,6 +1496,72 @@ def string2lines(astring):
     return astring.splitlines()
 
 
+def logical_rslice(astring, logical_start, tab_width):
+    """
+    Removes characters from the beginning of `astring` such that the logical
+    width of the removed characters is `logical_start`.  If `astring` is
+    logically shorter than `logical_start`, then an empty string is returned.
+
+    For all except the tab character, the logical width is 1.  If `tab_width`
+    >= 0, then a tab character is considered to be up to `tab_width` spaces
+    wide depending on the current column in `astring` and the next tab
+    position.  A tab position is every tab_width columns.  For a description of
+    tab positions see str.expandtabs().
+
+    If `logical_start` is such that only parts of a single tab character should
+    be removed from `astring`.  Then the tab character is replaced by spaces
+    and only the necessary amount of spaces is stripped.  In other words, the
+    tab is "broken apart in the middle" and the left over parts are replaced
+    with spaces.
+
+    Note:
+
+    > logical_rslice(astring, logical_start, tab_width)
+
+    is almost equivalent to
+
+    > astring.expandtabs(tab_width)[logical_start:]
+
+    except that only tabs in the stripped prefix are expanded into spaces,
+    other tab characters are left untouched.
+
+    Examples:
+
+    >>> logical_rslice("foo", 2, 8)
+    "o"
+    >>> logical_rslice("  \tfoo", 2, 8)
+    "\tfoo"
+    >>> logical_rslice("\tfoo", 5, 8)
+    "   foo"
+    >>> logical_rslice("  \tfoo", 5, 8)
+    "   foo"
+
+    Parameters:
+
+    - `astring`: a string to strip from
+    - `logical_start`: a number of logical character width to strip
+    - `tab_width`: the tab width
+    """
+    logical_pos = 0
+    string_pos = 0 # physical position in string
+    space_prefix = ''
+    try:
+        while logical_pos < logical_start:
+            ch = astring[string_pos]
+            string_pos += 1
+            if ch == '\t':
+                if tab_width > 0:
+                    logical_pos += tab_width - (logical_pos % tab_width)
+                else:
+                    pass
+            else:
+                logical_pos += 1
+        return (' ' * (logical_pos - logical_start)) + astring[string_pos:]
+    except IndexError:
+        # `astring` was logically shorter than `logical_start`
+        return ''
+
+
 def _exception_data():
     """
     Return exception information:
diff --git a/docutils/test/test_statemachine.py b/docutils/test/test_statemachine.py
index 8332a5a61..19f2bd874 100755
--- a/docutils/test/test_statemachine.py
+++ b/docutils/test/test_statemachine.py
@@ -290,6 +290,22 @@ def test_string2lines(self):
         self.assertEqual(statemachine.string2lines(self.s2l_string),
                          self.s2l_expected)
 
+    logical_rslice_test_data = [
+        # (input, logical_start, tab_width, expected)
+        ("foo\tfoo", 3,  8, "\tfoo"),
+        ("   \tfoo", 3,  8, "\tfoo"),
+        ("\tfoo",    3,  8, "     foo"),
+        ("   \tfoo", 8,  8, "foo"),
+        ("   \tfoo", 9,  8, "oo"),
+        ("   \tfoo", 9, 10, " foo"),
+        ("\tfoo",    5,  8, "   foo"),
+        ("\tfoo",    5,  1, ""),
+    ]
+
+    def test_logical_rslice(self):
+        for (i,s,t,e) in self.logical_rslice_test_data:
+            self.assertEqual(statemachine.logical_rslice(i,s,t), e)
+
 
 if __name__ == '__main__':
     unittest.main()
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.