From 4831515cffd2b32e9d1cce30b277db9050901c27 Mon Sep 17 00:00:00 2001 From: Tim Cera Date: Sat, 4 Feb 2023 13:49:07 -0500 Subject: [PATCH 1/5] ci: added .gitignore --- .gitignore | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03e9bdd --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +*.py[cod] +*.sw[op] + +# C extensions +*.so + +# Packages +*.egg +*.egg-info +dist +build +_build +eggs +parts +bin +var +sdist +develop-eggs +.installed.cfg +lib +lib64 +__pycache__ +.mypy_cache + +# Installer logs +pip-log.txt + +# Unit test / coverage reports +.coverage +.tox +nosetests.xml +htmlcov +.cache +venv From c8acb78553dc74853faf4c004ef2cecf000e61fc Mon Sep 17 00:00:00 2001 From: Tim Cera Date: Fri, 24 Feb 2023 00:34:22 -0500 Subject: [PATCH 2/5] refactor: black and isort refactor --- bin/rst2ansi | 5 +- rst2ansi/__init__.py | 39 +- rst2ansi/ansi.py | 858 +++++++++++++++++----------------- rst2ansi/functional.py | 16 +- rst2ansi/get_terminal_size.py | 8 +- rst2ansi/table.py | 579 ++++++++++++----------- rst2ansi/unicode.py | 47 +- rst2ansi/visitor.py | 32 +- rst2ansi/wrap.py | 58 +-- setup.py | 66 +-- 10 files changed, 878 insertions(+), 830 deletions(-) diff --git a/bin/rst2ansi b/bin/rst2ansi index 435c7ed..841b994 100755 --- a/bin/rst2ansi +++ b/bin/rst2ansi @@ -1,9 +1,10 @@ #!/usr/bin/env python -import sys -from rst2ansi import rst2ansi import argparse import io +import sys + +from rst2ansi import rst2ansi parser = argparse.ArgumentParser(description='Prints a reStructuredText input in an ansi-decorated format suitable for console output.') parser.add_argument('file', type=str, nargs='?', help='A path to the file to open') diff --git a/rst2ansi/__init__.py b/rst2ansi/__init__.py index 0ea9a97..2af1a21 100644 --- a/rst2ansi/__init__.py +++ b/rst2ansi/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,30 +22,34 @@ THE SOFTWARE. """ -from __future__ import unicode_literals -from docutils import nodes, core +from docutils import core, nodes from docutils.parsers.rst import roles -from .visitor import Writer from .ansi import COLORS, STYLES +from .visitor import Writer + -def rst2ansi(input_string, output_encoding='utf-8'): +def rst2ansi(input_string, output_encoding="utf-8"): - overrides = {} - overrides['input_encoding'] = 'unicode' + overrides = {} + overrides["input_encoding"] = "unicode" - def style_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - return [nodes.TextElement(rawtext, text, classes=[name])], [] + def style_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + return [nodes.TextElement(rawtext, text, classes=[name])], [] - for color in COLORS: - roles.register_local_role('ansi-fg-' + color, style_role) - roles.register_local_role('ansi-bg-' + color, style_role) - for style in STYLES: - roles.register_local_role('ansi-' + style, style_role) + for color in COLORS: + roles.register_local_role("ansi-fg-" + color, style_role) + roles.register_local_role("ansi-bg-" + color, style_role) + for style in STYLES: + roles.register_local_role("ansi-" + style, style_role) - if hasattr(input_string, 'decode'): - input_string = input_string.decode('utf-8') + if hasattr(input_string, "decode"): + input_string = input_string.decode("utf-8") - out = core.publish_string(input_string, settings_overrides=overrides, writer=Writer(unicode=output_encoding.startswith('utf'))) - return out.decode(output_encoding) + out = core.publish_string( + input_string, + settings_overrides=overrides, + writer=Writer(unicode=output_encoding.startswith("utf")), + ) + return out.decode(output_encoding) diff --git a/rst2ansi/ansi.py b/rst2ansi/ansi.py index ec91c8d..b75f87f 100644 --- a/rst2ansi/ansi.py +++ b/rst2ansi/ansi.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,450 +22,471 @@ THE SOFTWARE. """ -from __future__ import unicode_literals -from docutils import core, frontend, nodes, utils, writers, languages, io -from docutils.utils.error_reporting import SafeString -from docutils.transforms import writer_aux -from docutils.parsers.rst import roles +from copy import deepcopy -from copy import deepcopy, copy -from .wrap import wrap +from docutils import nodes +from .get_terminal_size import get_terminal_size from .table import TableSizeCalculator, TableWriter from .unicode import ref_to_unicode, u +from .wrap import wrap -from .get_terminal_size import get_terminal_size +COLORS = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white") +STYLES = ( + "bold", + "dim", + "italic", + "underline", + "blink", + "blink-fast", + "inverse", + "conceal", + "strikethrough", +) + + +class ANSICodes: + @staticmethod + def get_color_code(code, fg): + FG = 30 + BG = 40 + FG_256 = 38 + BG_256 = 48 + + if code in COLORS: + shift = FG if fg else BG + return str(shift + COLORS.index(code)) + elif isinstance(code, int) and 0 <= code <= 255: + shift = FG_256 if fg else BG_256 + return str(shift) + ";5;%d" % int(code) + elif not isinstance(code, str) and hasattr(code, "__len__") and len(code) == 3: + for c in code: + if not 0 <= c <= 255: + raise Exception('Invalid color "%s"' % code) + + r, g, b = code + shift = FG_256 if fg else BG_256 + return str(shift) + ";2;%d;%d;%d" % (int(r), int(g), int(b)) + + raise Exception('Invalid color "%s"' % code) + + @staticmethod + def get_style_code(code): + if code in STYLES: + return str(1 + STYLES.index(code)) + raise Exception('Invalid style "%s"' % code) + + @staticmethod + def to_ansi(codes): + return "\x1b[" + ";".join(codes) + "m" + + NONE = "0" + RESET = to_ansi.__func__(NONE) -import shutil -COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') -STYLES = ('bold', 'dim', 'italic', 'underline', 'blink', 'blink-fast', 'inverse', 'conceal', 'strikethrough') +from .functional import npartial + + +class ANSITranslator(nodes.NodeVisitor): + class Context: + def __init__(self): + self.output = "" + self.indent_level = 0 + self.in_list = False + self.has_title = False + self.list_counter = 0 + self.node_type = "" + + class StyleContext: + def __init__(self): + self.styles = set() + self.fg = ANSICodes.NONE + self.bg = ANSICodes.NONE + + def __init__(self, document, termsize=None, **options): + nodes.NodeVisitor.__init__(self, document) + self.document = document + self.output = "" + self.lines = [""] + self.line = 0 + self.indent_width = 2 + self.termsize = termsize or get_terminal_size((80, 20)) + self.options = options + self.references = [] + self.refcount = 0 + + self.ctx = self.Context() + self.ctx_stack = [] + self.style = self.StyleContext() + self.style_stack = [] + + def push_ctx(self, **kwargs): + self.ctx_stack.append(self.ctx) + self.ctx = deepcopy(self.ctx) + for k, v in kwargs.items(): + setattr(self.ctx, k, v) + + def pop_ctx(self): + self.ctx = self.ctx_stack.pop() + + def push_style(self, fg=None, bg=None, styles=[]): + self.style_stack.append(self.style) + self.style = deepcopy(self.style) + if fg: + self.style.fg = ANSICodes.get_color_code(fg, True) + if bg: + self.style.bg = ANSICodes.get_color_code(bg, False) + self.style.styles |= {ANSICodes.get_style_code(s) for s in styles} + + self._restyle() + + def pop_style(self): + self.style = self.style_stack.pop() + reset = ( + self.style.fg == ANSICodes.NONE + and self.style.bg == ANSICodes.NONE + and not self.style.styles + ) + self._restyle(reset) + + def append(self, *args, **kwargs): + try: + strict = kwargs["strict"] + except KeyError: + strict = False + if len(self.lines[self.line]) == 0 and not strict: + self.lines[self.line] += " " * self.ctx.indent_level * self.indent_width + + for a in args: + self.lines[self.line] += u(a) + + def newline(self, n=1): + self.lines.extend([""] * n) + self.line += n + + def prevline(self, n=1): + self.line -= n + + def nextline(self, n=1): + self.line += n + + def popline(self): + l = self.lines.pop(self.line) + self.line -= 1 + return l + + def replaceline(self, newline, strict=True): + if strict: + self.lines[self.line] = newline + else: + self.lines[self.line] = "" + self.append(newline) + + def addlines(self, lines, strict=False): + if strict: + self.lines.extend(lines) + self.line += len(lines) + self.newline() + else: + for l in lines: + self.append(l) + self.newline() + + def _restyle(self, reset=False): + if reset: + self.append(ANSICodes.RESET) + + styles = list(self.style.styles) + if self.style.fg != ANSICodes.NONE: + styles.append(self.style.fg) + if self.style.bg != ANSICodes.NONE: + styles.append(self.style.bg) + + if styles: + self.append(ANSICodes.to_ansi(styles)) + + def strip_empty_lines(self): + remove_last_n = 0 + for x in self.lines[::-1]: + if len(x.strip()) != 0: + break + remove_last_n += 1 + if remove_last_n != 0: + self.lines = self.lines[:-remove_last_n] + + # Structural nodes + + def visit_document(self, node): + self.push_ctx() + + def _print_references(self): + if not self.references: + return + + self.push_style(styles=["bold"]) + self.append("References:") + self.pop_style() + self.newline(2) + + self.push_ctx(indent_level=self.ctx.indent_level + 1) + for ref in self.references: + self.append("[%s]: <" % ref[0]) + self.push_style(fg="cyan", styles=["underline"]) + self.append(ref[1]) + self.pop_style() + self.append(">") + self.newline() + self.references = [] + self.pop_ctx() + + def depart_document(self, node): + self._print_references() + self.depart_section(node) + + self.pop_ctx() + self.strip_empty_lines() + + self.output = "\n".join(self.lines) + + def wrap_current_line(self): + indent = self.ctx.indent_level * self.indent_width + sublines = wrap( + self.curline, + width=self.termsize[0] - indent, + subsequent_indent=" " * indent, + ) + self.popline() + self.addlines(sublines, strict=True) + + def depart_paragraph(self, node): + if self.options.get("wrap_paragraphs", True): + self.wrap_current_line() + if not self.ctx.in_list: + self.newline() + + def visit_title(self, node): + self.push_style(styles=["bold"]) + + def depart_title(self, node): + self.pop_style() + self.push_ctx(has_title=True, indent_level=self.ctx.indent_level + 1) + self.newline(2) + + def visit_subtitle(self, node): + self.prevline(2) + self.append(" - ") + + def depart_subtitle(self, node): + self.nextline(2) + + def visit_Text(self, node): + self.append(node.astext()) + + def depart_section(self, node): + if self.ctx.has_title: + self.pop_ctx() + + def depart_transition(self, node): + indent = (self.ctx.indent_level + 2) * self.indent_width + char = "╌" if self.options["unicode"] else "-" + self.append( + " " * indent + char * (self.termsize[0] - 2 * indent) + " " * indent, + strict=True, + ) + self.newline(2) + + def _get_uri(self, node): + uri = node.attributes.get("refuri", "") + if not uri: + uri = node.attributes.get("uri", "") + return uri + + def visit_reference(self, node): + if self._get_uri(node) == node.astext().strip(): + self.append("<") + self.push_style(fg="cyan", styles=["underline"]) + + def depart_reference(self, node): + self.pop_style() + if self._get_uri(node) == node.astext().strip(): + self.append(">") + else: + self.references.append((self.refcount, self._get_uri(node))) + if self.options["unicode"] and self.options.get( + "unicode_superscript", False + ): + self.append(ref_to_unicode(self.refcount)) + else: + self.append(" [%s]" % self.refcount) + self.refcount += 1 + + # Style nodes + + visit_strong = npartial(push_style, styles=["bold"]) + depart_strong = npartial(pop_style) + + visit_emphasis = npartial(push_style, styles=["italic"]) + depart_emphasis = npartial(pop_style) + + def visit_TextElement(self, node): + ansi_props = [ + x[5:] for x in node.attributes["classes"] if x.startswith("ansi-") + ] + style = { + "fg": next( + (x[3:] for x in ansi_props if x.startswith("fg-") and x[3:] in COLORS), + None, + ), + "bg": next( + (x[3:] for x in ansi_props if x.startswith("bg-") and x[3:] in COLORS), + None, + ), + "styles": (x for x in ansi_props if x in STYLES), + } + self.push_style(**style) + + def depart_TextElement(self, node): + self.pop_style() + + visit_inline = visit_TextElement + depart_inline = depart_TextElement + + # Lists + + def visit_enumerated_list(self, node): + strt = node.attributes.get("start", 1) + self.push_ctx(in_list=True, list_counter=strt) + + def depart_enumerated_list(self, node): + self.pop_ctx() + if not self.ctx.in_list: + self.newline() + + def visit_bullet_list(self, node): + self.push_ctx(in_list=True, list_counter=0) + + def depart_bullet_list(self, node): + self.pop_ctx() + if not self.ctx.in_list: + self.newline() + + def visit_list_item(self, node): + if self.ctx.list_counter: + self.append(str(self.ctx.list_counter) + ". ") + self.ctx.list_counter += 1 + else: + self.append("• " if self.options["unicode"] else "* ") + self.push_ctx(indent_level=self.ctx.indent_level + 1) + + def depart_list_item(self, node): + self.pop_ctx() + + visit_definition_list = npartial(push_ctx, in_list=True, list_counter=0) + depart_definition_list = npartial(pop_ctx) + + def visit_definition(self, node): + self.newline() + self.push_ctx(indent_level=self.ctx.indent_level + 1) -class ANSICodes(object): + def depart_definition(self, node): + self.newline() + self.pop_ctx() - @staticmethod - def get_color_code(code, fg): - FG = 30 - BG = 40 - FG_256 = 38 - BG_256 = 48 + visit_option_list = npartial(push_ctx, in_list=True, list_counter=0) + depart_option_list = npartial(pop_ctx) - if code in COLORS: - shift = FG if fg else BG - return str(shift + COLORS.index(code)) - elif isinstance(code, int) and 0 <= code <= 255: - shift = FG_256 if fg else BG_256 - return str(shift) + ';5;%d' % int(code) - elif not isinstance(code, str) and hasattr(code, "__len__") and len(code) == 3: - for c in code: - if not 0 <= c <= 255: - raise Exception('Invalid color "%s"' % code) + def depart_option(self, node): + self.append(" | ") - r, g, b = code - shift = FG_256 if fg else BG_256 - return str(shift) + ';2;%d;%d;%d' % (int(r), int(g), int(b)) + def depart_option_group(self, node): + self.replaceline(self.lines[self.line][:-3], strict=True) + self.push_ctx(indent_level=self.ctx.indent_level + 2) + self.newline() - raise Exception('Invalid color "%s"' % code) + def visit_option_argument(self, node): + self.append(" ") - @staticmethod - def get_style_code(code): - if code in STYLES: - return str(1 + STYLES.index(code)) - raise Exception('Invalid style "%s"' % code) + def depart_option_list_item(self, node): + self.pop_ctx() - @staticmethod - def to_ansi(codes): - return '\x1b[' + ';'.join(codes) + 'm' + # Tables - NONE = '0' - RESET = to_ansi.__func__(NONE) + def visit_table(self, node): + props = TableSizeCalculator(self.document) + node.walkabout(props) -from .functional import npartial + writer = TableWriter(props, self.document, **self.options) + node.walkabout(writer) + self.addlines(writer.lines) -class ANSITranslator(nodes.NodeVisitor): + # Do not recurse + raise nodes.SkipChildren - class Context(object): - - def __init__(self): - self.output = '' - self.indent_level = 0 - self.in_list = False - self.has_title = False - self.list_counter = 0 - self.node_type = '' - - class StyleContext(object): - - def __init__(self): - self.styles = set() - self.fg = ANSICodes.NONE - self.bg = ANSICodes.NONE - - def __init__(self, document, termsize=None, **options): - nodes.NodeVisitor.__init__(self, document) - self.document = document - self.output = '' - self.lines = [''] - self.line = 0 - self.indent_width = 2 - self.termsize = termsize or get_terminal_size((80,20)) - self.options = options - self.references = [] - self.refcount = 0 - - self.ctx = self.Context() - self.ctx_stack = [] - self.style = self.StyleContext() - self.style_stack = [] - - def push_ctx(self, **kwargs): - self.ctx_stack.append(self.ctx) - self.ctx = deepcopy(self.ctx) - for k, v in kwargs.items(): - setattr(self.ctx, k, v) - - def pop_ctx(self): - self.ctx = self.ctx_stack.pop() - - def push_style(self, fg=None, bg=None, styles=[]): - self.style_stack.append(self.style) - self.style = deepcopy(self.style) - if fg: - self.style.fg = ANSICodes.get_color_code(fg, True) - if bg: - self.style.bg = ANSICodes.get_color_code(bg, False) - self.style.styles |= {ANSICodes.get_style_code(s) for s in styles} - - self._restyle() - - def pop_style(self): - self.style = self.style_stack.pop() - reset = self.style.fg == ANSICodes.NONE and \ - self.style.bg == ANSICodes.NONE and \ - not self.style.styles - self._restyle(reset) - - def append(self, *args, **kwargs): - try: - strict = kwargs['strict'] - except KeyError: - strict = False - if len(self.lines[self.line]) == 0 and not strict: - self.lines[self.line] += ' ' * self.ctx.indent_level * self.indent_width - - for a in args: - self.lines[self.line] += u(a) - - def newline(self, n=1): - self.lines.extend([''] * n) - self.line += n - - def prevline(self, n=1): - self.line -= n - - def nextline(self, n=1): - self.line += n - - def popline(self): - l = self.lines.pop(self.line) - self.line -= 1 - return l - - def replaceline(self, newline, strict=True): - if strict: - self.lines[self.line] = newline - else: - self.lines[self.line] = '' - self.append(newline) - - def addlines(self, lines, strict=False): - if strict: - self.lines.extend(lines) - self.line += len(lines) - self.newline() - else: - for l in lines: - self.append(l) + def depart_table(self, node): self.newline() - def _restyle(self, reset=False): - if reset: - self.append(ANSICodes.RESET) - - styles = list(self.style.styles) - if self.style.fg != ANSICodes.NONE: - styles.append(self.style.fg) - if self.style.bg != ANSICodes.NONE: - styles.append(self.style.bg) - - if styles: - self.append(ANSICodes.to_ansi(styles)) - - def strip_empty_lines(self): - remove_last_n = 0 - for x in self.lines[::-1]: - if len(x.strip()) != 0: - break - remove_last_n += 1 - if remove_last_n != 0: - self.lines = self.lines[:-remove_last_n] - - # Structural nodes - - def visit_document(self, node): - self.push_ctx() - - def _print_references(self): - if not self.references: - return - - self.push_style(styles = ['bold']) - self.append('References:') - self.pop_style() - self.newline(2) - - self.push_ctx(indent_level = self.ctx.indent_level + 1) - for ref in self.references: - self.append('[%s]: <' % ref[0]) - self.push_style(fg = 'cyan', styles = ['underline']) - self.append(ref[1]) - self.pop_style() - self.append('>') - self.newline() - self.references = [] - self.pop_ctx() - - def depart_document(self, node): - self._print_references() - self.depart_section(node) - - self.pop_ctx() - self.strip_empty_lines() - - self.output = '\n'.join(self.lines) - - def wrap_current_line(self): - indent = self.ctx.indent_level * self.indent_width - sublines = wrap(self.curline, width = self.termsize[0] - indent, - subsequent_indent = ' ' * indent) - self.popline() - self.addlines(sublines, strict=True) - - def depart_paragraph(self, node): - if self.options.get('wrap_paragraphs', True): - self.wrap_current_line() - if not self.ctx.in_list: - self.newline() - - def visit_title(self, node): - self.push_style(styles=['bold']) - - def depart_title(self, node): - self.pop_style() - self.push_ctx(has_title = True, indent_level = self.ctx.indent_level + 1) - self.newline(2) - - def visit_subtitle(self, node): - self.prevline(2) - self.append(' - ') - - def depart_subtitle(self, node): - self.nextline(2) - - def visit_Text(self, node): - self.append(node.astext()) - - def depart_section(self, node): - if self.ctx.has_title: - self.pop_ctx() - - def depart_transition(self, node): - indent = (self.ctx.indent_level + 2) * self.indent_width - char = '╌' if self.options['unicode'] else '-' - self.append(' ' * indent + char * (self.termsize[0] - 2 * indent) + ' ' * indent, strict=True) - self.newline(2) - - def _get_uri(self, node): - uri = node.attributes.get('refuri', '') - if not uri: - uri = node.attributes.get('uri', '') - return uri - - def visit_reference(self, node): - if self._get_uri(node) == node.astext().strip(): - self.append('<') - self.push_style(fg = 'cyan', styles = ['underline']) - - def depart_reference(self, node): - self.pop_style() - if self._get_uri(node) == node.astext().strip(): - self.append('>') - else: - self.references.append((self.refcount, self._get_uri(node))) - if self.options['unicode'] and self.options.get('unicode_superscript', False): - self.append(ref_to_unicode(self.refcount)) - else: - self.append(' [%s]' % self.refcount) - self.refcount += 1 - - # Style nodes - - visit_strong = npartial(push_style, styles=['bold']) - depart_strong = npartial(pop_style) - - visit_emphasis = npartial(push_style, styles=['italic']) - depart_emphasis = npartial(pop_style) - - def visit_TextElement(self, node): - ansi_props = [x[5:] for x in node.attributes['classes'] if x.startswith('ansi-')] - style = { - 'fg': next((x[3:] for x in ansi_props if x.startswith('fg-') and x[3:] in COLORS), None), - 'bg': next((x[3:] for x in ansi_props if x.startswith('bg-') and x[3:] in COLORS), None), - 'styles': (x for x in ansi_props if x in STYLES) - } - self.push_style(**style) - - def depart_TextElement(self, node): - self.pop_style() - - visit_inline = visit_TextElement - depart_inline = depart_TextElement - - # Lists - - def visit_enumerated_list(self, node): - strt = node.attributes.get('start', 1) - self.push_ctx(in_list = True, - list_counter = strt) - - def depart_enumerated_list(self, node): - self.pop_ctx() - if not self.ctx.in_list: - self.newline() - - def visit_bullet_list(self, node): - self.push_ctx(in_list = True, - list_counter = 0) - - def depart_bullet_list(self, node): - self.pop_ctx() - if not self.ctx.in_list: - self.newline() - - def visit_list_item(self, node): - if self.ctx.list_counter: - self.append(str(self.ctx.list_counter) + '. ') - self.ctx.list_counter += 1 - else: - self.append('• ' if self.options['unicode'] else '* ') - self.push_ctx(indent_level = self.ctx.indent_level + 1) - - def depart_list_item(self, node): - self.pop_ctx() - - visit_definition_list = npartial(push_ctx, in_list=True, list_counter=0) - depart_definition_list = npartial(pop_ctx) - - def visit_definition(self, node): - self.newline() - self.push_ctx(indent_level = self.ctx.indent_level + 1) - - def depart_definition(self, node): - self.newline() - self.pop_ctx() - - visit_option_list = npartial(push_ctx, in_list=True, list_counter=0) - depart_option_list = npartial(pop_ctx) - - def depart_option(self, node): - self.append(' | ') - - def depart_option_group(self, node): - self.replaceline(self.lines[self.line][:-3], strict=True) - self.push_ctx(indent_level = self.ctx.indent_level + 2) - self.newline() - - def visit_option_argument(self, node): - self.append(' ') - - def depart_option_list_item(self, node): - self.pop_ctx() - - # Tables - - def visit_table(self, node): - props = TableSizeCalculator(self.document) - node.walkabout(props) - - writer = TableWriter(props, self.document, **self.options) - node.walkabout(writer) - self.addlines(writer.lines) - - # Do not recurse - raise nodes.SkipChildren - - def depart_table(self, node): - self.newline() + # Misc + + def depart_image(self, node): + if type(node.parent) == nodes.figure: + self.visit_reference(node) + self.append("[" + node.attributes.get("alt", "Image") + "]") + self.depart_reference(node) + self.newline() + else: + self.append("[" + node.attributes.get("alt", "Image") + "]") - # Misc - - def depart_image(self, node): - if type(node.parent) == nodes.figure: - self.visit_reference(node) - self.append('[' + node.attributes.get('alt', 'Image') + ']') - self.depart_reference(node) - self.newline() - else: - self.append('[' + node.attributes.get('alt', 'Image') + ']') - - def depart_caption(self, node): - self.newline(2) - - def visit_substitution_definition(self, node): - raise nodes.SkipChildren + def depart_caption(self, node): + self.newline(2) - def visit_comment(self, node): - raise nodes.SkipChildren + def visit_substitution_definition(self, node): + raise nodes.SkipChildren - def depart_admonition(self, node): - if self.ctx.has_title: - self.pop_ctx() + def visit_comment(self, node): + raise nodes.SkipChildren - def visit_block_quote(self, node): - self.push_ctx(indent_level = self.ctx.indent_level + 1) - - def depart_block_quote(self, node): - self.pop_ctx() - - def depart_literal_block(self, node): - sublines = self.curline.split('\n') - self.replaceline(sublines[0]) - self.newline() - self.addlines(sublines[1:]) - self.newline() - - def depart_line(self, node): - if len(self.curline.strip()) == 0: - self.newline() - else: - self.wrap_current_line() + def depart_admonition(self, node): + if self.ctx.has_title: + self.pop_ctx() - def visit_line_block(self, node): - indent = self.ctx.indent_level + (1 if self.ctx.node_type == 'line_block' else 0) - self.push_ctx(indent_level = indent, node_type = 'line_block') + def visit_block_quote(self, node): + self.push_ctx(indent_level=self.ctx.indent_level + 1) - def depart_line_block(self, node): - self.pop_ctx() - if self.ctx.node_type != 'line_block': - self.newline() + def depart_block_quote(self, node): + self.pop_ctx() - def __getattr__(self, name): - if name.startswith('visit_') or name.startswith('depart_'): - def noop(*args, **kwargs): - pass - return noop - if name == 'curline': - return self.lines[self.line] - raise AttributeError(name) + def depart_literal_block(self, node): + sublines = self.curline.split("\n") + self.replaceline(sublines[0]) + self.newline() + self.addlines(sublines[1:]) + self.newline() + def depart_line(self, node): + if len(self.curline.strip()) == 0: + self.newline() + else: + self.wrap_current_line() + + def visit_line_block(self, node): + indent = self.ctx.indent_level + ( + 1 if self.ctx.node_type == "line_block" else 0 + ) + self.push_ctx(indent_level=indent, node_type="line_block") + + def depart_line_block(self, node): + self.pop_ctx() + if self.ctx.node_type != "line_block": + self.newline() + + def __getattr__(self, name): + if name.startswith("visit_") or name.startswith("depart_"): + + def noop(*args, **kwargs): + pass + + return noop + if name == "curline": + return self.lines[self.line] + raise AttributeError(name) diff --git a/rst2ansi/functional.py b/rst2ansi/functional.py index 90b226b..018df7d 100644 --- a/rst2ansi/functional.py +++ b/rst2ansi/functional.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,10 +22,13 @@ THE SOFTWARE. """ + def npartial(func, *args, **kwargs): - """ - Returns a partial node visitor function - """ - def wrapped(self, node): - func(self, *args, **kwargs) - return wrapped + """ + Returns a partial node visitor function + """ + + def wrapped(self, node): + func(self, *args, **kwargs) + + return wrapped diff --git a/rst2ansi/get_terminal_size.py b/rst2ansi/get_terminal_size.py index a25d16b..af67bd8 100644 --- a/rst2ansi/get_terminal_size.py +++ b/rst2ansi/get_terminal_size.py @@ -32,7 +32,6 @@ import os import struct import sys - from collections import namedtuple __all__ = ["get_terminal_size"] @@ -41,7 +40,7 @@ terminal_size = namedtuple("terminal_size", "columns lines") try: - from ctypes import windll, create_string_buffer, WinError + from ctypes import WinError, create_string_buffer, windll _handle_ids = { 0: -10, @@ -52,7 +51,7 @@ def _get_terminal_size(fd): handle = windll.kernel32.GetStdHandle(_handle_ids[fd]) if handle == 0: - raise OSError('handle cannot be retrieved') + raise OSError("handle cannot be retrieved") if handle == -1: raise WinError() csbi = create_string_buffer(22) @@ -73,7 +72,7 @@ def _get_terminal_size(fd): def _get_terminal_size(fd): try: res = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 4) - except IOError as e: + except OSError as e: raise OSError(e) lines, columns = struct.unpack("hh", res) @@ -123,4 +122,3 @@ def get_terminal_size(fallback=(80, 24)): lines = size.lines return terminal_size(columns, lines) - diff --git a/rst2ansi/table.py b/rst2ansi/table.py index 9f867a5..7ad95db 100644 --- a/rst2ansi/table.py +++ b/rst2ansi/table.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,301 +22,329 @@ THE SOFTWARE. """ -from __future__ import unicode_literals - -from docutils import nodes from textwrap import wrap +from docutils import nodes + from .unicode import u + class CellDimCalculator(nodes.NodeVisitor): + def __init__(self, document, cols, rows, width): + nodes.NodeVisitor.__init__(self, document) + self.cols = cols + self.rows = rows + self.width = width + self.height = 0 - def __init__(self, document, cols, rows, width): - nodes.NodeVisitor.__init__(self, document) - self.cols = cols - self.rows = rows - self.width = width - self.height = 0 + def visit_paragraph(self, node): + first_line = node.astext().split("\n")[0] - def visit_paragraph(self, node): - first_line = node.astext().split('\n')[0] + # handle weird table sizing from simple rst tables + # disregard cells spanning multiple columns, as + # these don't contribute to the cell width calculation + if len(first_line) >= self.width: + self.width = len(first_line) + 2 - # handle weird table sizing from simple rst tables - # disregard cells spanning multiple columns, as - # these don't contribute to the cell width calculation - if len(first_line) >= self.width: - self.width = len(first_line) + 2 + sublines = wrap(node.astext(), width=self.width) + self.height = int(len(sublines) / self.rows) + raise nodes.StopTraversal - sublines = wrap(node.astext(), width = self.width) - self.height = int(len(sublines) / self.rows) - raise nodes.StopTraversal + def visit_table(self, node): + c = TableSizeCalculator(self.document) + node.walkabout(c) + self.height = int(c.height / self.rows) + raise nodes.StopTraversal - def visit_table(self, node): - c = TableSizeCalculator(self.document) - node.walkabout(c) - self.height = int(c.height / self.rows) - raise nodes.StopTraversal + def visit_literal_block(self, node): + self.height = int(len(node.astext().split("\n")) / self.rows) + raise nodes.StopTraversal - def visit_literal_block(self, node): - self.height = int(len(node.astext().split('\n')) / self.rows) - raise nodes.StopTraversal + visit_Text = visit_literal_block - visit_Text = visit_literal_block + def __getattr__(self, name): + if name.startswith("visit_") or name.startswith("depart_"): - def __getattr__(self, name): - if name.startswith('visit_') or name.startswith('depart_'): - def noop(*args, **kwargs): - pass - return noop - raise AttributeError(name) + def noop(*args, **kwargs): + pass + + return noop + raise AttributeError(name) class TableSizeCalculator(nodes.NodeVisitor): + def __init__(self, document): + nodes.NodeVisitor.__init__(self, document) + self.level = 0 + self.widths = [] + self.heights = [] + self.rows = 0 + + def __getattr__(self, name): + if name.startswith("visit_") or name.startswith("depart_"): + + def noop(*args, **kwargs): + pass + + return noop + raise AttributeError(name) + + def visit_table(self, node): + if self.level > 0: + raise nodes.SkipChildren + self.level += 1 + + def depart_table(self, node): + self.width = sum(self.widths) + (len(self.widths) + 1) + self.height = sum(self.heights) + (len(self.heights) + 1) + self.level -= 1 + + def visit_tgroup(self, node): + self.cols = node.attributes["cols"] + + def visit_colspec(self, node): + self.widths.append(node.attributes["colwidth"]) + + def visit_row(self, node): + self.rows += 1 + self.heights.append(1) + self.col = 0 + + def visit_entry(self, node): + cols = node.attributes.get("morecols", 0) + 1 + rows = node.attributes.get("morerows", 0) + 1 + width = sum(self.widths[self.col : self.col + cols]) + (cols - 1) + + c = CellDimCalculator(self.document, cols, rows, width) + node.walkabout(c) + + # Correct invalid column sizing for simple rst tables + if c.width > width and cols == 1: + self.widths[self.col] = c.width + + self.heights[-1] = max(self.heights[-1], c.height) + self.col += 1 + raise nodes.SkipChildren - def __init__(self, document): - nodes.NodeVisitor.__init__(self, document) - self.level = 0 - self.widths = [] - self.heights = [] - self.rows = 0 - - def __getattr__(self, name): - if name.startswith('visit_') or name.startswith('depart_'): - def noop(*args, **kwargs): - pass - return noop - raise AttributeError(name) - - def visit_table(self, node): - if self.level > 0: - raise nodes.SkipChildren - self.level += 1 - - def depart_table(self, node): - self.width = sum(self.widths) + (len(self.widths) + 1) - self.height = sum(self.heights) + (len(self.heights) + 1) - self.level -= 1 - - def visit_tgroup(self, node): - self.cols = node.attributes['cols'] - - def visit_colspec(self, node): - self.widths.append(node.attributes['colwidth']) - - def visit_row(self, node): - self.rows += 1 - self.heights.append(1) - self.col = 0 - - def visit_entry(self, node): - cols = node.attributes.get('morecols', 0) + 1 - rows = node.attributes.get('morerows', 0) + 1 - width = sum(self.widths[self.col:self.col + cols]) + (cols - 1) - - c = CellDimCalculator(self.document, cols, rows, width) - node.walkabout(c) - - # Correct invalid column sizing for simple rst tables - if c.width > width and cols == 1: - self.widths[self.col] = c.width - - self.heights[-1] = max(self.heights[-1], c.height) - self.col += 1 - raise nodes.SkipChildren class TableDrawer(nodes.NodeVisitor): + def __init__(self, props, document, **options): + nodes.NodeVisitor.__init__(self, document) + self.props = props + self.level = 0 + self.lines = [""] + self.line = 0 + self.cursor = 0 + self.col = 0 + self.row = 0 + self.nb_rows = 0 + self.options = options + + def unicode_intersection(char, next): + switch = { + ("─", "│"): "┬", + ("┐", "│"): "┐", + ("┘", "│"): "┤", + ("┘", "─"): "┴", + ("┴", "│"): "┼", + ("│", "─"): "├", + ("┤", "─"): "┼", + (" ", "─"): "┘", + ("└", "─"): "└", + ("═", "│"): "╤", + ("╕", "│"): "╕", + ("╛", "│"): "╡", + ("╛", "═"): "╧", + ("╧", "│"): "╪", + ("│", "═"): "╞", + ("╡", "═"): "╪", + (" ", "═"): "╛", + ("╘", "═"): "╘", + } + return switch[(u(char), u(next))] + + if options.get("unicode", False): + self.char_single_rule = "─" + self.char_double_rule = "═" + self.char_vertical_rule = "│" + self.get_intersection = unicode_intersection + self.top_left = "┌" + self.top_right = "┐" + self.bottom_left = "╘" + else: + self.char_single_rule = "-" + self.char_double_rule = "=" + self.char_vertical_rule = "|" + self.get_intersection = lambda *args: "+" + self.top_left = self.bottom_left = self.top_right = "+" + + def __getattr__(self, name): + if name.startswith("visit_") or name.startswith("depart_"): + + def noop(*args, **kwargs): + pass + + return noop + if name == "curline": + return self.lines[self.line] + raise AttributeError(name) + + def _draw_rule(self): + self.lines[self.line] += ( + self.top_left + + self.char_single_rule * (self.props.width - 2) + + self.top_right + ) + self.lines.extend( + [self.char_vertical_rule + " " * (self.props.width - 1)] + * (self.props.height - 2) + ) + self.lines.extend([self.bottom_left + " " * (self.props.width - 1)]) + self.line += 1 + self.cursor = 0 + + def visit_table(self, node): + if self.level > 0: + raise nodes.SkipChildren + self.level += 1 + self._draw_rule() + + def depart_table(self, node): + self.level -= 1 + + def visit_row(self, node): + self.col = 0 + self.cursor = 0 + + def depart_row(self, node): + self.line += self.props.heights[self.row] + 1 + self.row += 1 + self.local_row += 1 + + def visit_thead(self, node): + self.nb_rows = len(node.children) + self.local_row = 0 + + visit_tbody = visit_thead + + def visit_entry(self, node): + cols = node.attributes.get("morecols", 0) + 1 + rows = node.attributes.get("morerows", 0) + 1 + + width = sum(self.props.widths[self.col : self.col + cols]) + (cols - 1) + height = sum(self.props.heights[self.row : self.row + rows]) + (rows - 1) + + rule = ( + self.char_double_rule + if self.local_row + rows - 1 == self.nb_rows - 1 + else self.char_single_rule + ) + sep = self.char_vertical_rule + + # Draw the horizontal rule + + line = self.lines[self.line + height] + int1 = self.get_intersection(line[self.cursor], rule) + int2 = self.get_intersection(line[self.cursor + width + 1], rule) + line = ( + line[: self.cursor] + + int1 + + (width * rule) + + int2 + + line[self.cursor + width + 2 :] + ) + self.lines[self.line + height] = line + + # Draw the vertical rule + + for i in range(height): + line = self.lines[self.line + i] + line = ( + line[: self.cursor + width + 1] + sep + line[self.cursor + width + 2 :] + ) + self.lines[self.line + i] = line + + line = self.lines[self.line - 1] + int3 = self.get_intersection(line[self.cursor + width + 1], sep) + line = line[: self.cursor + width + 1] + int3 + line[self.cursor + width + 2 :] + self.lines[self.line - 1] = line + + self.col += cols + self.cursor += width + 1 + + # Do not recurse + raise nodes.SkipChildren - def __init__(self, props, document, **options): - nodes.NodeVisitor.__init__(self, document) - self.props = props - self.level = 0 - self.lines = [''] - self.line = 0 - self.cursor = 0 - self.col = 0 - self.row = 0 - self.nb_rows = 0 - self.options = options - - def unicode_intersection(char, next): - switch = { - ('─', '│'): '┬', - ('┐', '│'): '┐', - ('┘', '│'): '┤', - ('┘', '─'): '┴', - ('┴', '│'): '┼', - ('│', '─'): '├', - ('┤', '─'): '┼', - (' ', '─'): '┘', - ('└', '─'): '└', - - ('═', '│'): '╤', - ('╕', '│'): '╕', - ('╛', '│'): '╡', - ('╛', '═'): '╧', - ('╧', '│'): '╪', - ('│', '═'): '╞', - ('╡', '═'): '╪', - (' ', '═'): '╛', - ('╘', '═'): '╘', - } - return switch[(u(char), u(next))] - - if options.get('unicode', False): - self.char_single_rule = '─' - self.char_double_rule = '═' - self.char_vertical_rule = '│' - self.get_intersection = unicode_intersection - self.top_left = '┌' - self.top_right = '┐' - self.bottom_left = '╘' - else: - self.char_single_rule = '-' - self.char_double_rule = '=' - self.char_vertical_rule = '|' - self.get_intersection = lambda *args: '+' - self.top_left = self.bottom_left = self.top_right = '+' - - def __getattr__(self, name): - if name.startswith('visit_') or name.startswith('depart_'): - def noop(*args, **kwargs): - pass - return noop - if name == 'curline': - return self.lines[self.line] - raise AttributeError(name) - - def _draw_rule(self): - self.lines[self.line] += self.top_left + self.char_single_rule * (self.props.width - 2) + self.top_right - self.lines.extend([self.char_vertical_rule + ' ' * (self.props.width - 1)] * (self.props.height - 2)) - self.lines.extend([self.bottom_left + ' ' * (self.props.width - 1)]) - self.line += 1 - self.cursor = 0 - - def visit_table(self, node): - if self.level > 0: - raise nodes.SkipChildren - self.level += 1 - self._draw_rule() - - def depart_table(self, node): - self.level -= 1 - - def visit_row(self, node): - self.col = 0 - self.cursor = 0 - - def depart_row(self, node): - self.line += self.props.heights[self.row] + 1 - self.row += 1 - self.local_row += 1 - - def visit_thead(self, node): - self.nb_rows = len(node.children) - self.local_row = 0 - - visit_tbody = visit_thead - - def visit_entry(self, node): - cols = node.attributes.get('morecols', 0) + 1 - rows = node.attributes.get('morerows', 0) + 1 - - width = sum(self.props.widths[self.col:self.col + cols]) + (cols - 1) - height = sum(self.props.heights[self.row:self.row + rows]) + (rows - 1) - - rule = self.char_double_rule if self.local_row + rows - 1 == self.nb_rows - 1 else self.char_single_rule - sep = self.char_vertical_rule - - # Draw the horizontal rule - - line = self.lines[self.line + height] - int1 = self.get_intersection(line[self.cursor], rule) - int2 = self.get_intersection(line[self.cursor + width + 1], rule) - line = line[:self.cursor] + int1 + (width * rule) + int2 + line[self.cursor + width + 2:] - self.lines[self.line + height] = line - - # Draw the vertical rule - - for i in range(height): - line = self.lines[self.line + i] - line = line[:self.cursor + width + 1] + sep + line[self.cursor + width + 2:] - self.lines[self.line + i] = line - - line = self.lines[self.line - 1] - int3 = self.get_intersection(line[self.cursor + width + 1], sep) - line = line[:self.cursor + width + 1] + int3 + line[self.cursor + width + 2:] - self.lines[self.line - 1] = line - - self.col += cols - self.cursor += width + 1 - - # Do not recurse - raise nodes.SkipChildren class TableWriter(nodes.NodeVisitor): - - def __init__(self, props, document, **options): - nodes.NodeVisitor.__init__(self, document) - self.props = props - self.level = 0 - self.line = 0 - self.cursor = 0 - self.col = 0 - self.row = 0 - self.nb_rows = 0 - self.options = options - - def __getattr__(self, name): - if name.startswith('visit_') or name.startswith('depart_'): - def noop(*args, **kwargs): - pass - return noop - raise AttributeError(name) - - def visit_table(self, node): - drawer = TableDrawer(self.props, self.document, **self.options) - node.walkabout(drawer) - self.lines = drawer.lines - - def visit_row(self, node): - self.col = 0 - self.cursor = 0 - - def depart_row(self, node): - self.line += self.props.heights[self.row] + 1 - self.row += 1 - self.local_row += 1 - - def visit_thead(self, node): - self.nb_rows = len(node.children) - self.local_row = 0 - - visit_tbody = visit_thead - - def visit_entry(self, node): - cols = node.attributes.get('morecols', 0) + 1 - rows = node.attributes.get('morerows', 0) + 1 - - width = sum(self.props.widths[self.col:self.col + cols]) + (cols - 1) - height = sum(self.props.heights[self.row:self.row + rows]) + (rows - 1) - - from .ansi import ANSITranslator - - if node.children: - v = ANSITranslator(self.document, termsize=(width - 2, height), **self.options) - node.children[0].walkabout(v) - v.strip_empty_lines() - i = 1 - for l in v.lines: - for sl in l.split('\n'): - line = self.lines[self.line + i] - line = line[:self.cursor + 2] + sl + line[self.cursor + 2 + len(sl):] - self.lines[self.line + i] = line - i += 1 - - self.col += cols - self.cursor += width + 1 - - # Do not recurse - raise nodes.SkipChildren + def __init__(self, props, document, **options): + nodes.NodeVisitor.__init__(self, document) + self.props = props + self.level = 0 + self.line = 0 + self.cursor = 0 + self.col = 0 + self.row = 0 + self.nb_rows = 0 + self.options = options + + def __getattr__(self, name): + if name.startswith("visit_") or name.startswith("depart_"): + + def noop(*args, **kwargs): + pass + + return noop + raise AttributeError(name) + + def visit_table(self, node): + drawer = TableDrawer(self.props, self.document, **self.options) + node.walkabout(drawer) + self.lines = drawer.lines + + def visit_row(self, node): + self.col = 0 + self.cursor = 0 + + def depart_row(self, node): + self.line += self.props.heights[self.row] + 1 + self.row += 1 + self.local_row += 1 + + def visit_thead(self, node): + self.nb_rows = len(node.children) + self.local_row = 0 + + visit_tbody = visit_thead + + def visit_entry(self, node): + cols = node.attributes.get("morecols", 0) + 1 + rows = node.attributes.get("morerows", 0) + 1 + + width = sum(self.props.widths[self.col : self.col + cols]) + (cols - 1) + height = sum(self.props.heights[self.row : self.row + rows]) + (rows - 1) + + from .ansi import ANSITranslator + + if node.children: + v = ANSITranslator( + self.document, termsize=(width - 2, height), **self.options + ) + node.children[0].walkabout(v) + v.strip_empty_lines() + i = 1 + for l in v.lines: + for sl in l.split("\n"): + line = self.lines[self.line + i] + line = ( + line[: self.cursor + 2] + sl + line[self.cursor + 2 + len(sl) :] + ) + self.lines[self.line + i] = line + i += 1 + + self.col += cols + self.cursor += width + 1 + + # Do not recurse + raise nodes.SkipChildren diff --git a/rst2ansi/unicode.py b/rst2ansi/unicode.py index dff892e..da85245 100644 --- a/rst2ansi/unicode.py +++ b/rst2ansi/unicode.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,38 +22,38 @@ THE SOFTWARE. """ -from __future__ import unicode_literals import sys + def num_to_superscript(n): - sups = { - '0': '\u2070', - '1': '\xb9', - '2': '\xb2', - '3': '\xb3', - '4': '\u2074', - '5': '\u2075', - '6': '\u2076', - '7': '\u2077', - '8': '\u2078', - '9': '\u2079' - } - return ''.join(sups.get(c, c) for c in str(n)) + sups = { + "0": "\u2070", + "1": "\xb9", + "2": "\xb2", + "3": "\xb3", + "4": "\u2074", + "5": "\u2075", + "6": "\u2076", + "7": "\u2077", + "8": "\u2078", + "9": "\u2079", + } + return "".join(sups.get(c, c) for c in str(n)) + def ref_to_unicode(n): - return '⁽' + num_to_superscript(n) + '⁾' + return "⁽" + num_to_superscript(n) + "⁾" + def u(s): - # Useful for very coarse version differentiation. - PY2 = sys.version_info[0] == 2 - PY3 = sys.version_info[0] == 3 - if PY3: - return s - else: + # Useful for very coarse version differentiation. + PY3 = sys.version_info[0] == 3 + if PY3: + return s # Workaround for standalone backslash try: - ret_s = unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + ret_s = s.replace(r"\\", r"\\\\").encode("unicode_escape") except TypeError: - ret_s = s.replace(r'\\', r'\\\\') + ret_s = s.replace(r"\\", r"\\\\") return ret_s diff --git a/rst2ansi/visitor.py b/rst2ansi/visitor.py index ef77aa0..870bd47 100644 --- a/rst2ansi/visitor.py +++ b/rst2ansi/visitor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,25 +22,22 @@ THE SOFTWARE. """ -from docutils import core, frontend, nodes, utils, writers, languages, io -from docutils.utils.error_reporting import SafeString +from docutils import writers from docutils.transforms import writer_aux -from docutils.parsers.rst import roles from .ansi import ANSITranslator -class Writer(writers.Writer): - - def __init__(self, **options): - writers.Writer.__init__(self) - self.translator_class = ANSITranslator - self.options = options - - def translate(self): - visitor = self.translator_class(self.document, **self.options) - self.document.walkabout(visitor) - self.output = visitor.output - - def get_transforms(self): - return writers.Writer.get_transforms(self) + [writer_aux.Admonitions] +class Writer(writers.Writer): + def __init__(self, **options): + writers.Writer.__init__(self) + self.translator_class = ANSITranslator + self.options = options + + def translate(self): + visitor = self.translator_class(self.document, **self.options) + self.document.walkabout(visitor) + self.output = visitor.output + + def get_transforms(self): + return writers.Writer.get_transforms(self) + [writer_aux.Admonitions] diff --git a/rst2ansi/wrap.py b/rst2ansi/wrap.py index e00ad86..bbc4035 100644 --- a/rst2ansi/wrap.py +++ b/rst2ansi/wrap.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -25,33 +24,34 @@ import re -_ANSI_CODE = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') +_ANSI_CODE = re.compile(r"(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]") -def word_size(word): - return len(_ANSI_CODE.sub('', word)) - -def wrap(text, width=80, subsequent_indent=''): - words = text.split() - line_size = 0 - lines = [[]] - - for w in words: - size = word_size(w) + 1 - if size == 0: - continue - if line_size + size - 1 > width and line_size > width / 2: - line_size = len(subsequent_indent) - lines.append([]) - while line_size + size - 1 > width: - stripped = width - line_size - 1 - lines[-1].append(w[:stripped] + '-') - line_size = len(subsequent_indent) - lines.append([]) - w = w[stripped:] - size -= stripped - if size == 0: - continue - lines[-1].append(w) - line_size += size - return [subsequent_indent + ' '.join(words) for words in lines] +def word_size(word): + return len(_ANSI_CODE.sub("", word)) + + +def wrap(text, width=80, subsequent_indent=""): + words = text.split() + line_size = 0 + lines = [[]] + + for w in words: + size = word_size(w) + 1 + if size == 0: + continue + if line_size + size - 1 > width and line_size > width / 2: + line_size = len(subsequent_indent) + lines.append([]) + while line_size + size - 1 > width: + stripped = width - line_size - 1 + lines[-1].append(w[:stripped] + "-") + line_size = len(subsequent_indent) + lines.append([]) + w = w[stripped:] + size -= stripped + if size == 0: + continue + lines[-1].append(w) + line_size += size + return [subsequent_indent + " ".join(words) for words in lines] diff --git a/setup.py b/setup.py index 46727b8..ae50159 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,45 @@ #!/usr/bin/env python import os -import sys + try: - from setuptools import setup + from setuptools import setup except ImportError: - from distutils.core import setup + from distutils.core import setup + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( - name="rst2ansi", - version="0.1.5", - author="Snaipe", - author_email="franklinmathieu@gmail.com", - description="A rst converter to ansi-decorated console output", - long_description=read('README.rst'), - license="MIT", - keywords="rst restructuredtext ansi console code converter", - url="https://github.com/Snaipe/python-rst-to-ansi", - packages=['rst2ansi'], - requires=['docutils'], - scripts=['bin/rst2ansi'], - data_files=[], - classifiers=[ - "Development Status :: 4 - Beta", - "Environment :: Console", - "Intended Audience :: Developers", - "Intended Audience :: End Users/Desktop", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.3", - "Programming Language :: Python :: 3.4", - "Programming Language :: Python :: 3.5", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Text Processing :: Markup", - "Topic :: Utilities", - ], + name="rst2ansi", + version="0.1.5", + author="Snaipe", + author_email="franklinmathieu@gmail.com", + description="A rst converter to ansi-decorated console output", + long_description=read("README.rst"), + license="MIT", + keywords="rst restructuredtext ansi console code converter", + url="https://github.com/Snaipe/python-rst-to-ansi", + packages=["rst2ansi"], + requires=["docutils"], + scripts=["bin/rst2ansi"], + data_files=[], + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Developers", + "Intended Audience :: End Users/Desktop", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Text Processing :: Markup", + "Topic :: Utilities", + ], ) From 95c00ef9c0aa92d004f1c7669b2b33cec2c24468 Mon Sep 17 00:00:00 2001 From: Tim Cera Date: Sat, 4 Nov 2023 23:51:07 -0400 Subject: [PATCH 3/5] refactor: cleanup, refactor, and documentation changes --- rst2ansi/__init__.py | 8 +-- rst2ansi/ansi.py | 120 ++++++++++++++++++++------------ rst2ansi/get_terminal_size.py | 124 ---------------------------------- rst2ansi/table.py | 19 +++--- rst2ansi/unicode.py | 18 +---- rst2ansi/wrap.py | 2 +- setup.py | 5 +- 7 files changed, 92 insertions(+), 204 deletions(-) delete mode 100644 rst2ansi/get_terminal_size.py diff --git a/rst2ansi/__init__.py b/rst2ansi/__init__.py index 2af1a21..022b20c 100644 --- a/rst2ansi/__init__.py +++ b/rst2ansi/__init__.py @@ -22,7 +22,6 @@ THE SOFTWARE. """ - from docutils import core, nodes from docutils.parsers.rst import roles @@ -31,7 +30,6 @@ def rst2ansi(input_string, output_encoding="utf-8"): - overrides = {} overrides["input_encoding"] = "unicode" @@ -39,10 +37,10 @@ def style_role(name, rawtext, text, lineno, inliner, options={}, content=[]): return [nodes.TextElement(rawtext, text, classes=[name])], [] for color in COLORS: - roles.register_local_role("ansi-fg-" + color, style_role) - roles.register_local_role("ansi-bg-" + color, style_role) + roles.register_local_role(f"ansi-fg-{color}", style_role) + roles.register_local_role(f"ansi-bg-{color}", style_role) for style in STYLES: - roles.register_local_role("ansi-" + style, style_role) + roles.register_local_role(f"ansi-{style}", style_role) if hasattr(input_string, "decode"): input_string = input_string.decode("utf-8") diff --git a/rst2ansi/ansi.py b/rst2ansi/ansi.py index b75f87f..1e5bef5 100644 --- a/rst2ansi/ansi.py +++ b/rst2ansi/ansi.py @@ -22,14 +22,14 @@ THE SOFTWARE. """ - from copy import deepcopy +from os import get_terminal_size from docutils import nodes -from .get_terminal_size import get_terminal_size +from .functional import npartial from .table import TableSizeCalculator, TableWriter -from .unicode import ref_to_unicode, u +from .unicode import ref_to_unicode from .wrap import wrap COLORS = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white") @@ -47,47 +47,78 @@ class ANSICodes: + """Work with ANSI codes.""" + @staticmethod def get_color_code(code, fg): - FG = 30 - BG = 40 - FG_256 = 38 - BG_256 = 48 + """Return the ANSI code for a color. + + Parameters + ---------- + code : str, int, tuple, or list + The color code. If a string, it must be one of the following: + ``black``, ``red``, ``green``, ``yellow``, ``blue``, ``magenta``, + ``cyan``, ``white``. If an integer, it must be between 0 and 255. + If a tuple or list, it must be a 3-tuple of integers between 0 and + 255. + fg : bool + Whether to return a foreground or background color code. + """ + fgc_256 = 38 + bgc_256 = 48 if code in COLORS: - shift = FG if fg else BG + fgc = 30 + bgc = 40 + shift = fgc if fg else bgc return str(shift + COLORS.index(code)) - elif isinstance(code, int) and 0 <= code <= 255: - shift = FG_256 if fg else BG_256 - return str(shift) + ";5;%d" % int(code) - elif not isinstance(code, str) and hasattr(code, "__len__") and len(code) == 3: + + if isinstance(code, int) and 0 <= code <= 255: + shift = fgc_256 if fg else bgc_256 + return f"{shift};5;{code}" + + if isinstance(code, (tuple, list)) and len(code) == 3: for c in code: if not 0 <= c <= 255: - raise Exception('Invalid color "%s"' % code) + raise ValueError(f'Invalid color "{code}"') r, g, b = code - shift = FG_256 if fg else BG_256 - return str(shift) + ";2;%d;%d;%d" % (int(r), int(g), int(b)) + shift = fgc_256 if fg else bgc_256 + return f"{shift};2;{r};{g};{b}" - raise Exception('Invalid color "%s"' % code) + raise ValueError(f'Invalid color "{code}"') @staticmethod def get_style_code(code): + """Return the ANSI code for a style. + + Parameters + ---------- + code : str + The style code. It must be one of the following: + ``bold``, ``dim``, ``italic``, ``underline``, ``blink``, + ``blink-fast``, ``inverse``, ``conceal``, ``strikethrough``. + """ if code in STYLES: return str(1 + STYLES.index(code)) - raise Exception('Invalid style "%s"' % code) + raise ValueError(f'Invalid style "{code}"') @staticmethod def to_ansi(codes): - return "\x1b[" + ";".join(codes) + "m" + """Return the ANSI escape sequence for a list of codes. + + Parameters + ---------- + codes : list + A list of ANSI codes. + """ + codes = ";".join(codes) + return f"\x1b[{codes}m" NONE = "0" RESET = to_ansi.__func__(NONE) -from .functional import npartial - - class ANSITranslator(nodes.NodeVisitor): class Context: def __init__(self): @@ -111,7 +142,10 @@ def __init__(self, document, termsize=None, **options): self.lines = [""] self.line = 0 self.indent_width = 2 - self.termsize = termsize or get_terminal_size((80, 20)) + try: + self.termsize = termsize or get_terminal_size() + except OSError: + self.termsize = (80, 24) self.options = options self.references = [] self.refcount = 0 @@ -130,7 +164,9 @@ def push_ctx(self, **kwargs): def pop_ctx(self): self.ctx = self.ctx_stack.pop() - def push_style(self, fg=None, bg=None, styles=[]): + def push_style(self, fg=None, bg=None, styles=None): + if styles is None: + styles = [] self.style_stack.append(self.style) self.style = deepcopy(self.style) if fg: @@ -159,7 +195,7 @@ def append(self, *args, **kwargs): self.lines[self.line] += " " * self.ctx.indent_level * self.indent_width for a in args: - self.lines[self.line] += u(a) + self.lines[self.line] += a def newline(self, n=1): self.lines.extend([""] * n) @@ -172,9 +208,9 @@ def nextline(self, n=1): self.line += n def popline(self): - l = self.lines.pop(self.line) + line = self.lines.pop(self.line) self.line -= 1 - return l + return line def replaceline(self, newline, strict=True): if strict: @@ -189,8 +225,8 @@ def addlines(self, lines, strict=False): self.line += len(lines) self.newline() else: - for l in lines: - self.append(l) + for line in lines: + self.append(line) self.newline() def _restyle(self, reset=False): @@ -231,15 +267,18 @@ def _print_references(self): self.push_ctx(indent_level=self.ctx.indent_level + 1) for ref in self.references: - self.append("[%s]: <" % ref[0]) - self.push_style(fg="cyan", styles=["underline"]) - self.append(ref[1]) - self.pop_style() - self.append(">") - self.newline() + self._print_references_inner(ref) self.references = [] self.pop_ctx() + def _print_references_inner(self, ref): + self.append(f"[{ref[0]}]: <") + self.push_style(fg="cyan", styles=["underline"]) + self.append(ref[1]) + self.pop_style() + self.append(">") + self.newline() + def depart_document(self, node): self._print_references() self.depart_section(node) @@ -297,10 +336,7 @@ def depart_transition(self, node): self.newline(2) def _get_uri(self, node): - uri = node.attributes.get("refuri", "") - if not uri: - uri = node.attributes.get("uri", "") - return uri + return node.attributes.get("refuri", "") or node.attributes.get("uri", "") def visit_reference(self, node): if self._get_uri(node) == node.astext().strip(): @@ -318,7 +354,7 @@ def depart_reference(self, node): ): self.append(ref_to_unicode(self.refcount)) else: - self.append(" [%s]" % self.refcount) + self.append(f" [{self.refcount}]") self.refcount += 1 # Style nodes @@ -373,7 +409,7 @@ def depart_bullet_list(self, node): def visit_list_item(self, node): if self.ctx.list_counter: - self.append(str(self.ctx.list_counter) + ". ") + self.append(f"{str(self.ctx.list_counter)}. ") self.ctx.list_counter += 1 else: self.append("• " if self.options["unicode"] else "* ") @@ -431,11 +467,11 @@ def depart_table(self, node): def depart_image(self, node): if type(node.parent) == nodes.figure: self.visit_reference(node) - self.append("[" + node.attributes.get("alt", "Image") + "]") + self.append(f"[{node.attributes.get('alt', 'Image')}]") self.depart_reference(node) self.newline() else: - self.append("[" + node.attributes.get("alt", "Image") + "]") + self.append(f"[{node.attributes.get('alt', 'Image')}]") def depart_caption(self, node): self.newline(2) diff --git a/rst2ansi/get_terminal_size.py b/rst2ansi/get_terminal_size.py deleted file mode 100644 index af67bd8..0000000 --- a/rst2ansi/get_terminal_size.py +++ /dev/null @@ -1,124 +0,0 @@ -"""This is a backport of shutil.get_terminal_size from Python 3.3. - -The original implementation is in C, but here we use the ctypes and -fcntl modules to create a pure Python version of os.get_terminal_size. - -Pulled from https://github.com/chrippa/backports.shutil_get_terminal_size - - -The MIT License (MIT) - -Copyright (c) 2014 Christopher Rosell - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -""" - -import os -import struct -import sys -from collections import namedtuple - -__all__ = ["get_terminal_size"] - - -terminal_size = namedtuple("terminal_size", "columns lines") - -try: - from ctypes import WinError, create_string_buffer, windll - - _handle_ids = { - 0: -10, - 1: -11, - 2: -12, - } - - def _get_terminal_size(fd): - handle = windll.kernel32.GetStdHandle(_handle_ids[fd]) - if handle == 0: - raise OSError("handle cannot be retrieved") - if handle == -1: - raise WinError() - csbi = create_string_buffer(22) - res = windll.kernel32.GetConsoleScreenBufferInfo(handle, csbi) - if res: - res = struct.unpack("hhhhHhhhhhh", csbi.raw) - left, top, right, bottom = res[5:9] - columns = right - left + 1 - lines = bottom - top + 1 - return terminal_size(columns, lines) - else: - raise WinError() - -except ImportError: - import fcntl - import termios - - def _get_terminal_size(fd): - try: - res = fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\x00" * 4) - except OSError as e: - raise OSError(e) - lines, columns = struct.unpack("hh", res) - - return terminal_size(columns, lines) - - -def get_terminal_size(fallback=(80, 24)): - """Get the size of the terminal window. - - For each of the two dimensions, the environment variable, COLUMNS - and LINES respectively, is checked. If the variable is defined and - the value is a positive integer, it is used. - - When COLUMNS or LINES is not defined, which is the common case, - the terminal connected to sys.__stdout__ is queried - by invoking os.get_terminal_size. - - If the terminal size cannot be successfully queried, either because - the system doesn't support querying, or because we are not - connected to a terminal, the value given in fallback parameter - is used. Fallback defaults to (80, 24) which is the default - size used by many terminal emulators. - - The value returned is a named tuple of type os.terminal_size. - """ - # Try the environment first - try: - columns = int(os.environ["COLUMNS"]) - except (KeyError, ValueError): - columns = 0 - - try: - lines = int(os.environ["LINES"]) - except (KeyError, ValueError): - lines = 0 - - # Only query if necessary - if columns <= 0 or lines <= 0: - try: - size = _get_terminal_size(sys.__stdout__.fileno()) - except (NameError, OSError): - size = terminal_size(*fallback) - - if columns <= 0: - columns = size.columns - if lines <= 0: - lines = size.lines - - return terminal_size(columns, lines) diff --git a/rst2ansi/table.py b/rst2ansi/table.py index 7ad95db..410b9d0 100644 --- a/rst2ansi/table.py +++ b/rst2ansi/table.py @@ -22,13 +22,10 @@ THE SOFTWARE. """ - from textwrap import wrap from docutils import nodes -from .unicode import u - class CellDimCalculator(nodes.NodeVisitor): def __init__(self, document, cols, rows, width): @@ -141,7 +138,7 @@ def __init__(self, props, document, **options): self.nb_rows = 0 self.options = options - def unicode_intersection(char, next): + def unicode_intersection(char, next_intersection): switch = { ("─", "│"): "┬", ("┐", "│"): "┐", @@ -162,7 +159,7 @@ def unicode_intersection(char, next): (" ", "═"): "╛", ("╘", "═"): "╘", } - return switch[(u(char), u(next))] + return switch[(char, next_intersection)] if options.get("unicode", False): self.char_single_rule = "─" @@ -333,15 +330,15 @@ def visit_entry(self, node): ) node.children[0].walkabout(v) v.strip_empty_lines() - i = 1 - for l in v.lines: - for sl in l.split("\n"): - line = self.lines[self.line + i] + cnt = 1 + for line in v.lines: + for sl in line.split("\n"): + line = self.lines[self.line + cnt] line = ( line[: self.cursor + 2] + sl + line[self.cursor + 2 + len(sl) :] ) - self.lines[self.line + i] = line - i += 1 + self.lines[self.line + cnt] = line + cnt += 1 self.col += cols self.cursor += width + 1 diff --git a/rst2ansi/unicode.py b/rst2ansi/unicode.py index da85245..89be889 100644 --- a/rst2ansi/unicode.py +++ b/rst2ansi/unicode.py @@ -23,9 +23,6 @@ """ -import sys - - def num_to_superscript(n): sups = { "0": "\u2070", @@ -43,17 +40,4 @@ def num_to_superscript(n): def ref_to_unicode(n): - return "⁽" + num_to_superscript(n) + "⁾" - - -def u(s): - # Useful for very coarse version differentiation. - PY3 = sys.version_info[0] == 3 - if PY3: - return s - # Workaround for standalone backslash - try: - ret_s = s.replace(r"\\", r"\\\\").encode("unicode_escape") - except TypeError: - ret_s = s.replace(r"\\", r"\\\\") - return ret_s + return f"⁽{num_to_superscript(n)}⁾" diff --git a/rst2ansi/wrap.py b/rst2ansi/wrap.py index bbc4035..788cdda 100644 --- a/rst2ansi/wrap.py +++ b/rst2ansi/wrap.py @@ -45,7 +45,7 @@ def wrap(text, width=80, subsequent_indent=""): lines.append([]) while line_size + size - 1 > width: stripped = width - line_size - 1 - lines[-1].append(w[:stripped] + "-") + lines[-1].append(f"{w[:stripped]}-") line_size = len(subsequent_indent) lines.append([]) w = w[stripped:] diff --git a/setup.py b/setup.py index ae50159..3e3b257 100644 --- a/setup.py +++ b/setup.py @@ -2,10 +2,7 @@ import os -try: - from setuptools import setup -except ImportError: - from distutils.core import setup +from setuptools import setup def read(fname): From b64d5702f837f82056b5c80b7d995da0cbdf7240 Mon Sep 17 00:00:00 2001 From: Tim Cera Date: Sun, 15 Jun 2025 10:35:43 -0400 Subject: [PATCH 4/5] refactor: passes "ruff check" and ran isort --- rst2ansi/__init__.py | 3 ++- rst2ansi/ansi.py | 2 +- rst2ansi/visitor.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rst2ansi/__init__.py b/rst2ansi/__init__.py index 022b20c..f6ac108 100644 --- a/rst2ansi/__init__.py +++ b/rst2ansi/__init__.py @@ -22,9 +22,10 @@ THE SOFTWARE. """ -from docutils import core, nodes from docutils.parsers.rst import roles +from docutils import core, nodes + from .ansi import COLORS, STYLES from .visitor import Writer diff --git a/rst2ansi/ansi.py b/rst2ansi/ansi.py index 1e5bef5..82c24a3 100644 --- a/rst2ansi/ansi.py +++ b/rst2ansi/ansi.py @@ -465,7 +465,7 @@ def depart_table(self, node): # Misc def depart_image(self, node): - if type(node.parent) == nodes.figure: + if isinstance(node.parent, nodes.figure): self.visit_reference(node) self.append(f"[{node.attributes.get('alt', 'Image')}]") self.depart_reference(node) diff --git a/rst2ansi/visitor.py b/rst2ansi/visitor.py index 870bd47..a574d96 100644 --- a/rst2ansi/visitor.py +++ b/rst2ansi/visitor.py @@ -22,9 +22,10 @@ THE SOFTWARE. """ -from docutils import writers from docutils.transforms import writer_aux +from docutils import writers + from .ansi import ANSITranslator From 4644e442611a3ebd412f2f01804844da9af2cbd1 Mon Sep 17 00:00:00 2001 From: Tim Cera Date: Sat, 21 Feb 2026 23:56:03 -0500 Subject: [PATCH 5/5] refactor --- bin/rst2ansi | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/bin/rst2ansi b/bin/rst2ansi index 841b994..8b23eeb 100755 --- a/bin/rst2ansi +++ b/bin/rst2ansi @@ -6,22 +6,25 @@ import sys from rst2ansi import rst2ansi -parser = argparse.ArgumentParser(description='Prints a reStructuredText input in an ansi-decorated format suitable for console output.') -parser.add_argument('file', type=str, nargs='?', help='A path to the file to open') +parser = argparse.ArgumentParser( + description="Prints a reStructuredText input in an ansi-decorated format suitable for console output." +) +parser.add_argument("file", type=str, nargs="?", help="A path to the file to open") args = parser.parse_args() + def process_file(f): - out = rst2ansi(f.read()) - if out: - try: - print(out) - except UnicodeEncodeError: - print(out.encode('ascii', errors='backslashreplace').decode('ascii')) + out = rst2ansi(f.read()) + if out: + try: + print(out) + except UnicodeEncodeError: + print(out.encode("ascii", errors="backslashreplace").decode("ascii")) + if args.file: - with io.open(args.file, 'rb') as f: - process_file(f) + with io.open(args.file, "rb") as f: + process_file(f) else: - process_file(sys.stdin) - + process_file(sys.stdin)