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 diff --git a/bin/rst2ansi b/bin/rst2ansi index 435c7ed..8b23eeb 100755 --- a/bin/rst2ansi +++ b/bin/rst2ansi @@ -1,26 +1,30 @@ #!/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') +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) diff --git a/rst2ansi/__init__.py b/rst2ansi/__init__.py index 0ea9a97..f6ac108 100644 --- a/rst2ansi/__init__.py +++ b/rst2ansi/__init__.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,30 +22,33 @@ THE SOFTWARE. """ -from __future__ import unicode_literals - -from docutils import nodes, core from docutils.parsers.rst import roles -from .visitor import Writer +from docutils import core, nodes + from .ansi import COLORS, STYLES +from .visitor import Writer -def rst2ansi(input_string, output_encoding='utf-8'): - overrides = {} - overrides['input_encoding'] = 'unicode' +def rst2ansi(input_string, output_encoding="utf-8"): + 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(f"ansi-fg-{color}", style_role) + roles.register_local_role(f"ansi-bg-{color}", style_role) + for style in STYLES: + roles.register_local_role(f"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..82c24a3 100644 --- a/rst2ansi/ansi.py +++ b/rst2ansi/ansi.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,450 +22,507 @@ THE SOFTWARE. """ -from __future__ import unicode_literals +from copy import deepcopy +from os import get_terminal_size -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 docutils import nodes -from copy import deepcopy, copy +from .functional import npartial +from .table import TableSizeCalculator, TableWriter +from .unicode import ref_to_unicode from .wrap import wrap -from .table import TableSizeCalculator, TableWriter -from .unicode import ref_to_unicode, u +COLORS = ("black", "red", "green", "yellow", "blue", "magenta", "cyan", "white") +STYLES = ( + "bold", + "dim", + "italic", + "underline", + "blink", + "blink-fast", + "inverse", + "conceal", + "strikethrough", +) + + +class ANSICodes: + """Work with ANSI codes.""" + + @staticmethod + def get_color_code(code, fg): + """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: + fgc = 30 + bgc = 40 + shift = fgc if fg else bgc + return str(shift + COLORS.index(code)) + + 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 ValueError(f'Invalid color "{code}"') + + r, g, b = code + shift = fgc_256 if fg else bgc_256 + return f"{shift};2;{r};{g};{b}" + + 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 ValueError(f'Invalid style "{code}"') + + @staticmethod + def to_ansi(codes): + """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 .get_terminal_size import get_terminal_size -import shutil +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 + try: + self.termsize = termsize or get_terminal_size() + except OSError: + self.termsize = (80, 24) + 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=None): + if styles is 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] += 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): + line = self.lines.pop(self.line) + self.line -= 1 + return line + + 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 line in lines: + self.append(line) + 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._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() -COLORS = ('black', 'red', 'green', 'yellow', 'blue', 'magenta', 'cyan', 'white') -STYLES = ('bold', 'dim', 'italic', 'underline', 'blink', 'blink-fast', 'inverse', 'conceal', 'strikethrough') + 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): + return node.attributes.get("refuri", "") or node.attributes.get("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(f" [{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(f"{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 - # 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_image(self, node): + if isinstance(node.parent, nodes.figure): + self.visit_reference(node) + self.append(f"[{node.attributes.get('alt', 'Image')}]") + self.depart_reference(node) + self.newline() + else: + self.append(f"[{node.attributes.get('alt', 'Image')}]") - def visit_comment(self, node): - raise nodes.SkipChildren + def depart_caption(self, node): + self.newline(2) - def depart_admonition(self, node): - if self.ctx.has_title: - self.pop_ctx() + def visit_substitution_definition(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 visit_comment(self, node): + raise nodes.SkipChildren - 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_admonition(self, node): + if self.ctx.has_title: + self.pop_ctx() - def depart_line_block(self, node): - self.pop_ctx() - if self.ctx.node_type != 'line_block': - self.newline() + def visit_block_quote(self, node): + self.push_ctx(indent_level=self.ctx.indent_level + 1) - 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_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 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 deleted file mode 100644 index a25d16b..0000000 --- a/rst2ansi/get_terminal_size.py +++ /dev/null @@ -1,126 +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 windll, create_string_buffer, WinError - - _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 IOError 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 9f867a5..410b9d0 100644 --- a/rst2ansi/table.py +++ b/rst2ansi/table.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,301 +22,326 @@ THE SOFTWARE. """ -from __future__ import unicode_literals +from textwrap import wrap from docutils import nodes -from textwrap import wrap - -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_intersection): + switch = { + ("─", "│"): "┬", + ("┐", "│"): "┐", + ("┘", "│"): "┤", + ("┘", "─"): "┴", + ("┴", "│"): "┼", + ("│", "─"): "├", + ("┤", "─"): "┼", + (" ", "─"): "┘", + ("└", "─"): "└", + ("═", "│"): "╤", + ("╕", "│"): "╕", + ("╛", "│"): "╡", + ("╛", "═"): "╧", + ("╧", "│"): "╪", + ("│", "═"): "╞", + ("╡", "═"): "╪", + (" ", "═"): "╛", + ("╘", "═"): "╘", + } + return switch[(char, next_intersection)] + + 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() + 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 + cnt] = line + cnt += 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..89be889 100644 --- a/rst2ansi/unicode.py +++ b/rst2ansi/unicode.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,38 +22,22 @@ 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) + '⁾' - -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: - # Workaround for standalone backslash - try: - ret_s = unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - except TypeError: - ret_s = s.replace(r'\\', r'\\\\') - return ret_s + return f"⁽{num_to_superscript(n)}⁾" diff --git a/rst2ansi/visitor.py b/rst2ansi/visitor.py index ef77aa0..a574d96 100644 --- a/rst2ansi/visitor.py +++ b/rst2ansi/visitor.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- """ The MIT License (MIT) @@ -23,25 +22,23 @@ THE SOFTWARE. """ -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 .ansi import ANSITranslator - -class Writer(writers.Writer): +from docutils import writers - 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 +from .ansi import ANSITranslator - 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..788cdda 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(f"{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..3e3b257 100644 --- a/setup.py +++ b/setup.py @@ -1,43 +1,42 @@ #!/usr/bin/env python import os -import sys -try: - from setuptools import setup -except ImportError: - from distutils.core import setup + +from setuptools 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", + ], )