Skip to content

Commit

Permalink
Handle state in code, not in HTML; support more ANSI codes
Browse files Browse the repository at this point in the history
  • Loading branch information
hartwork committed Sep 26, 2013
1 parent 4088081 commit fce66a6
Showing 1 changed file with 126 additions and 17 deletions.
143 changes: 126 additions & 17 deletions ansi2html/converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@
from six.moves import map
from six.moves import zip


ANSI_FULL_RESET = 0
ANSI_INTENSITY_INCREASED = 1
ANSI_INTENSITY_REDUCED = 2
ANSI_INTENSITY_NORMAL = 22
ANSI_STYLE_ITALIC = 3
ANSI_STYLE_NORMAL = 23
ANSI_BLINK_SLOW = 5
ANSI_BLINK_FAST = 6
ANSI_BLINK_OFF = 25
ANSI_UNDERLINE_ON = 4
ANSI_UNDERLINE_OFF = 24
ANSI_CROSSED_OUT_ON = 9
ANSI_CROSSED_OUT_OFF = 29
ANSI_VISIBILITY_ON = 28
ANSI_VISIBILITY_OFF = 8
ANSI_FOREGROUND_CUSTOM_MIN = 30
ANSI_FOREGROUND_CUSTOM_MAX = 37
ANSI_FOREGROUND_256 = 38
ANSI_FOREGROUND_DEFAULT = 39
ANSI_BACKGROUND_CUSTOM_MIN = 40
ANSI_BACKGROUND_CUSTOM_MAX = 47
ANSI_BACKGROUND_256 = 48
ANSI_BACKGROUND_DEFAULT = 49
ANSI_NEGATIVE_ON = 7
ANSI_NEGATIVE_OFF = 27


_template = six.u("""<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
Expand All @@ -49,6 +77,81 @@
""")


class _State(object):
def __init__(self):
self.reset()

def reset(self):
self.intensity = ANSI_INTENSITY_NORMAL
self.style = ANSI_STYLE_NORMAL
self.blink = ANSI_BLINK_OFF
self.underline = ANSI_UNDERLINE_OFF
self.crossedout = ANSI_CROSSED_OUT_OFF
self.visibility = ANSI_VISIBILITY_ON
self.foreground = (ANSI_FOREGROUND_DEFAULT, None)
self.background = (ANSI_BACKGROUND_DEFAULT, None)
self.negative = ANSI_NEGATIVE_OFF

def adjust(self, ansi_code, parameter=None):
if ansi_code in (ANSI_INTENSITY_INCREASED, ANSI_INTENSITY_REDUCED, ANSI_INTENSITY_NORMAL):
self.intensity = ansi_code
elif ansi_code in (ANSI_STYLE_ITALIC, ANSI_STYLE_NORMAL):
self.style = ansi_code
elif ansi_code in (ANSI_BLINK_SLOW, ANSI_BLINK_FAST, ANSI_BLINK_OFF):
self.blink = ansi_code
elif ansi_code in (ANSI_UNDERLINE_ON, ANSI_UNDERLINE_OFF):
self.underline = ansi_code
elif ansi_code in (ANSI_CROSSED_OUT_ON, ANSI_CROSSED_OUT_OFF):
self.crossedout = ansi_code
elif ansi_code in (ANSI_VISIBILITY_ON, ANSI_VISIBILITY_OFF):
self.visibility = ansi_code
elif ANSI_FOREGROUND_CUSTOM_MIN <= ansi_code <= ANSI_FOREGROUND_CUSTOM_MAX:
self.foreground = (ansi_code, None)
elif ansi_code == ANSI_FOREGROUND_256:
self.foreground = (ansi_code, parameter)
elif ansi_code == ANSI_FOREGROUND_DEFAULT:
self.foreground = (ansi_code, None)
elif ANSI_BACKGROUND_CUSTOM_MIN <= ansi_code <= ANSI_BACKGROUND_CUSTOM_MAX:
self.background = (ansi_code, None)
elif ansi_code == ANSI_BACKGROUND_256:
self.background = (ansi_code, parameter)
elif ansi_code == ANSI_BACKGROUND_DEFAULT:
self.background = (ansi_code, None)
elif ansi_code in (ANSI_NEGATIVE_ON, ANSI_NEGATIVE_OFF):
self.negative = ansi_code

