Replace ansi2html (GPL) with ansiconv (MIT).

This commit is contained in:
Chris Church
2015-02-19 23:47:46 -05:00
parent a63eb24cc0
commit 7f2a029532
9 changed files with 199 additions and 1622 deletions

View File

@@ -0,0 +1,50 @@
{% if content_only %}<div class="nocode ansi_fore ansi_back{% if dark %} ansi_dark{% endif %}">{% else %}
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>{{ title }}</title>
{% endif %}<style type="text/css">
.ansi_fore { color: #000000; }
.ansi_back { background-color: #F5F5F5; }
.ansi_fore.ansi_dark { color: #AAAAAA; }
.ansi_back.ansi_dark { background-color: #000000; }
.ansi1 { font-weight: bold; }
.ansi3 { font-weight: italic; }
.ansi4 { text-decoration: underline; }
.ansi9 { text-decoration: line-through; }
.ansi30 { color: #000316; }
.ansi31 { color: #AA0000; }
.ansi32 { color: #00AA00; }
.ansi33 { color: #AA5500; }
.ansi34 { color: #0000AA; }
.ansi35 { color: #E850A8; }
.ansi36 { color: #00AAAA; }
.ansi37 { color: #F5F1DE; }
.ansi40 { background-color: #000000; }
.ansi41 { background-color: #AA0000; }
.ansi42 { background-color: #00AA00; }
.ansi43 { background-color: #AA5500; }
.ansi44 { background-color: #0000AA; }
.ansi45 { background-color: #E850A8; }
.ansi46 { background-color: #00AAAA; }
.ansi47 { background-color: #F5F1DE; }
body.ansi_back pre {
font-family: Monaco, Menlo, Consolas, "Courier New", monospace;
font-size: 12px;
}
div.ansi_back.ansi_dark {
padding: 0 8px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
</style>{% if content_only %}{{ body }}
</div>
{% else %}
</head>
<body class="ansi_fore ansi_back{% if dark %} ansi_dark{% endif %}">
<pre>{{ body }}</pre>
</body>
</html>
{% endif %}

View File

@@ -3,6 +3,7 @@
# All Rights Reserved. # All Rights Reserved.
# Python # Python
import cgi
import datetime import datetime
import dateutil import dateutil
import time import time
@@ -33,13 +34,12 @@ from rest_framework.settings import api_settings
from rest_framework.views import exception_handler from rest_framework.views import exception_handler
from rest_framework import status from rest_framework import status
# Ansi2HTML
from ansi2html import Ansi2HTMLConverter
from ansi2html.style import SCHEME
# QSStats # QSStats
import qsstats import qsstats
# ANSIConv
import ansiconv
# AWX # AWX
from awx.main.task_engine import TaskSerializer, TASK_FILE from awx.main.task_engine import TaskSerializer, TASK_FILE
from awx.main.access import get_user_queryset from awx.main.access import get_user_queryset
@@ -2201,28 +2201,23 @@ class UnifiedJobStdout(RetrieveAPIView):
def retrieve(self, request, *args, **kwargs): def retrieve(self, request, *args, **kwargs):
unified_job = self.get_object() unified_job = self.get_object()
if request.accepted_renderer.format in ('html', 'api', 'json'): if request.accepted_renderer.format in ('html', 'api', 'json'):
scheme = request.QUERY_PARAMS.get('scheme', None)
start_line = request.QUERY_PARAMS.get('start_line', 0) start_line = request.QUERY_PARAMS.get('start_line', 0)
end_line = request.QUERY_PARAMS.get('end_line', None) end_line = request.QUERY_PARAMS.get('end_line', None)
if scheme not in SCHEME:
scheme = 'ansi2html'
dark_val = request.QUERY_PARAMS.get('dark', '') dark_val = request.QUERY_PARAMS.get('dark', '')
dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y')) dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y'))
content_only = bool(request.accepted_renderer.format in ('api', 'json')) content_only = bool(request.accepted_renderer.format in ('api', 'json'))
dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val)) dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val))
conv = Ansi2HTMLConverter(scheme=scheme, dark_bg=dark_bg,
title=get_view_name(self.__class__))
content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line) content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line)
if content_only:
headers = conv.produce_headers() body = ansiconv.to_html(cgi.escape(content))
body = conv.convert(content, full=False) # Escapes any HTML that may be in content. context = {
data = '\n'.join([headers, body]) 'title': get_view_name(self.__class__),
data = '<div class="nocode body_foreground body_background">%s</div>' % data 'body': mark_safe(body),
else: 'dark': dark_bg,
data = conv.convert(content) 'content_only': content_only,
# Fix ugly grey background used by default. }
data = data.replace('.body_background { background-color: #AAAAAA; }', data = render_to_string('api/stdout.html', context).strip()
'.body_background { background-color: #f5f5f5; }')
if request.accepted_renderer.format == 'api': if request.accepted_renderer.format == 'api':
return Response(mark_safe(data)) return Response(mark_safe(data))
if request.accepted_renderer.format == 'json': if request.accepted_renderer.format == 'json':

View File

@@ -2,7 +2,7 @@ Local versions of third-party packages required by Tower. Package names and
versions are listed below, along with notes on which files are included. versions are listed below, along with notes on which files are included.
amqp==1.4.5 (amqp/*) amqp==1.4.5 (amqp/*)
ansi2html==1.0.6 (ansi2html/*) ansiconv==1.0.0 (ansiconv.py)
anyjson==0.3.3 (anyjson/*) anyjson==0.3.3 (anyjson/*)
argparse==1.2.1 (argparse.py, needed for Python 2.6 support) argparse==1.2.1 (argparse.py, needed for Python 2.6 support)
azure==0.9.0 (azure/*) azure==0.9.0 (azure/*)

View File

@@ -1,2 +0,0 @@
from ansi2html.converter import Ansi2HTMLConverter
__all__ = ['Ansi2HTMLConverter']

View File

@@ -1,492 +0,0 @@
# This file is part of ansi2html
# Convert ANSI (terminal) colours and attributes to HTML
# Copyright (C) 2012 Ralph Bean <rbean@redhat.com>
# Copyright (C) 2013 Sebastian Pipping <sebastian@pipping.org>
#
# Inspired by and developed off of the work by pixelbeat and blackjack.
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
# <http://www.gnu.org/licenses/>.
import re
import sys
import optparse
import pkg_resources
try:
from collections import OrderedDict
except ImportError:
from ordereddict import OrderedDict
from ansi2html.style import get_styles, SCHEME
import six
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>
<meta http-equiv="Content-Type" content="text/html; charset=%(output_encoding)s">
<title>%(title)s</title>
<style type="text/css">\n%(style)s\n</style>
</head>
<body class="body_foreground body_background" style="font-size: %(font_size)s;" >
<pre>
%(content)s
</pre>
</body>
</html>
""")
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, color, default, negative, neg_css_class):
value, parameter = color
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))
return line
def _needs_extra_newline(text):
if not text or text.endswith('\n'):
return False
return True
class CursorMoveUp(object):
pass
class Ansi2HTMLConverter(object):
""" Convert Ansi color codes to CSS+HTML
Example:
>>> conv = Ansi2HTMLConverter()
>>> ansi = " ".join(sys.stdin.readlines())
>>> html = conv.convert(ansi)
"""
def __init__(self,
inline=False,
dark_bg=True,
font_size='normal',
linkify=False,
escaped=True,
markup_lines=False,
output_encoding='utf-8',
scheme='ansi2html',
title=''
):
self.inline = inline
self.dark_bg = dark_bg
self.font_size = font_size
self.linkify = linkify
self.escaped = escaped
self.markup_lines = markup_lines
self.output_encoding = output_encoding
self.scheme = scheme
self.title = title
self._attrs = None
if inline:
self.styles = dict([(item.klass.strip('.'), item) for item in get_styles(self.dark_bg, self.scheme)])
self.ansi_codes_prog = re.compile('\033\\[' '([\\d;]*)' '([a-zA-z])')
def apply_regex(self, ansi):
parts = self._apply_regex(ansi)
parts = self._collapse_cursor(parts)
parts = list(parts)
if self.linkify:
parts = [linkify(part) for part in parts]
combined = "".join(parts)
if self.markup_lines:
combined = "\n".join([
"""<span id="line-%i">%s</span>""" % (i, line)
for i, line in enumerate(combined.split('\n'))
])
return combined
def _apply_regex(self, ansi):
if self.escaped:
specials = OrderedDict([
('&', '&amp;'),
('<', '&lt;'),
('>', '&gt;'),
])
for pattern, special in specials.items():
ansi = ansi.replace(pattern, special)
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()
params, command = match.groups()
if command not in 'mMA':
continue
# Special cursor-moving code. The only supported one.
if command == 'A':
yield CursorMoveUp
continue
try:
params = list(map(int, params.split(';')))
except ValueError:
params = [ANSI_FULL_RESET]
# Find latest reset marker
last_null_index = None
skip_after_index = -1
for i, v in enumerate(params):
if i <= skip_after_index:
continue
if v == ANSI_FULL_RESET:
last_null_index = i
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 inside_span:
inside_span = False
yield '</span>'
state.reset()
if not params:
continue
# Turn codes into CSS classes
skip_after_index = -1
for i, v in enumerate(params):
if i <= skip_after_index:
continue
if v in (ANSI_FOREGROUND_256, ANSI_BACKGROUND_256):
try:
parameter = params[i + 2]
except IndexError:
continue
skip_after_index = i + 2
else:
parameter = None
state.adjust(v, parameter=parameter)
if inside_span:
yield '</span>'
inside_span = False
css_classes = state.to_css_classes()
if not css_classes:
continue
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 """
final_parts = []
for part in parts:
# Throw out empty string tokens ("")
if not part:
continue
# Go back, deleting every token in the last 'line'
if part == CursorMoveUp:
final_parts.pop()
while '\n' not in final_parts[-1]:
final_parts.pop()
continue
# Otherwise, just pass this token forward
final_parts.append(part)
return final_parts
def prepare(self, ansi='', ensure_trailing_newline=False):
""" Load the contents of 'ansi' into this object """
body = self.apply_regex(ansi)
if ensure_trailing_newline and _needs_extra_newline(body):
body += '\n'
self._attrs = {
'dark_bg': self.dark_bg,
'font_size': self.font_size,
'body': body,
}
return self._attrs
def attrs(self):
""" Prepare attributes for the template """
if not self._attrs:
raise Exception("Method .prepare not yet called.")
return self._attrs
def convert(self, ansi, full=True, ensure_trailing_newline=False):
attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline)
if not full:
return attrs["body"]
else:
return _template % {
'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme))),
'title' : self.title,
'font_size' : self.font_size,
'content' : attrs["body"],
'output_encoding' : self.output_encoding,
}
def produce_headers(self):
return '<style type="text/css">\n%(style)s\n</style>\n' % {
'style' : "\n".join(map(str, get_styles(self.dark_bg, self.scheme)))
}
def main():
"""
$ ls --color=always | ansi2html > directories.html
$ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html
$ task burndown | ansi2html > burndown.html
"""
scheme_names = sorted(six.iterkeys(SCHEME))
version_str = pkg_resources.get_distribution('ansi2html').version
parser = optparse.OptionParser(
usage=main.__doc__,
version="%%prog %s" % version_str)
parser.add_option(
"-p", "--partial", dest="partial",
default=False, action="store_true",
help="Process lines as them come in. No headers are produced.")
parser.add_option(
"-i", "--inline", dest="inline",
default=False, action="store_true",
help="Inline style without headers or template.")
parser.add_option(
"-H", "--headers", dest="headers",
default=False, action="store_true",
help="Just produce the <style> tag.")
parser.add_option(
"-f", '--font-size', dest='font_size', metavar='SIZE',
default="normal",
help="Set the global font size in the output.")
parser.add_option(
"-l", '--light-background', dest='light_background',
default=False, action="store_true",
help="Set output to 'light background' mode.")
parser.add_option(
"-a", '--linkify', dest='linkify',
default=False, action="store_true",
help="Transform URLs into <a> links.")
parser.add_option(
"-u", '--unescape', dest='escaped',
default=True, action="store_false",
help="Do not escape XML tags found in the input.")
parser.add_option(
"-m", '--markup-lines', dest="markup_lines",
default=False, action="store_true",
help="Surround lines with <span id='line-n'>..</span>.")
parser.add_option(
'--input-encoding', dest='input_encoding', metavar='ENCODING',
default='utf-8',
help="Specify input encoding")
parser.add_option(
'--output-encoding', dest='output_encoding', metavar='ENCODING',
default='utf-8',
help="Specify output encoding")
parser.add_option(
'-s', '--scheme', dest='scheme', metavar='SCHEME',
default='ansi2html', choices=scheme_names,
help=("Specify color palette scheme. Default: %%default. Choices: %s"
% scheme_names))
parser.add_option(
'-t', '--title', dest='output_title',
default='',
help="Specify output title")
opts, args = parser.parse_args()
conv = Ansi2HTMLConverter(
inline=opts.inline,
dark_bg=not opts.light_background,
font_size=opts.font_size,
linkify=opts.linkify,
escaped=opts.escaped,
markup_lines=opts.markup_lines,
output_encoding=opts.output_encoding,
scheme=opts.scheme,
title=opts.output_title,
)
def _read(input_bytes):
if six.PY3:
# This is actually already unicode. How to we explicitly decode in
# python3? I don't know the answer yet.
return input_bytes
else:
return input_bytes.decode(opts.input_encoding)
def _print(output_unicode, end='\n'):
if hasattr(sys.stdout, 'buffer'):
output_bytes = (output_unicode + end).encode(opts.output_encoding)
sys.stdout.buffer.write(output_bytes)
elif not six.PY3:
sys.stdout.write((output_unicode + end).encode(opts.output_encoding))
else:
sys.stdout.write(output_unicode + end)
# Produce only the headers and quit
if opts.headers:
_print(conv.produce_headers(), end='')
return
full = not bool(opts.partial or opts.inline)
if six.PY3:
output = conv.convert("".join(sys.stdin.readlines()), full=full, ensure_trailing_newline=True)
_print(output, end='')
else:
output = conv.convert(six.u("").join(
map(_read, sys.stdin.readlines())
), full=full, ensure_trailing_newline=True)
_print(output, end='')

View File

@@ -1,113 +0,0 @@
# This file is part of ansi2html.
# Copyright (C) 2012 Kuno Woudt <kuno@frob.nl>
# Copyright (C) 2013 Sebastian Pipping <sebastian@pipping.org>
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see
# <http://www.gnu.org/licenses/>.
import sys
class Rule(object):
def __init__(self, klass, **kw):
self.klass = klass
self.kw = '; '.join([(k.replace('_', '-')+': '+kw[k])
for k in sorted(kw.keys())]).strip()
def __str__(self):
return '%s { %s; }' % (self.klass, self.kw)
def index(r, g, b):
return str(16 + (r * 36) + (g * 6) + b)
def color(r, g, b):
return "#%.2x%.2x%.2x" % (r * 42, g * 42, b * 42)
def level(grey):
return "#%.2x%.2x%.2x" % (((grey * 10) + 8,) * 3)
def index2(grey):
return str(232 + grey)
# http://en.wikipedia.org/wiki/ANSI_escape_code#Colors
SCHEME = { # black red green brown/yellow blue magenta cyan grey/white
'ansi2html': ("#000316", "#aa0000", "#00aa00", "#aa5500", "#0000aa",
"#E850A8", "#00aaaa", "#F5F1DE"),
'xterm': ("#000000", "#cd0000", "#00cd00", "#cdcd00", "#0000ee",
"#cd00cd", "#00cdcd", "#e5e5e5"),
'xterm-bright': ("#7f7f7f", "#ff0000", "#00ff00", "#ffff00", "#5c5cff",
"#ff00ff", "#00ffff", "#ffffff"),
'osx': ("#000000", "#c23621", "#25bc24", "#adad27", "#492ee1",
"#d338d3", "#33bbc8", "#cbcccd"),
# http://ethanschoonover.com/solarized
'solarized': ("#262626", "#d70000", "#5f8700", "#af8700", "#0087ff",
"#af005f", "#00afaf", "#e4e4e4"),
}
def get_styles(dark_bg=True, scheme='ansi2html'):
css = [
Rule('.body_foreground', color=('#000000', '#AAAAAA')[dark_bg]),
Rule('.body_background', background_color=('#AAAAAA', '#000000')[dark_bg]),
Rule('.body_foreground > .bold,.bold > .body_foreground, body.body_foreground > pre > .bold',
color=('#000000', '#FFFFFF')[dark_bg], font_weight=('bold', 'normal')[dark_bg]),
Rule('.inv_foreground', color=('#000000', '#FFFFFF')[not dark_bg]),
Rule('.inv_background', background_color=('#AAAAAA', '#000000')[not dark_bg]),
Rule('.ansi1', font_weight='bold'),
Rule('.ansi2', font_weight='lighter'),
Rule('.ansi3', font_style='italic'),
Rule('.ansi4', text_decoration='underline'),
Rule('.ansi5', text_decoration='blink'),
Rule('.ansi6', text_decoration='blink'),
Rule('.ansi8', visibility='hidden'),
Rule('.ansi9', text_decoration='line-through'),
]
# set palette
pal = SCHEME[scheme]
for _index in range(8):
css.append(Rule('.ansi3%s' % _index, color=pal[_index]))
css.append(Rule('.inv3%s' % _index, background_color=pal[_index]))
for _index in range(8):
css.append(Rule('.ansi4%s' % _index, background_color=pal[_index]))
css.append(Rule('.inv4%s' % _index, color=pal[_index]))
# css.append("/* Define the explicit color codes (obnoxious) */\n\n")
for green in range(0, 6):
for red in range(0, 6):
for blue in range(0, 6):
css.append(Rule(".ansi38-%s" % index(red, green, blue),
color=color(red, green, blue)))
css.append(Rule(".inv38-%s" % index(red, green, blue),
background=color(red, green, blue)))
css.append(Rule(".ansi48-%s" % index(red, green, blue),
background=color(red, green, blue)))
css.append(Rule(".inv48-%s" % index(red, green, blue),
color=color(red, green, blue)))
for grey in range(0, 24):
css.append(Rule('.ansi38-%s' % index2(grey), color=level(grey)))
css.append(Rule('.inv38-%s' % index2(grey), background=level(grey)))
css.append(Rule('.ansi48-%s' % index2(grey), background=level(grey)))
css.append(Rule('.inv48-%s' % index2(grey), color=level(grey)))
return css

View File

@@ -1,2 +0,0 @@
def read_to_unicode(obj):
return [line.decode('utf-8') for line in obj.readlines()]

View File

@@ -0,0 +1,127 @@
"""
Converts ANSI coded text and converts it to either plain text
or to HTML.
"""
import re
supported_sgr_codes = [1, 3, 4, 9, 30, 31, 32, 33, 34, 35, 36, 37, 40, 41, 42,
43, 44, 45, 46, 47]
def to_plain(ansi):
"""Takes the given string and strips all ANSI codes out.
:param ansi: The string to strip
:return: The stripped string
"""
return re.sub(r'\x1B\[[0-9;]*[ABCDEFGHJKSTfmnsulh]', '', ansi)
def to_html(ansi, replace_newline=False):
"""Converts the given ANSI string to HTML
If `replace_newline` is set to True, then all newlines will be
replaced with <br />.
:param ansi: The ANSI text.
:param replace_newline: Whether to replace newlines with HTML.
:return: The resulting HTML string.
"""
blocks = ansi.split('\x1B')
parsed_blocks = []
for block in blocks:
command, text = _block_to_html(block)
# The command "A" means move the cursor up, so we emulate that here.
if command == 'A' and len(parsed_blocks) > 0:
parsed_blocks.pop()
while len(parsed_blocks) > 0 and '\n' not in parsed_blocks[-1]:
parsed_blocks.pop()
parsed_blocks.append(text)
text = ''.join(parsed_blocks)
if replace_newline:
text = text.replace('\n', '<br />\n')
return text
def base_css(dark=True):
"""Some base CSS with all of the default ANSI styles/colors.
:param dark: Whether background should be dark or light.
:return: A string of CSS
"""
return "\n".join([
css_rule('.ansi_fore', color=('#000000', '#FFFFFF')[dark]),
css_rule('.ansi_back', background_color=('#FFFFFF', '#000000')[dark]),
css_rule('.ansi1', font_weight='bold'),
css_rule('.ansi3', font_weight='italic'),
css_rule('.ansi4', text_decoration='underline'),
css_rule('.ansi9', text_decoration='line-through'),
css_rule('.ansi30', color="#000000"),
css_rule('.ansi31', color="#FF0000"),
css_rule('.ansi32', color="#00FF00"),
css_rule('.ansi33', color="#FFFF00"),
css_rule('.ansi34', color="#0000FF"),
css_rule('.ansi35', color="#FF00FF"),
css_rule('.ansi36', color="#00FFFF"),
css_rule('.ansi37', color="#FFFFFF"),
css_rule('.ansi40', background_color="#000000"),
css_rule('.ansi41', background_color="#FF0000"),
css_rule('.ansi42', background_color="#00FF00"),
css_rule('.ansi43', background_color="#FFFF00"),
css_rule('.ansi44', background_color="#0000FF"),
css_rule('.ansi45', background_color="#FF00FF"),
css_rule('.ansi46', background_color="#00FFFF"),
css_rule('.ansi47', background_color="#FFFFFF")
])
def css_rule(class_name, **properties):
"""Creates a CSS rule string.
The named parameters are used as the css properties. Underscores
are converted to hyphens.
:param class_name: The CSS class name
:param properties: The properties sent as named params.
:return: The CSS string
"""
prop_str = lambda name, val: name.replace('_', '-') + ': ' + val
return '{0} {{ {1}; }}'.format(
class_name,
'; '.join([prop_str(prop, properties[prop]) for prop in properties])
)
def _block_to_html(text):
"""Converts the given block of ANSI coded text to HTML.
The text is only given back as HTML if the ANSI code is at the
beginning of the string (e.g. "[0;33mFoobar")
:param text: The text block to convert.
:return: The text as HTML
"""
match = re.match(r'^\[(?P<code>\d+(?:;\d+)*)?(?P<command>[Am])', text)
if match is None:
return None, text
command = match.group('command')
text = text[match.end():]
if match.group('code') is None:
return command, text
classes = []
for code in match.group('code').split(';'):
if int(code) in supported_sgr_codes:
classes.append('ansi{0}'.format(code))
if classes:
text = '<span class="{0}">{1}</span>'.format(' '.join(classes), text)
return command, text

File diff suppressed because it is too large Load Diff