From 7f2a02953290ff3a4e02164c60cc1c3f8487198c Mon Sep 17 00:00:00 2001 From: Chris Church Date: Thu, 19 Feb 2015 23:47:46 -0500 Subject: [PATCH] Replace ansi2html (GPL) with ansiconv (MIT). --- awx/api/templates/api/stdout.html | 50 + awx/api/views.py | 33 +- awx/lib/site-packages/README | 2 +- awx/lib/site-packages/ansi2html/__init__.py | 2 - awx/lib/site-packages/ansi2html/converter.py | 492 --------- awx/lib/site-packages/ansi2html/style.py | 113 -- awx/lib/site-packages/ansi2html/util.py | 2 - awx/lib/site-packages/ansiconv.py | 127 +++ awx/ui/static/less/stdout.less | 1000 +----------------- 9 files changed, 199 insertions(+), 1622 deletions(-) create mode 100644 awx/api/templates/api/stdout.html delete mode 100644 awx/lib/site-packages/ansi2html/__init__.py delete mode 100644 awx/lib/site-packages/ansi2html/converter.py delete mode 100644 awx/lib/site-packages/ansi2html/style.py delete mode 100644 awx/lib/site-packages/ansi2html/util.py create mode 100644 awx/lib/site-packages/ansiconv.py diff --git a/awx/api/templates/api/stdout.html b/awx/api/templates/api/stdout.html new file mode 100644 index 0000000000..25aa79d08c --- /dev/null +++ b/awx/api/templates/api/stdout.html @@ -0,0 +1,50 @@ +{% if content_only %}
{% else %} + + + + + {{ title }} +{% endif %}{% if content_only %}{{ body }} +
+{% else %} + + +
{{ body }}
+ + +{% endif %} \ No newline at end of file diff --git a/awx/api/views.py b/awx/api/views.py index 7a212976cd..064ae2a9b9 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -3,6 +3,7 @@ # All Rights Reserved. # Python +import cgi import datetime import dateutil import time @@ -33,13 +34,12 @@ from rest_framework.settings import api_settings from rest_framework.views import exception_handler from rest_framework import status -# Ansi2HTML -from ansi2html import Ansi2HTMLConverter -from ansi2html.style import SCHEME - # QSStats import qsstats +# ANSIConv +import ansiconv + # AWX from awx.main.task_engine import TaskSerializer, TASK_FILE from awx.main.access import get_user_queryset @@ -2201,28 +2201,23 @@ class UnifiedJobStdout(RetrieveAPIView): def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() 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) end_line = request.QUERY_PARAMS.get('end_line', None) - if scheme not in SCHEME: - scheme = 'ansi2html' dark_val = request.QUERY_PARAMS.get('dark', '') dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y')) 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)) - 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) - if content_only: - headers = conv.produce_headers() - body = conv.convert(content, full=False) # Escapes any HTML that may be in content. - data = '\n'.join([headers, body]) - data = '
%s
' % data - else: - data = conv.convert(content) - # Fix ugly grey background used by default. - data = data.replace('.body_background { background-color: #AAAAAA; }', - '.body_background { background-color: #f5f5f5; }') + + body = ansiconv.to_html(cgi.escape(content)) + context = { + 'title': get_view_name(self.__class__), + 'body': mark_safe(body), + 'dark': dark_bg, + 'content_only': content_only, + } + data = render_to_string('api/stdout.html', context).strip() + if request.accepted_renderer.format == 'api': return Response(mark_safe(data)) if request.accepted_renderer.format == 'json': diff --git a/awx/lib/site-packages/README b/awx/lib/site-packages/README index 741be9bf2c..5275c4cbd6 100644 --- a/awx/lib/site-packages/README +++ b/awx/lib/site-packages/README @@ -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. amqp==1.4.5 (amqp/*) -ansi2html==1.0.6 (ansi2html/*) +ansiconv==1.0.0 (ansiconv.py) anyjson==0.3.3 (anyjson/*) argparse==1.2.1 (argparse.py, needed for Python 2.6 support) azure==0.9.0 (azure/*) diff --git a/awx/lib/site-packages/ansi2html/__init__.py b/awx/lib/site-packages/ansi2html/__init__.py deleted file mode 100644 index 58250b8196..0000000000 --- a/awx/lib/site-packages/ansi2html/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from ansi2html.converter import Ansi2HTMLConverter -__all__ = ['Ansi2HTMLConverter'] diff --git a/awx/lib/site-packages/ansi2html/converter.py b/awx/lib/site-packages/ansi2html/converter.py deleted file mode 100644 index 04cf89cd87..0000000000 --- a/awx/lib/site-packages/ansi2html/converter.py +++ /dev/null @@ -1,492 +0,0 @@ -# This file is part of ansi2html -# Convert ANSI (terminal) colours and attributes to HTML -# Copyright (C) 2012 Ralph Bean -# Copyright (C) 2013 Sebastian Pipping -# -# 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 -# . - -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(""" - - - -%(title)s - - - -
-%(content)s
-
- - - -""") - - -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, '%s' % (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([ - """%s""" % (i, line) - for i, line in enumerate(combined.split('\n')) - ]) - - return combined - - def _apply_regex(self, ansi): - if self.escaped: - specials = OrderedDict([ - ('&', '&'), - ('<', '<'), - ('>', '>'), - ]) - 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 '' - 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 '' - 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 '' % "; ".join(style) - else: - yield '' % " ".join(css_classes) - inside_span = True - - yield ansi[last_end:] - if inside_span: - yield '' - 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 '\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