def to_css_classes(self):
css_classes = []

def append_unless_default(output, value, default):
if value != default:
css_class = 'ansi%d' % value
output.append(css_class)

def append_color_unless_default(output, (value, parameter), default, negative, neg_css_class):
if value != default:
prefix = 'inv' if negative else 'ansi'
css_class_index = str(value) \
if (parameter is None) \
else '%d-%d' % (value, parameter)
output.append(prefix + css_class_index)
elif negative:
output.append(neg_css_class)

append_unless_default(css_classes, self.intensity, ANSI_INTENSITY_NORMAL)
append_unless_default(css_classes, self.style, ANSI_STYLE_NORMAL)
append_unless_default(css_classes, self.blink, ANSI_BLINK_OFF)
append_unless_default(css_classes, self.underline, ANSI_UNDERLINE_OFF)
append_unless_default(css_classes, self.crossedout, ANSI_CROSSED_OUT_OFF)
append_unless_default(css_classes, self.visibility, ANSI_VISIBILITY_ON)

flip_fore_and_background = (self.negative == ANSI_NEGATIVE_ON)
append_color_unless_default(css_classes, self.foreground, ANSI_FOREGROUND_DEFAULT, flip_fore_and_background, 'inv_background')
append_color_unless_default(css_classes, self.background, ANSI_BACKGROUND_DEFAULT, flip_fore_and_background, 'inv_foreground')

return css_classes


def linkify(line):
for match in re.findall(r'https?:\/\/\S+', line):
line = line.replace(match, '<a href="%s">%s</a>' % (match, match))
Expand Down Expand Up @@ -121,9 +224,9 @@ def _apply_regex(self, ansi):
for pattern, special in specials.items():
ansi = ansi.replace(pattern, special)

# n_open is a count of the number of open tags
# last_end is the index of the last end of a code we've seen
n_open, last_end = 0, 0
state = _State()
inside_span = False
last_end = 0 # the index of the last end of a code we've seen
for match in self.ansi_codes_prog.finditer(ansi):
yield ansi[last_end:match.start()]
last_end = match.end()
Expand All @@ -141,7 +244,7 @@ def _apply_regex(self, ansi):
try:
params = list(map(int, params.split(';')))
except ValueError:
params = [0]
params = [ANSI_FULL_RESET]

# Find latest reset marker
last_null_index = None
Expand All @@ -150,50 +253,56 @@ def _apply_regex(self, ansi):
if i <= skip_after_index:
continue

if v == 0:
if v == ANSI_FULL_RESET:
last_null_index = i
elif v in [38, 48]:
elif v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
skip_after_index = i + 2

# Process reset marker, drop everything before
if last_null_index is not None:
params = params[last_null_index + 1:]
# If the control code 0 is present, close all tags we've
# opened so far. i.e. reset all attributes
yield '</span>' * n_open
n_open = 0
if inside_span:
inside_span = False
yield '</span>'
state.reset()

if not params:
continue

# Turn codes into CSS classes
css_classes = []
skip_after_index = -1
for i, v in enumerate(params):
if i <= skip_after_index:
continue

if v in [38, 48]: # 256 color mode switches
if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
try:
css_class = 'ansi%i-%i' % (params[i], params[i + 2])
parameter = params[i + 2]
except IndexError:
continue
skip_after_index = i + 2
else:
css_class = 'ansi%d' % v
css_classes.append(css_class)
parameter = None
state.adjust(v, parameter=parameter)

css_classes = state.to_css_classes()

# Count how many tags we're opening
n_open += 1
if inside_span:
yield '</span>'
inside_span = False

if self.inline:
style = [self.styles[klass].kw for klass in css_classes if
klass in self.styles]
yield '<span style="%s">' % "; ".join(style)
else:
yield '<span class="%s">' % " ".join(css_classes)
inside_span = True

yield ansi[last_end:]
if inside_span:
yield '</span>'
inside_span = False

def _collapse_cursor(self, parts):
""" Act on any CursorMoveUp commands by deleting preceding tokens """
Expand Down

0 comments on commit fce66a6

Please sign in to comment.