| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | import io |
| | import optparse |
| | import re |
| | import sys |
| | from collections import OrderedDict |
| | from typing import Iterator, List, Optional, Set, Tuple, Union |
| |
|
| | from ansi2html.style import ( |
| | SCHEME, |
| | add_truecolor_style_rule, |
| | get_styles, |
| | pop_truecolor_styles, |
| | ) |
| |
|
| | if sys.version_info >= (3, 8): |
| | from importlib.metadata import version |
| | else: |
| | from importlib_metadata import version |
| |
|
| | if sys.version_info >= (3, 8): |
| | from typing import TypedDict |
| | else: |
| | from typing_extensions import TypedDict |
| |
|
| |
|
| | 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 = 38 |
| | ANSI_FOREGROUND_DEFAULT = 39 |
| | ANSI_BACKGROUND_CUSTOM_MIN = 40 |
| | ANSI_BACKGROUND_CUSTOM_MAX = 47 |
| | ANSI_BACKGROUND = 48 |
| | ANSI_BACKGROUND_DEFAULT = 49 |
| | ANSI_NEGATIVE_ON = 7 |
| | ANSI_NEGATIVE_OFF = 27 |
| | ANSI_FOREGROUND_HIGH_INTENSITY_MIN = 90 |
| | ANSI_FOREGROUND_HIGH_INTENSITY_MAX = 97 |
| | ANSI_BACKGROUND_HIGH_INTENSITY_MIN = 100 |
| | ANSI_BACKGROUND_HIGH_INTENSITY_MAX = 107 |
| | ANSI_256_COLOR_ID = 5 |
| | ANSI_TRUECOLOR_ID = 2 |
| |
|
| | VT100_BOX_CODES = { |
| | "0x71": "─", |
| | "0x74": "├", |
| | "0x75": "┤", |
| | "0x76": "┴", |
| | "0x77": "┬", |
| | "0x78": "│", |
| | "0x6a": "┘", |
| | "0x6b": "┐", |
| | "0x6c": "┌", |
| | "0x6d": "└", |
| | "0x6e": "┼", |
| | } |
| |
|
| | |
| | _latex_template = """\\documentclass{scrartcl} |
| | \\usepackage[utf8]{inputenc} |
| | \\usepackage{fancyvrb} |
| | \\usepackage[usenames,dvipsnames]{xcolor} |
| | %% \\definecolor{red-sd}{HTML}{7ed2d2} |
| | %(hyperref)s |
| | \\title{%(title)s} |
| | |
| | \\fvset{commandchars=\\\\\\{\\}} |
| | |
| | \\begin{document} |
| | |
| | \\begin{Verbatim} |
| | %(content)s |
| | \\end{Verbatim} |
| | \\end{document} |
| | """ |
| |
|
| | _html_template = """<!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 class="ansi2html-content"> |
| | %(content)s |
| | </pre> |
| | </body> |
| | |
| | </html> |
| | """ |
| |
|
| |
|
| | class _State: |
| | def __init__(self) -> None: |
| | self.inside_span = False |
| | self.reset() |
| |
|
| | def reset(self) -> None: |
| | self.intensity: int = ANSI_INTENSITY_NORMAL |
| | self.style: int = ANSI_STYLE_NORMAL |
| | self.blink: int = ANSI_BLINK_OFF |
| | self.underline: int = ANSI_UNDERLINE_OFF |
| | self.crossedout: int = ANSI_CROSSED_OUT_OFF |
| | self.visibility: int = ANSI_VISIBILITY_ON |
| | self.foreground: Tuple[int, Optional[str]] = (ANSI_FOREGROUND_DEFAULT, None) |
| | self.background: Tuple[int, Optional[str]] = (ANSI_BACKGROUND_DEFAULT, None) |
| | self.negative: int = ANSI_NEGATIVE_OFF |
| |
|
| | def adjust(self, ansi_code: int, parameter: Optional[str] = None) -> 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_FOREGROUND_HIGH_INTENSITY_MIN |
| | <= ansi_code |
| | <= ANSI_FOREGROUND_HIGH_INTENSITY_MAX |
| | ): |
| | self.foreground = (ansi_code, None) |
| | elif ansi_code == ANSI_FOREGROUND: |
| | 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_BACKGROUND_HIGH_INTENSITY_MIN |
| | <= ansi_code |
| | <= ANSI_BACKGROUND_HIGH_INTENSITY_MAX |
| | ): |
| | self.background = (ansi_code, None) |
| | elif ansi_code == ANSI_BACKGROUND: |
| | 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 adjust_truecolor(self, ansi_code: int, r: int, g: int, b: int) -> None: |
| | parameter = "{:03d}{:03d}{:03d}".format( |
| | r, g, b |
| | ) |
| |
|
| | is_foreground = ansi_code == ANSI_FOREGROUND |
| | add_truecolor_style_rule(is_foreground, ansi_code, r, g, b, parameter) |
| | if is_foreground: |
| | self.foreground = (ansi_code, parameter) |
| | else: |
| | self.background = (ansi_code, parameter) |
| |
|
| | def to_css_classes(self) -> List[str]: |
| | css_classes: List[str] = [] |
| |
|
| | def append_unless_default(output: List[str], value: int, default: int) -> None: |
| | if value != default: |
| | css_class = "ansi%d" % value |
| | output.append(css_class) |
| |
|
| | def append_color_unless_default( |
| | output: List[str], |
| | color: Tuple[int, Optional[str]], |
| | default: int, |
| | negative: bool, |
| | neg_css_class: str, |
| | ) -> None: |
| | value, parameter = color |
| | if value != default: |
| | prefix = "inv" if negative else "ansi" |
| | css_class_index = ( |
| | str(value) if (parameter is None) else "%d-%s" % (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 |
| |
|
| |
|
| | class OSC_Link: |
| | def __init__(self, url: str, text: str) -> None: |
| | self.url = url |
| | self.text = text |
| |
|
| |
|
| | def map_vt100_box_code(char: str) -> str: |
| | char_hex = hex(ord(char)) |
| | return VT100_BOX_CODES[char_hex] if char_hex in VT100_BOX_CODES else char |
| |
|
| |
|
| | def _needs_extra_newline(text: str) -> bool: |
| | if not text or text.endswith("\n"): |
| | return False |
| | return True |
| |
|
| |
|
| | class CursorMoveUp: |
| | pass |
| |
|
| |
|
| | class Attributes(TypedDict): |
| | dark_bg: bool |
| | line_wrap: bool |
| | font_size: str |
| | body: str |
| | styles: Set[str] |
| |
|
| |
|
| | class Ansi2HTMLConverter: |
| | """Convert Ansi color codes to CSS+HTML |
| | |
| | Example: |
| | |
| | >>> conv = Ansi2HTMLConverter() |
| | >>> ansi = " ".join(sys.stdin.readlines()) |
| | >>> html = conv.convert(ansi) |
| | """ |
| |
|
| | def __init__( |
| | self, |
| | latex: bool = False, |
| | inline: bool = False, |
| | dark_bg: bool = True, |
| | line_wrap: bool = True, |
| | font_size: str = "normal", |
| | linkify: bool = False, |
| | escaped: bool = True, |
| | markup_lines: bool = False, |
| | output_encoding: str = "utf-8", |
| | scheme: str = "ansi2html", |
| | title: str = "", |
| | ) -> None: |
| |
|
| | self.latex = latex |
| | self.inline = inline |
| | self.dark_bg = dark_bg |
| | self.line_wrap = line_wrap |
| | 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: Attributes |
| | self.hyperref = False |
| | if inline: |
| | self.styles = dict( |
| | [ |
| | (item.klass.strip("."), item) |
| | for item in get_styles(self.dark_bg, self.line_wrap, self.scheme) |
| | ] |
| | ) |
| |
|
| | self.vt100_box_codes_prog = re.compile("\033\\(([B0])") |
| | self.ansi_codes_prog = re.compile("\033\\[([\\d;:]*)([a-zA-z])") |
| | self.url_matcher = re.compile( |
| | r"(((((https?|ftps?|gopher|telnet|nntp)://)|" |
| | r"(mailto:|news:))(%[0-9A-Fa-f]{2}|[-()_.!~*" |
| | r"\';/?:@&=+$,A-Za-z0-9])+)([).!\';/?:,][\s])?)" |
| | ) |
| | self.osc_link_re = re.compile("\033\\]8;;(.*?)\007(.*?)\033\\]8;;\007") |
| |
|
| | def do_linkify(self, line: str) -> str: |
| | if not isinstance(line, str): |
| | return line |
| | |
| | if self.latex: |
| | return self.url_matcher.sub(r"\\url{\1}", line) |
| | return self.url_matcher.sub(r'<a href="\1">\1</a>', line) |
| |
|
| | def handle_osc_links(self, part: OSC_Link) -> str: |
| | if self.latex: |
| | self.hyperref = True |
| | return """\\href{%s}{%s}""" % (part.url, part.text) |
| | return """<a href="%s">%s</a>""" % (part.url, part.text) |
| |
|
| | def apply_regex(self, ansi: str) -> Tuple[str, Set[str]]: |
| | styles_used: Set[str] = set() |
| | all_parts = self._apply_regex(ansi, styles_used) |
| | no_cursor_parts = self._collapse_cursor(all_parts) |
| | no_cursor_parts = list(no_cursor_parts) |
| |
|
| | def _check_links(parts: List[Union[str, OSC_Link]]) -> Iterator[str]: |
| | for part in parts: |
| | if isinstance(part, str): |
| | if self.linkify: |
| | yield self.do_linkify(part) |
| | else: |
| | yield part |
| | elif isinstance(part, OSC_Link): |
| | yield self.handle_osc_links(part) |
| | else: |
| | yield part |
| |
|
| | parts = list(_check_links(no_cursor_parts)) |
| | combined = "".join(parts) |
| | if self.markup_lines and not self.latex: |
| | combined = "\n".join( |
| | [ |
| | """<span id="line-%i">%s</span>""" % (i, line) |
| | for i, line in enumerate(combined.split("\n")) |
| | ] |
| | ) |
| | return combined, styles_used |
| |
|
| | def _apply_regex( |
| | self, ansi: str, styles_used: Set[str] |
| | ) -> Iterator[Union[str, OSC_Link, CursorMoveUp]]: |
| | if self.escaped: |
| | if ( |
| | self.latex |
| | ): |
| | specials = OrderedDict([]) |
| | else: |
| | specials = OrderedDict( |
| | [ |
| | ("&", "&"), |
| | ("<", "<"), |
| | (">", ">"), |
| | ] |
| | ) |
| | for pattern, special in specials.items(): |
| | ansi = ansi.replace(pattern, special) |
| |
|
| | def _vt100_box_drawing() -> Iterator[str]: |
| | last_end = 0 |
| | box_drawing_mode = False |
| | for match in self.vt100_box_codes_prog.finditer(ansi): |
| | trailer = ansi[last_end : match.start()] |
| | if box_drawing_mode: |
| | for char in trailer: |
| | yield map_vt100_box_code(char) |
| | else: |
| | yield trailer |
| | last_end = match.end() |
| | box_drawing_mode = match.groups()[0] == "0" |
| | yield ansi[last_end:] |
| |
|
| | ansi = "".join(_vt100_box_drawing()) |
| |
|
| | def _osc_link(ansi: str) -> Iterator[Union[str, OSC_Link]]: |
| | last_end = 0 |
| | for match in self.osc_link_re.finditer(ansi): |
| | trailer = ansi[last_end : match.start()] |
| | yield trailer |
| | url = match.groups()[0] |
| | text = match.groups()[1] |
| | yield OSC_Link(url, text) |
| | last_end = match.end() |
| | yield ansi[last_end:] |
| |
|
| | state = _State() |
| | for part in _osc_link(ansi): |
| | if isinstance(part, OSC_Link): |
| | yield part |
| | else: |
| | yield from self._handle_ansi_code(part, styles_used, state) |
| | if state.inside_span: |
| | if self.latex: |
| | yield "}" |
| | else: |
| | yield "</span>" |
| |
|
| | def _handle_ansi_code( |
| | self, ansi: str, styles_used: Set[str], state: _State |
| | ) -> Iterator[Union[str, CursorMoveUp]]: |
| | last_end = 0 |
| | for match in self.ansi_codes_prog.finditer(ansi): |
| | yield ansi[last_end : match.start()] |
| | last_end = match.end() |
| |
|
| | params: Union[str, List[int]] |
| | params, command = match.groups() |
| |
|
| | if command not in "mMA": |
| | continue |
| |
|
| | |
| | if command == "A": |
| | yield CursorMoveUp() |
| | continue |
| |
|
| | while True: |
| | param_len = len(params) |
| | params = params.replace("::", ":") |
| | params = params.replace(";;", ";") |
| | if len(params) == param_len: |
| | break |
| |
|
| | try: |
| | params = [int(x) for x in re.split("[;:]", params)] |
| | except ValueError: |
| | params = [ANSI_FULL_RESET] |
| |
|
| | |
| | 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, ANSI_BACKGROUND): |
| | try: |
| | x_bit_color_id = params[i + 1] |
| | except IndexError: |
| | x_bit_color_id = -1 |
| | is_256_color = x_bit_color_id == ANSI_256_COLOR_ID |
| | shift = 2 if is_256_color else 4 |
| | skip_after_index = i + shift |
| |
|
| | |
| | if last_null_index is not None: |
| | params = params[last_null_index + 1 :] |
| | if state.inside_span: |
| | state.inside_span = False |
| | if self.latex: |
| | yield "}" |
| | else: |
| | yield "</span>" |
| | state.reset() |
| |
|
| | if not params: |
| | continue |
| |
|
| | |
| | skip_after_index = -1 |
| | for i, v in enumerate(params): |
| | if i <= skip_after_index: |
| | continue |
| |
|
| | is_x_bit_color = v in (ANSI_FOREGROUND, ANSI_BACKGROUND) |
| | try: |
| | x_bit_color_id = params[i + 1] |
| | except IndexError: |
| | x_bit_color_id = -1 |
| | is_256_color = x_bit_color_id == ANSI_256_COLOR_ID |
| | is_truecolor = x_bit_color_id == ANSI_TRUECOLOR_ID |
| | if is_x_bit_color and is_256_color: |
| | try: |
| | parameter: Optional[str] = str(params[i + 2]) |
| | except IndexError: |
| | continue |
| | skip_after_index = i + 2 |
| | elif is_x_bit_color and is_truecolor: |
| | try: |
| | state.adjust_truecolor( |
| | v, params[i + 2], params[i + 3], params[i + 4] |
| | ) |
| | except IndexError: |
| | continue |
| | skip_after_index = i + 4 |
| | continue |
| | else: |
| | parameter = None |
| | state.adjust(v, parameter=parameter) |
| |
|
| | if state.inside_span: |
| | if self.latex: |
| | yield "}" |
| | else: |
| | yield "</span>" |
| | state.inside_span = False |
| |
|
| | css_classes = state.to_css_classes() |
| | if not css_classes: |
| | continue |
| | styles_used.update(css_classes) |
| |
|
| | if self.inline: |
| | self.styles.update(pop_truecolor_styles()) |
| | if self.latex: |
| | style = [ |
| | self.styles[klass].kwl[0][1] |
| | for klass in css_classes |
| | if self.styles[klass].kwl[0][0] == "color" |
| | ] |
| | yield "\\textcolor[HTML]{%s}{" % style[0] |
| | else: |
| | style = [ |
| | self.styles[klass].kw |
| | for klass in css_classes |
| | if klass in self.styles |
| | ] |
| | yield '<span style="%s">' % "; ".join(style) |
| | else: |
| | if self.latex: |
| | yield "\\textcolor{%s}{" % " ".join(css_classes) |
| | else: |
| | yield '<span class="%s">' % " ".join(css_classes) |
| | state.inside_span = True |
| | yield ansi[last_end:] |
| |
|
| | def _collapse_cursor( |
| | self, parts: Iterator[Union[str, OSC_Link, CursorMoveUp]] |
| | ) -> List[Union[str, OSC_Link]]: |
| | """Act on any CursorMoveUp commands by deleting preceding tokens""" |
| |
|
| | final_parts: List[Union[str, OSC_Link]] = [] |
| | for part in parts: |
| |
|
| | |
| | if not part: |
| | continue |
| |
|
| | |
| | if isinstance(part, CursorMoveUp): |
| | if final_parts: |
| | final_parts.pop() |
| |
|
| | while final_parts and ( |
| | isinstance(final_parts[-1], OSC_Link) |
| | or ( |
| | isinstance(final_parts[-1], str) and "\n" not in final_parts[-1] |
| | ) |
| | ): |
| | final_parts.pop() |
| |
|
| | continue |
| |
|
| | |
| | final_parts.append(part) |
| |
|
| | return final_parts |
| |
|
| | def prepare( |
| | self, ansi: str = "", ensure_trailing_newline: bool = False |
| | ) -> Attributes: |
| | """Load the contents of 'ansi' into this object""" |
| |
|
| | body, styles = self.apply_regex(ansi) |
| |
|
| | if ensure_trailing_newline and _needs_extra_newline(body): |
| | body += "\n" |
| |
|
| | self._attrs = { |
| | "dark_bg": self.dark_bg, |
| | "line_wrap": self.line_wrap, |
| | "font_size": self.font_size, |
| | "body": body, |
| | "styles": styles, |
| | } |
| |
|
| | return self._attrs |
| |
|
| | def convert( |
| | self, ansi: str, full: bool = True, ensure_trailing_newline: bool = False |
| | ) -> str: |
| | r""" |
| | :param ansi: ANSI sequence to convert. |
| | :param full: Whether to include the full HTML document or only the body. |
| | :param ensure_trailing_newline: Ensures that ``\n`` character is present at the end of the output. |
| | """ |
| | attrs = self.prepare(ansi, ensure_trailing_newline=ensure_trailing_newline) |
| | if not full: |
| | return attrs["body"] |
| | if self.latex: |
| | _template = _latex_template |
| | else: |
| | _template = _html_template |
| | all_styles = get_styles(self.dark_bg, self.line_wrap, self.scheme) |
| | backgrounds = all_styles[:5] |
| | used_styles = filter( |
| | lambda e: e.klass.lstrip(".") in attrs["styles"], all_styles |
| | ) |
| |
|
| | return _template % { |
| | "style": "\n".join(list(map(str, backgrounds + list(used_styles)))), |
| | "title": self.title, |
| | "font_size": self.font_size, |
| | "content": attrs["body"], |
| | "output_encoding": self.output_encoding, |
| | "hyperref": "\\usepackage{hyperref}" if self.hyperref else "", |
| | } |
| |
|
| | def produce_headers(self) -> str: |
| | return '<style type="text/css">\n%(style)s\n</style>\n' % { |
| | "style": "\n".join( |
| | map(str, get_styles(self.dark_bg, self.line_wrap, self.scheme)) |
| | ) |
| | } |
| |
|
| |
|
| | def main() -> None: |
| | """ |
| | $ ls --color=always | ansi2html > directories.html |
| | $ sudo tail /var/log/messages | ccze -A | ansi2html > logs.html |
| | $ task burndown | ansi2html > burndown.html |
| | """ |
| |
|
| | scheme_names = sorted(SCHEME.keys()) |
| | version_str = version("ansi2html") |
| | 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( |
| | "-L", |
| | "--latex", |
| | dest="latex", |
| | default=False, |
| | action="store_true", |
| | help="Export as LaTeX instead of HTML.", |
| | ) |
| | 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( |
| | "-W", |
| | "--no-line-wrap", |
| | dest="no_line_wrap", |
| | default=False, |
| | action="store_true", |
| | help="Disable line wrapping.", |
| | ) |
| | 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( |
| | latex=opts.latex, |
| | inline=opts.inline, |
| | dark_bg=not opts.light_background, |
| | line_wrap=not opts.no_line_wrap, |
| | 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, |
| | ) |
| |
|
| | if hasattr(sys.stdin, "detach") and not isinstance( |
| | sys.stdin, io.StringIO |
| | ): |
| | input_buffer = sys.stdin.detach() |
| | sys.stdin = io.TextIOWrapper(input_buffer, opts.input_encoding, "replace") |
| |
|
| | def _print(output_unicode: str, end: str = "\n") -> None: |
| | if hasattr(sys.stdout, "buffer"): |
| | output_bytes = (output_unicode + end).encode(opts.output_encoding) |
| | sys.stdout.buffer.write(output_bytes) |
| | else: |
| | sys.stdout.write(output_unicode + end) |
| |
|
| | |
| | if opts.headers: |
| | _print(conv.produce_headers(), end="") |
| | return |
| |
|
| | full = not bool(opts.partial or opts.inline) |
| | output = conv.convert( |
| | "".join(sys.stdin.readlines()), full=full, ensure_trailing_newline=True |
| | ) |
| | _print(output, end="") |
| |
|