mirror of
https://github.com/ansible/awx.git
synced 2026-05-07 17:37:37 -02:30
Replace ansi2html (GPL) with ansiconv (MIT).
This commit is contained in:
50
awx/api/templates/api/stdout.html
Normal file
50
awx/api/templates/api/stdout.html
Normal 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 %}
|
||||||
@@ -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':
|
||||||
|
|||||||
@@ -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/*)
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
from ansi2html.converter import Ansi2HTMLConverter
|
|
||||||
__all__ = ['Ansi2HTMLConverter']
|
|
||||||
@@ -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([
|
|
||||||
('&', '&'),
|
|
||||||
('<', '<'),
|
|
||||||
('>', '>'),
|
|
||||||
])
|
|
||||||
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='')
|
|
||||||
@@ -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
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
def read_to_unicode(obj):
|
|
||||||
return [line.decode('utf-8') for line in obj.readlines()]
|
|
||||||
127
awx/lib/site-packages/ansiconv.py
Normal file
127
awx/lib/site-packages/ansiconv.py
Normal 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
Reference in New Issue
Block a user