#!/usr/bin/python # -*- coding: utf-8 -*- """Render nested tables in fixed-width Unicode text or HTML. A table is a list of rows. A row is a list of cells. A cell is either a table; a 2-tuple (justification, cell), where `justification` is one of "<", ">", "^", or "." for left, right, center, or decimal justification, respectively; or a (unicode or byte) string s. Justification is inherited into nested tables. Since a table can be single-column, you can get multiple lines of text into a cell by writing them as a list of one-cell rows. "." decimal justification is not implemented yet. """ import cgi import collections def example(): "Spits out some ugly demo text on stdout." tbl = [[('^','Republic of Brazil'), [[u"São Paulo", ':', "Fernando Ferreira"], ["Piracicaba", ':', "aafgarci"]], "br"], [('^','China'), ('>', [["Beijing"], ["Guangzhou"], ["Hefei"]]), "cn"]] for line in render(tbl, column_separator=' '): print '|', line, '|' print print html_render(tbl) def render(rows, **params): """Converts a table into a sequence of lines of fixed-width text. Available parameters: * `justification`: sets default for contents * `column_separator`: a string to put between cells on the same line; defaults to " " """ if rows == []: return if 'justification' not in params: params['justification'] = '<' rows = [[flatten(cell, **params) for cell in row] for row in rows] widths = [max(max(len(line) for line in row[ii].lines) for row in rows) for ii in range(len(rows[0]))] sep = params.get('column_separator', u' ') for row in rows: for line in range(max(len(cell.lines) for cell in row)): yield sep.join(justify(cell.line(line), width, cell.justification) for cell, width in zip(row, widths)) class Cell(collections.namedtuple('Cell', ['justification', 'lines'])): "A uniform flat structure; `lines` is a list of Unicode strings." def line(self, n): if n < len(self.lines): return self.lines[n] else: return '' def flatten(cell, **params): "Reduce input cell to the uniform flat (justification, linelist) Cell." if isinstance(cell, tuple): justification, s = cell params['justification'] = justification return Cell(justification, flatten_contents(s, **params)) else: return flatten((params['justification'], cell), **params) def flatten_contents(cell_contents, **params): "Reduce input cell to a list of lines without justification." if isinstance(cell_contents, tuple): return flatten(cell_contents[1], **params)[1] elif isinstance(cell_contents, unicode): return [cell_contents] elif isinstance(cell_contents, basestring): return [unicode(cell_contents, 'UTF-8')] else: # Otherwise it must be a table. return list(render(cell_contents, **params)) def justify(text, width, direction): pad = ' ' * (width - len(text)) if direction == '<': return text + pad elif direction == '>': return pad + text elif direction == '^': return pad[:len(pad)//2] + text + pad[len(pad)//2:] elif direction == '.': # XXX wrong return pad + text else: raise UnjustifiableException(text, width, direction) class UnjustifiableException(Exception): pass def html_render(rows, **params): """Produces HTML with presentational style attributes. Available parameters: * `justification`: sets default for contents * `indent`: a whitespace indentation string for the left of each line (not visible in rendered HTML) To get HTML results similar to the plain text render() function, you may want to apply additional CSS to the resulting HTML, something like the following: td { vertical-align: top; padding: 0 0.25em } table { margin: 0; border-spacing: 0 } td table { width: 100% } """ return ''.join(_html_render(rows, **params)) def _html_render(rows, **params): yield '\n' % justification_attribute(params.get('justification')) indent = params.get('indent', '') for row in rows: yield indent yield ' ' yield ''.join(_htmlcell(cell, indent) for cell in row) yield '\n' yield indent yield '' def _htmlcell(cell, indent): justification = None # OK, so is it a table, a 2-tuple, or a string? # First, eliminate the 2-tuple case: while isinstance(cell, tuple): new_justification, cell = cell if justification is None: justification = new_justification # Then, eliminate the non-Unicode case: if isinstance(cell, basestring) and not isinstance(cell, unicode): cell = unicode(cell, 'UTF-8') # Then, distinguish between the base string case and a nested table: if isinstance(cell, unicode): return tdcell(justification) + cgi.escape(cell) else: contents = html_render(cell, indent=indent+' ') return indent + tdcell(justification) + contents def tdcell(justification): return '' % justification_attribute(justification) def justification_attribute(justification): if justification is None: return '' elif justification == '<': return ' style="text-align: left"' elif justification in ('>', '.'): return ' style="text-align: right"' elif justification == '^': return ' style="text-align: center"' else: raise UnjustifiableException(justification) __all__ = ['example', 'render', 'html_render'] if __name__ == '__main__': example()