[patchew-devel] [PATCH 3/6] ansi2html: create ANSI to text converter

  • From: Paolo Bonzini <pbonzini@xxxxxxxxxx>
  • To: patchew-devel@xxxxxxxxxxxxx
  • Date: Mon, 5 Mar 2018 10:16:31 +0100

This converter compresses sequences involving carriage returns or backspaces,
to make them readable from an editor or mail reader.

Signed-off-by: Paolo Bonzini <pbonzini@xxxxxxxxxx>
---
 patchew/logviewer.py      | 29 ++++++++++++++++++
 patchew/settings.py       |  3 ++
 patchew/tags.py           | 19 ++++++++++++
 tests/test_ansi2html.py   | 75 ++++++++++++++++++++++++++++++++++++++++++++++-
 tests/test_custom_tags.py | 27 +++++++++++++++++
 5 files changed, 152 insertions(+), 1 deletion(-)
 create mode 100644 patchew/tags.py
 create mode 100755 tests/test_custom_tags.py

diff --git a/patchew/logviewer.py b/patchew/logviewer.py
index aa5d2bb..b84d487 100644
--- a/patchew/logviewer.py
+++ b/patchew/logviewer.py
@@ -286,6 +286,29 @@ class ANSIProcessor(object):
         self._reset_attrs()
 
 
+class ANSI2TextConverter(ANSIProcessor):
+    FF = '\u2015' * 72 + '\n'
+    SYMBOLS = {
+        '\x00' : '\u2400', '\x01' : '\u2401',      '\x02' : '\u2402',
+        '\x03' : '\u2403', '\x04' : '\u2404',      '\x05' : '\u2405',
+        '\x06' : '\u2406', '\x07' : '\U00001F514', '\x0B' : '\u240B',
+        '\x0E' : '\u240E', '\x0F' : '\u240F',      '\x10' : '\u2410',
+        '\x11' : '\u2411', '\x12' : '\u2412',      '\x13' : '\u2413',
+        '\x14' : '\u2414', '\x15' : '\u2415',      '\x16' : '\u2416',
+        '\x17' : '\u2417', '\x18' : '\u2418',      '\x19' : '\u2419',
+        '\x1A' : '\u241A', '\x1B' : '\u241B',      '\x1C' : '\u241C',
+        '\x1D' : '\u241D', '\x1E' : '\u241E',      '\x1F' : '\u241F',
+        '\x7F' : '\u2326'
+    }
+    RE_SYMBOLS = re.compile('[\x00-\x1F\x7F]')
+
+    def _write_span(self, text, class_id):
+        yield self.RE_SYMBOLS.sub(lambda x: self.SYMBOLS[x.group(0)], text)
+
+    def _write_form_feed(self):
+        yield self.FF
+
+
 class ANSI2HTMLConverter(ANSIProcessor):
     ENTITIES = {
         '\x00' : '&#x2400;', '\x01' : '&#x2401;',  '\x02' : '&#x2402;',
@@ -399,6 +422,12 @@ class ANSI2HTMLConverter(ANSIProcessor):
         self.prefix = '<pre class="ansi">'
 
 
+def ansi2text(input):
+    c = ANSI2TextConverter()
+    yield from c.convert(input)
+    yield from c.finish()
+
+
 def ansi2html(input, white_bg=False):
     c = ANSI2HTMLConverter(white_bg=white_bg)
     yield from c.convert(input)
diff --git a/patchew/settings.py b/patchew/settings.py
index 90a28c3..65a3b29 100644
--- a/patchew/settings.py
+++ b/patchew/settings.py
@@ -84,6 +84,9 @@ TEMPLATES = [
                 'django.contrib.auth.context_processors.auth',
                 'django.contrib.messages.context_processors.messages',
             ],
+            'builtins': [
+                'patchew.tags',
+            ],
         },
     },
 ]
diff --git a/patchew/tags.py b/patchew/tags.py
new file mode 100644
index 0000000..22d64b0
--- /dev/null
+++ b/patchew/tags.py
@@ -0,0 +1,19 @@
+#!/usr/bin/env python3
+#
+# Copyright 2018 Red Hat, Inc.
+#
+# Authors:
+#     Paolo Bonzini <pbonzini@xxxxxxxxxx>
+#
+# This work is licensed under the MIT License.  Please see the LICENSE file or
+# http://opensource.org/licenses/MIT.
+
+from django import template
+from patchew import logviewer
+
+register = template.Library()
+
+@register.simple_tag
+@register.filter
+def ansi2text(value):
+    return ''.join(logviewer.ansi2text(value))
diff --git a/tests/test_ansi2html.py b/tests/test_ansi2html.py
index cb709fd..e146c3e 100644
--- a/tests/test_ansi2html.py
+++ b/tests/test_ansi2html.py
@@ -6,7 +6,7 @@
 
 import unittest
 
-from patchew.logviewer import ansi2html
+from patchew.logviewer import ansi2html, ansi2text, ANSI2TextConverter
 
 class ANSI2HTMLTest(unittest.TestCase):
     def assertAnsi(self, test, expected, **kwargs):
