Add the code to track cursor movement and overwrites within a line, and a
simple lexer based on re.finditer. Use it to implement \b, \t, \r and \f.
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
---
patchew/logviewer.py | 82 +++++++++++++++++++++++++++++++++++++++++++++++--
tests/test_ansi2html.py | 31 +++++++++++++++++++
2 files changed, 110 insertions(+), 3 deletions(-)
diff --git a/patchew/logviewer.py b/patchew/logviewer.py
index 4438fa4..1e3495b 100644
--- a/patchew/logviewer.py
+++ b/patchew/logviewer.py
@@ -4,30 +4,106 @@
#
# Author: Paolo Bonzini <pbonzini@redhat.com>
+# Entity table based on ansi2html.c from colorized-logs.
+
+import re
import abc
import sys
from django.views import View
from django.http import HttpResponse, StreamingHttpResponse
-from django.utils.html import format_html
from django.utils.safestring import mark_safe
class ANSI2HTMLConverter(object):
+ RE_STRING = '[^\b\t\n\f\r\x1B]+'
+ RE_NUMS = '[0-9]+(?:;[0-9]+)*'
+ RE_CSI = r'\[\??(?:' + RE_NUMS + ')?[^;0-9]'
+ RE_OSC = r'].*?(?:\x1B\\|\x07)'
+ RE_CONTROL = '\x1B(?:%s|%s|[^][])|[\b\t\n\f\r]' % (RE_CSI, RE_OSC)
+ RE = re.compile('(%s)|(%s)' % (RE_STRING, RE_CONTROL))
+
+ ENTITIES = {
+ '\x00' : '␀', '\x01' : '␁', '\x02' : '␂',
+ '\x03' : '␃', '\x04' : '␄', '\x05' : '␅',
+ '\x06' : '␆', '\x07' : '🔔', '\x0B' : '␋',
+ '\x0E' : '␎', '\x0F' : '␏', '\x10' : '␐',
+ '\x11' : '␑', '\x12' : '␒', '\x13' : '␓',
+ '\x14' : '␔', '\x15' : '␕', '\x16' : '␖',
+ '\x17' : '␗', '\x18' : '␘', '\x19' : '␙',
+ '\x1A' : '␚', '\x1B' : '␛', '\x1C' : '␜',
+ '\x1D' : '␝', '\x1E' : '␞', '\x1F' : '␟',
+ '<' : '<', '>' : '>', '&' : '&',
+ '\x7F' : '⌦'
+ }
+ RE_ENTITIES = re.compile('[\x00-\x1F<>&\x7F]')
+
def __init__(self, white_bg=False):
self.prefix = '<pre class="ansi">'
+ self._reset()
+
+ def _reset(self):
+ self.line = []
+ self.pos = 0
+
+ # self.line holds the characters for the current line.
+ # Writing can overwrite some characters if self.pos is
+ # not pointing to the end of the line, and then appends.
+ # Moving the cursor right can add spaces to the end.
+
+ def _write(self, chars):
+ cur_len = len(self.line)
+ if self.pos < cur_len:
+ last = min(cur_len - self.pos, len(chars))
+ self.line[self.pos:self.pos+last] = list(chars[0:last])
+ else:
+ last = 0
+
+ if len(chars) > last:
+ self.line += list(chars[last:])
+ self.pos += len(chars)
+
+ def _set_pos(self, pos):
+ self.pos = pos
+ if self.pos > len(self.line):
+ num = self.pos - len(self.line)
+ self.line += [' '] * num
def _write_prefix(self):
if self.prefix != '':
yield self.prefix
self.prefix = ''
+ def _write_span(self, text):
+ yield self.RE_ENTITIES.sub(lambda x: self.ENTITIES[x.group(0)], text)
+
+ def _write_line(self, suffix):
+ text = "".join(self.line)
+ yield from self._write_span(text)
+ yield suffix
+ self._reset()
+
def convert(self, input):
yield from self._write_prefix()
- yield format_html('{}', input)
+ for m in self.RE.finditer(input):
+ if m.group(1):
+ self._write(m.group(1))
+ else:
+ seq = m.group(2)
+ if seq == '\n':
+ yield from self._write_line('\n')
+ elif seq == '\f':
+ yield from self._write_line('\n<hr>')
+ elif seq == '\b':
+ if self.pos > 0:
+ self.pos -= 1
+ elif seq == '\t':
+ self._set_pos(self.pos + (8 - self.pos % 8))
+ elif seq == '\r':
+ self.pos = 0
def finish(self):
yield from self._write_prefix()
- yield '</pre>'
+ yield from self._write_line('</pre>')
self.prefix = '<pre class="ansi">'
def ansi2html(input, white_bg=False):
diff --git a/tests/test_ansi2html.py b/tests/test_ansi2html.py
index 5ad993b..731f243 100644
--- a/tests/test_ansi2html.py
+++ b/tests/test_ansi2html.py
@@ -22,8 +22,39 @@ class ANSI2HTMLTest(unittest.TestCase):
# basic formatting tests
def test_basic(self):
+ self.assertBlackBg('\tb', ' b')
+ self.assertBlackBg('\t\ta', ' a')
+ self.assertBlackBg('a\tb', 'a b')
+ self.assertBlackBg('ab\tc', 'ab c')
self.assertBlackBg('a\nbc', 'a\nbc')
+ self.assertBlackBg('a\f', 'a\n<hr>')
+ self.assertBlackBg('a\n\f', 'a\n\n<hr>')
self.assertBlackBg('<', '<')
+ self.assertBlackBg('\x07', '🔔')
+
+ # backspace and carriage return
+ def test_set_pos(self):
+ self.assertBlackBg('abc\b\bBC', 'aBC')
+ self.assertBlackBg('a\b<', '<')
+ self.assertBlackBg('<\ba', 'a')
+ self.assertBlackBg('a\b\bbc', 'bc')
+ self.assertBlackBg('a\rbc', 'bc')
+ self.assertBlackBg('a\nb\bc', 'a\nc')
+ self.assertBlackBg('a\t\bb', 'a b')
+ self.assertBlackBg('a\tb\b\bc', 'a cb')
+ self.assertBlackBg('01234567\r\tb', '01234567b')
+
+ # Escape sequences
+ def test_esc_parsing(self):
+ self.assertBlackBg('{\x1b%}', '{}')
+ self.assertBlackBg('{\x1b[0m}', '{}')
+ self.assertBlackBg('{\x1b[m}', '{}')
+ self.assertBlackBg('{\x1b[0;1;7;0m}', '{}')
+ self.assertBlackBg('{\x1b[1;7m\x1b[m}', '{}')
+ self.assertBlackBg('{\x1b]test\x1b\\}', '{}')
+ self.assertBlackBg('{\x1b]test\x07}', '{}')
+ self.assertBlackBg('{\x1b]test\x1b[0m\x07}', '{}')
+ self.assertBlackBg('{\x1b]test\x1b[7m\x07}', '{}')
if __name__ == '__main__':
--
2.14.3