#!/usr/bin/python3 # -*- coding: utf-8 -*- """Algebraic layout, in this case with ASCII text. Here is a layout is specified in five lines of code, given Python lists called “titles”, “pnos”, and “chapno”: vr, hr, dhr = [~String(s) for s in '|-='] left = (('Chapters' | String('')) - hr - (chapno | vr | [t | ~String(' . ') for t in titles])) pages = 'Page' - hr - pnos table = ~('*' - hr - '*' - ' ') | ' ' | dhr - (vr | left | pages | vr) - dhr Here "-" stacks boxes on top of each other, "|" stacks them left to right, and "~" produces repetition. Python lists have their items converted to boxes and then stacked on top of each other. This code produces this layout: * ======================================================================== - |Chapters Page| * |----------------------------------------------------------------------| | 0.|The unchained desert of the singing perfume. . . . . . . 1| * | 1.|La brisa ilusa de la neblina abandonada. . . . . . . . 43| - | 2.|The smiling murder within her young daughters. . . . . . 80| * | 3.|Aquella hija tortuosa de su precipicio árido. . . . . . . 96| | 4.|False villages of the foolish monks. . . . . . . . . . 114| * | 5.|El roble sacrílego de mi sacerdote desmoronado y enlutado. . 124| - | 6.|The brazen river of his filthy, fallen ivory. . . . . . . 166| * | 7.|The laughing monster of his humble scream. . . . . . . . 203| | 8.|The rough tattoo of his unchained abomination. . . . . . 219| * | 9.|Los perfumes silenciosos de un árbol monstruoso muriéndose. . 237| - |10.|The sweet tower of the smooth fire. . . . . . . . . . 247| * ======================================================================== This uses one primitive box type, String, combined using Hbox, Vbox, and Repeat box operators to produce the above layout. These types are defined in 65 lines of Python. For maximal efficiency, the text block is scanned out row by row, traversing only the boxes on a given line of text, as in scanline rendering; while the size requirements propagate bottom-up when the tree is constructed. This is pretty cool as far as it goes, permitting even apparently spanned cells in this example, but it has a number of limitations in this form: - It doesn’t *really* do table layout. The table above is really a horizontal stackup of independent columns; the cells in one column are totally free to have different heights from the corresponding cells in the next column, making the table all cattywompus. It happens that in this case the cells are all exactly one line of text high, so they happen to line up. Doing real table layout with rowspans and colspans will require slightly more sophisticated constraint solving, even without word wrap. Maybe not much, though. - There’s no official interface to set a minimum or maximum width or height or padding or alignment on anything. Strings are right-aligned, but Hboxes give any space beyond the space requested to their right halves, so you can left-align a string by putting another string to its right, even an empty one. And you can add horizontal padding explicitly as strings with ' ' | or vertical padding with - '' - ''. And it actually should work to say foo.width = 37, too, at least before you put foo into other things. - Repeat boxes, used for the dot leaders in the example, for the left-margin decorations, and for the horizontal and vertical rules, repeat their contents in both X and Y. Since they are in some sense just fillers they shouldn’t request any space of their own — except that in this case we really do want the vertical rules to request a space of horizontal space, and we want the horizontal rules to request a line of vertical space. So the easy hack was to have them just request the size of their contents, usually 1×1. - When the amount of space allocated is other than the amount requested, either truncation or filling is required. At present this always falls on the last leafnodes: the rightmost columns and the bottommost rows. This works okay with the dot leaders, and provides customizable justification with the right-justification behavior of text nodes, but filling with the right and bottom borders is pretty undesirable. And truncating the whole table at the right is also pretty undesirable; we’d like to be able to specify which fields get truncated or wrapped or whatever. - What would this look like if it were capable of PDF layout or ZUI interaction? I don’t think the scanline approach would work. How would page breaks or column breaks work? A fun thing to think about here is what this would look like not as an algebra of boxes but as a tag-soup stream of text with formatting codes in it, such as TAB and LF. This is especially appealing for interactive editing of documents. There’s a reimplementation of this in alglayout.ml. """ import sys def as_layout(o): return (o if isinstance(o, Box) else stack(o) if isinstance(o, list) else String(str(o))) class Box: def __sub__(self, other): return Vbox(self, as_layout(other)) def __or__(self, other): return Hbox(self, as_layout(other)) def __rsub__(self, other): return as_layout(other) - self def __ror__(self, other): return as_layout(other) | self def __invert__(self): return Repeat(self) class String(Box): def __init__(self, s): self.s = s self.width = len(s) height = 1 def scan(self, y, w): w = max(0, w) yield self.s.rjust(w)[:w] if y == 0 else ' ' * w class Vbox(Box): def __init__(self, top, bottom): self.top = top self.bottom = bottom self.off = self.top.height self.width = max(self.top.width, self.bottom.width) self.height = self.off + self.bottom.height def scan(self, y, w): return (self.top.scan(y, w) if y < self.off else self.bottom.scan(y - self.off, w)) class Hbox(Box): def __init__(self, left, right): self.left = left self.right = right self.off = self.left.width self.width = self.off + self.right.width self.height = max(self.left.height, self.right.height) def scan(self, y, w): for s in self.left.scan(y, min(w, self.off)): yield s for s in self.right.scan(y, w - self.off): yield s class Repeat(Box): def __init__(self, contents): self.contents = contents self.width = self.contents.width self.height = self.contents.height def scan(self, y, w): y %= self.height for x in range(0, w, self.contents.width or 1): for s in self.contents.scan(y, min(self.contents.width, w - x)): yield s def stack(layouts, combiner=Vbox): result = as_layout(layouts[0]) for layout in layouts[1:]: result = combiner(result, as_layout(layout)) return result def draw(stream, layout, min_y=0, max_y=None, w=None): for y in range(min_y, max_y if max_y is not None else layout.height): stream.writelines(layout.scan(y, w if w is not None else layout.width)) stream.writelines('\n') def demo(): titles = ['The unchained desert of the singing perfume.', 'La brisa ilusa de la neblina abandonada.', 'The smiling murder within her young daughters.', 'Aquella hija tortuosa de su precipicio árido.', 'False villages of the foolish monks.', 'El roble sacrílego de mi sacerdote desmoronado y enlutado.', 'The brazen river of his filthy, fallen ivory.', 'The laughing monster of his humble scream.', 'The rough tattoo of his unchained abomination.', 'Los perfumes silenciosos de un árbol monstruoso muriéndose.', 'The sweet tower of the smooth fire.', ] chapno = ['%d.' % i for i in range(len(titles))] pnos = [1] while len(pnos) < len(titles): pnos.append(pnos[-1] + 6 + pnos[-1] * 77 % 41) vr, hr, dhr = [~String(s) for s in '|-='] left = (('Chapters' | String('')) - hr - (chapno | vr | [t | ~String(' . ') for t in titles])) pages = 'Page' - hr - pnos table = ~('*' - hr - '*' - ' ') | ' ' | dhr - (vr | left | pages | vr) - dhr draw(sys.stdout, table) if __name__ == '__main__': demo()