@@ -291,5 +291,78 @@ class ANSI2HTMLTest(unittest.TestCase):
         self.assertWhiteBg('abc\x1b[7m\x1b[1Kabc', '   <span class="WHI 
BBLK">abc</span>')
 
 
+class ANSI2TextTest(unittest.TestCase):
+    def assertAnsi(self, test, expected, **kwargs):
+        self.assertEqual(''.join(ansi2text(test, **kwargs)), expected,
+                         repr(test))
+
+    # basic formatting tests
+    def test_basic(self):
+        self.assertAnsi('\tb', '        b')
+        self.assertAnsi('\t\ta', '                a')
+        self.assertAnsi('a\tb', 'a       b')
+        self.assertAnsi('ab\tc', 'ab      c')
+        self.assertAnsi('a\nbc', 'a\nbc')
+        self.assertAnsi('a\f', 'a\n' + ANSI2TextConverter.FF)
+        self.assertAnsi('a\n\f', 'a\n\n' + ANSI2TextConverter.FF)
+        self.assertAnsi('<', '<')
+        self.assertAnsi('\x07', '\U00001F514')
+
+    # backspace and carriage return
+    def test_set_pos(self):
+        self.assertAnsi('abc\b\bBC', 'aBC')
+        self.assertAnsi('a\b<', '<')
+        self.assertAnsi('<\ba', 'a')
+        self.assertAnsi('a\b\bbc', 'bc')
+        self.assertAnsi('a\rbc', 'bc')
+        self.assertAnsi('a\nb\bc', 'a\nc')
+        self.assertAnsi('a\t\bb', 'a      b')
+        self.assertAnsi('a\tb\b\bc', 'a      cb')
+        self.assertAnsi('01234567\r\tb', '01234567b')
+
+    # Escape sequences
+    def test_esc_parsing(self):
+        self.assertAnsi('{\x1b%}', '{}')
+        self.assertAnsi('{\x1b[0m}', '{}')
+        self.assertAnsi('{\x1b[m}', '{}')
+        self.assertAnsi('{\x1b[0;1;7;0m}', '{}')
+        self.assertAnsi('{\x1b[1;7m\x1b[m}', '{}')
+        self.assertAnsi('{\x1b]test\x1b\\}', '{}')
+        self.assertAnsi('{\x1b]test\x07}', '{}')
+        self.assertAnsi('{\x1b]test\x1b[0m\x07}', '{}')
+        self.assertAnsi('{\x1b]test\x1b[7m\x07}', '{}')
+
+    # ESC [C and ESC [D
+    def test_horiz_movement(self):
+        self.assertAnsi('abc\x1b[2DB', 'aBc')
+        self.assertAnsi('abc\x1b[3CD', 'abc   D')
+        self.assertAnsi('abcd\x1b[3DB\x1b[1CD', 'aBcD')
+        self.assertAnsi('abc\x1b[0CD', 'abc D')
+        self.assertAnsi('abc\x1b[CD', 'abc D')
+
+    # ESC [K
+    def test_clear_line(self):
+        self.assertAnsi('\x1b[Kabcd', 'abcd')
+        self.assertAnsi('abcd\r\x1b[K', '')
+        self.assertAnsi('abcd\b\x1b[K', 'abc')
+        self.assertAnsi('abcd\r\x1b[KDef', 'Def')
+        self.assertAnsi('abcd\b\x1b[KDef', 'abcDef')
+        self.assertAnsi('abcd\r\x1b[0K', '')
+        self.assertAnsi('abcd\b\x1b[0K', 'abc')
+        self.assertAnsi('abcd\r\x1b[1K', 'abcd')
+        self.assertAnsi('abcd\b\x1b[1K', '   d')
+        self.assertAnsi('abcd\r\x1b[2K', '')
+        self.assertAnsi('abcd\b\x1b[2K', '   ')
+        self.assertAnsi('abcd\r\x1b[2KDef', 'Def')
+        self.assertAnsi('abcd\b\x1b[2KDef', '   Def')
+
+    # combining cursor movement and formatting
+    def test_movement_and_formatting(self):
+        self.assertAnsi('\x1b[42m\tabc', '        abc')
+        self.assertAnsi('abc\x1b[42m\x1b[1Kabc', '   abc')
+        self.assertAnsi('\x1b[7m\tabc', '        abc')
+        self.assertAnsi('abc\x1b[7m\x1b[1Kabc', '   abc')
+
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/tests/test_custom_tags.py b/tests/test_custom_tags.py
new file mode 100755
index 0000000..ec875c8
--- /dev/null
+++ b/tests/test_custom_tags.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+#
+# Copyright 2018 Red Hat, Inc.
+#
+# Authors:
+#     Paolo Bonzini <pbonzini@xxxxxxxxxx>
+#
+# This work is licensed under the MIT License.  Please see the LICENSE file or
+# http://opensource.org/licenses/MIT.
+
+from django.template import Context, Template
+import patchewtest
+import unittest
+
+class CustomTagsTest(unittest.TestCase):
+    def assertTemplate(self, template, expected, **kwargs):
+        context = Context(kwargs)
+        self.assertEqual(Template(template).render(context), expected)
+
+    def test_template_filters(self):
+        self.assertTemplate('{{s|ansi2text}}', 'dbc', s='abc\rd')
+
+    def test_template_tags(self):
+        self.assertTemplate('{% ansi2text s %}', 'dbc', s='abc\rd')
+
+if __name__ == '__main__':
+    unittest.main()
-- 
2.14.3



Other related posts: