#!/usr/bin/fades r"""Render a nested box model like graphviz’s record format. There are three kinds of boxes: text boxes, hboxes, and vboxes. Hboxes and vboxes contain ordered lists of children, while text boxes just contain a bytestrings. Hboxes can only be children of vboxes, while vboxes can only be children of hboxes. Thus the structure can be specified entirely by S-expression-like nesting and a single bit to specify whether the root box is an hbox or a vbox. Routines are provided to convert a nested list to a box, a box to drawing instructions, drawing instructions to a list of bytearrays (one per line), and — for convenience — a ``lists_to_bytes`` function which skips over the intermediate steps and just gives you a bytestring. This code, for example: sys.stdout.buffer.write(lists_to_bytes(["hello ", ["cool", ["a", " ", "new"], "world!"], "!"]) + b'\n') displays this text: hello cool ! a new world! """ import sys import attr # fades attrs @attr.s class Nest: "A kind of box that acts as an hbox or vbox depending on context." kids = attr.ib() def boundaries(self, is_horizontal=True): "Return the (width, height) required in the given context." widths, heights = zip(*(kid.boundaries(not is_horizontal) for kid in self.kids)) if is_horizontal: return sum(widths), max(heights) else: return max(widths), sum(heights) def render(self, is_horizontal=True): "Generate the drawing instructions to draw the box in the given context." # XXX this is quadratic in tree height widths, heights = zip(*(kid.boundaries(not is_horizontal) for kid in self.kids)) if is_horizontal: x = [0] + cumsum(widths)[:-1] y = [0] * len(heights) else: x = [0] * len(widths) y = [0] + cumsum(heights)[:-1] for kid, xi, yi in zip(self.kids, x, y): for instruction in kid.render(not is_horizontal): yield translate(instruction, xi, yi) @attr.s class Text: "A kind of box containing just a bytestring." contents = attr.ib() def boundaries(self, *_): "Return the (width, height) required; height is always 1." return len(self.contents), 1 def render(self, *_): "Generate the single drawing instruction needed to draw the box." yield 0, 0, 'text', self.contents def cumsum_gen(xs): "Generator version of cumsum." total = 0 for x in xs: total += x yield total def cumsum(xs): "See numpy." return list(cumsum_gen(xs)) def translate(instruction, x, y): "Convert a drawing instruction into a new one translated by (x,y)." return (instruction[0] + x, instruction[1] + y) + instruction[2:] def from_lists(thing): "Convert a nested list structure to a box." return (Text(thing.encode('utf-8')) if type(thing) is str else Text(thing) if type(thing) is bytes else Nest(list(map(from_lists, thing)))) def text_render(instructions): "Render drawing instructions into a list of bytearrays, one per line." lines = [] for x, y, cmd, *args in instructions: if cmd == 'text': text, = args while len(lines) <= y: lines.append(bytearray()) line = lines[y] if len(line) < x: line.extend(b' ' * (x - len(line))) line[x:x+len(text)] = text return lines def lists_to_bytes(lists, is_horizontal=True): "Convert a nested list structure to a byte string." return b'\n'.join(text_render(from_lists(lists).render(is_horizontal))) if __name__ == '__main__': sys.stdout.buffer.write(lists_to_bytes(["hello ", ["cool", ["a", " ", "new"], "world!"], "!"]) + b'\n')