"""Formatting Utilities This module formats structured text, including lines, spaces, text, word-wrapped text, rows, columns, and tables. There are also some higher-level constructs, like ``tabbed box''. The basic usage is to construct a format element and then call the fmt method on it, which converts the element into plain text (a list of strings). The constructors for format elements are all top-level functions in this module: hspace() spaces filling available area horizontally vspace() spaces filling available area vertically hline() horizontal line vline() vertical line text(t1,t2,...,tn) n lines of text paragraph(text) one line of text, word-wrapped if needed hfill(t1,...,tn) n lines of text repeated horizontally vfill(t1,...,tn) n lines of text repeated vertically table(r1,...,rn) n rows (each a list of glyphs) formatted as a table bordertable(...) like table, but with lines between rows and columns row(g1,...,gn) row of n elements col(g1,...,gn) column of n elements border(g) element g, but with a border around it center(g) element g modified to be centered tabbed_box(t,g) element g, inside a tabbed box with text t on the tab scroll(t1,t2,g) element g, inside a scroll with t1,t2 as header lines Once you create a format element (either a base element or a compound element composed from other format elements), call the .fmt() method on it to get a list of strings (one to be displayed per line). An optional parameter to .fmt() controls the maximum width; it is common to use 75 or 80 here. Since the result is a list of strings rather than a single string, you may want to join() them with newlines. Note: This code only works with strings of printable characters. It won't work for strings that contain control characters, such as tabs, newlines, or escape characters. """ # Text Formatting module # Mar 1997, Amit J Patel # Originally written for interactive text systems like POO and SRE import string; def _linesplit(text,width): """Return a list of strings, each no more than width chars long""" lines = [] while len(text) > width: cutoff1 = string.rfind(text[:width]," ") cutoff2 = string.find(text[:width],"\n") if cutoff2 > cutoff1: cutoff = cutoff2 elif cutoff1 < width*4/5: cutoff = width else: cutoff = cutoff1 lines.append(text[:cutoff]) text = " " + string.lstrip(text[cutoff:]) if width < 3: text = string.lstrip(text) lines.append(text) return lines def _listslice(list,p): """Return the pth element of each sequence in the list""" return map(lambda x,p=p: x[p],list) def _fixtable(rows): """Make sure the rows have the same length and contain Glyphs""" rows = map(None,rows) s = Space() numcols = max([0]+map(len,rows)) for row in rows: # Pad on the right with spaces while len(row) < numcols: row.append(Space()) # Make sure that it's a list of glyphs for g in row: # Make sure it's an object if type(g) != type(s): raise TypeError # Then see if it looks like a FormatElement (approximation) try: g.format except: raise TypeError return rows class FormatElement: """Base class for the 'glyphs' that can be formatted""" bignum = 1000 def __init__(self): self.hjustify = 'left' self.vjustify = 'top' def filler_space(self): """Return the kind of space with which we fill blank areas""" return ' ' def fmt(self,maxwidth=bignum): """Format this element to be at most maxwidth wide, and return a list of strings to be displayed""" w = self.widths() return self.format(min(w[1],maxwidth)) def hformat(self,text,width): """Pad the text according to the current horizontal justification""" fn = string.ljust if self.hjustify == 'center': fn = string.center if self.hjustify == 'right': fn = string.rjust return map(lambda x,fn=fn,width=width: fn(x,width),text) def vformat(self,text,width,height): """Pad the text according to the current vertical justification""" spc = self.filler_space()*width while len(text) < height: if self.vjustify != 'top': text.insert(0,spc) if self.vjustify != 'bottom': text.append(spc) return text[:height] class Space(FormatElement): """Glyph representing an area of spaces""" def __init__(self,char=' ',width=1): FormatElement.__init__(self) self.char = char self.width = width def widths(self): return (0,self.width,self.width) def format(self,width): return [self.char*width] class HFill(FormatElement): """Glyph representing a horizontal stretchy line""" def __init__(self,*lines): FormatElement.__init__(self) if not lines: lines = ('-',) for i in range(len(lines)): if not lines[i]: lines[i] = ' ' self.lines = lines def widths(self): return (0,max(map(len,self.lines)),FormatElement.bignum) def format(self,width): return map(lambda x,width=width: (x*width)[:width], self.lines) class VFill(FormatElement): """Glyph representing a vertical stretchy line""" def __init__(self,*lines): FormatElement.__init__(self) if not lines: lines = ('|',) for i in range(len(lines)): if not lines[i]: lines[i] = ' ' self.lines = lines def widths(self): ln = max(map(len,self.lines)) return (ln,ln,ln) def format(self,width): return map(lambda x,width=width: (x*width)[:width], self.lines) def vformat(self,text,width,height): while len(text) < height: text = text+text return text[:height] class Paragraph(FormatElement): """Glyph representing a single line of text wrapped into a paragraph""" def __init__(self,text): FormatElement.__init__(self) self.text = text def widths(self): words = ['']+string.split(self.text) lengths = map(len,words) return (min(lengths),len(self.text),len(self.text)) def format(self,width): return self.hformat(_linesplit(self.text,width),width) class Text(FormatElement): """Glyph representing a single line of text that is not wrapped""" def __init__(self,*text): FormatElement.__init__(self) if type(text) == type(''): self.text = [text] elif type(text) == type(()): self.text = map(None,text) else: raise TypeError def widths(self): m = max([0]+map(len,self.text)) return (m,m,m) def format(self,width): return self.hformat(self.text,width) class Table(FormatElement): """Compound Glyph composed of multiple rows of glyphs""" def __init__(self,*rows): FormatElement.__init__(self) self.rows = _fixtable(rows) self.numcols = max([0]+map(len,self.rows)) def widths(self): a = [0,0,0] for col in range(self.numcols): column = _listslice(self.rows,col) sizes = map(lambda x: x.widths(),column) for i in range(len(a)): a[i] = a[i] + max(_listslice(sizes,i)) return (a[0],a[1],a[2]) def calc_sizes(self,widthlist,totalwidth): # Slice out min, nat, max sizes mn = _listslice(widthlist,0) nt = _listslice(widthlist,1) mx = _listslice(widthlist,2) # Find the min, nat, max of the entire list def sum(x,y): return x+y natsum = reduce(sum,nt,0) minsum = reduce(sum,mn,0) maxsum = reduce(sum,mx,0) absmin = map(lambda x: 0,widthlist) absmax = map(lambda x: FormatElement.bignum,widthlist) # Determine in which range the widths lie # Low: absmin .. min # Normal: min .. nat or nat .. max # High: max .. absmax if totalwidth < minsum: a,b,aleft,bleft = absmin,mn,0,minsum elif totalwidth < natsum: a,b,aleft,bleft = mn,nt,minsum,natsum elif totalwidth < maxsum: a,b,aleft,bleft = nt,mx,natsum,maxsum else: a,b,aleft,bleft = mx,absmax,maxsum,\ FormatElement.bignum*len(widthlist) # Give some space to each element in the list count = totalwidth c = [] for i in range(len(widthlist)): # Calculate the size to give to element i ai,bi = a[i],b[i] diff = bleft-aleft ci = ai if diff > 0: ci = ai + ((bi-ai)*(count-aleft)+diff/2)/diff # Add the size to the size list c.append(ci) # Reduce the amount of space we have left to give aleft = aleft-ai bleft = bleft-bi count = count-ci # c now stores a list of widths return c def format(self,width): columnwidths = [] sz = map(lambda x: map(lambda y: y.widths(),x),self.rows) for i in range(self.numcols): temp = [] for j in range(3): temp.append( max([0]+_listslice(_listslice(sz,i),j)) ) columnwidths.append(temp) widths = self.calc_sizes(columnwidths,width) # Generate output output = [] for row in self.rows: texts = [] for i in range(self.numcols): texts.append(row[i].format(widths[i])) maxsize = max(map(len,texts)) for i in range(self.numcols): texts[i] = row[i].vformat(texts[i],widths[i],maxsize) for j in range(maxsize): output.append(string.join(_listslice(texts,j),'')) return output # These are the convenience functions that should be used; # they are documented as the module's interface def hspace(): return HFill(' ') def vspace(): return VFill(' ') def hline(): return HFill('-') def vline(): return VFill('|') def hfill(*x): return apply(HFill,x) def vfill(*x): return apply(VFill,x) def text(*x): return apply(Text,x) def paragraph(x): return Paragraph(x) def table(*x): return apply(Table,x) def row(*x): return Table(map(None,x)) def col(*x): return apply(Table,tuple(map(lambda y:[y],x))) def border(g): return Table([Space(','),hfill(),Space('.')], [vfill(),g,vfill()], [Space("`"),hfill(),Space("'")]) def center(g): g.hjustify = 'center' g.vjustify = 'center' return g # Although not as short as the other convenience functions, this too # is just for convenience. It creates a box with a tab on top. def tabbed_box(title,glyph): if type(title) != type(''): raise TypeError h1 = text(' ',' ') h2 = text('_____') h2.vjustify = 'bottom' h3 = hfill('_') h3.vjustify = 'bottom' header = row(h3,text(' '+'_'*len(title)+' ','/'+title+'\\'),h2) t = table([h1,header,h1], [vfill(),glyph,vfill()], [text("`"),hfill(),text("'")]) return t # This convenience function creates a scroll with text on top. def scroll(title1,title2,glyph): if type(title1) != type(''): raise TypeError if type(title2) != type(''): raise TypeError scrleft = text(' _____', ' / \\ ', '| ____)', '| \\__/_') scrright = text('_ ', ' \\ ', ' )', '_/ ') title = col(hfill('_'), center(text(title1)), center(text(title2)), hfill('_')) header = row(scrleft, title, scrright) footer = row(text(' \\'), hfill('_'), text('\\ ')) display = row(vfill('| '), glyph, vfill(' | ')) return col(header, display, footer) # This convenience function adds lines to a table def bordertable(*x): x = _fixtable(x) if not x: return border(Space()) rows = [] # put in vertical dividers for row in x: r = [vfill()] for y in row: r.append(y) r.append(vfill()) rows.append(r) # create a horizontal divider divider = [Space('|')] for r in x[0]: divider.append(hfill()) divider.append(Space('+')) divider[-1] = Space('|') top = [Space(',')]+divider[1:-1]+[Space('.')] bottom = [Space("`")]+divider[1:-1]+[Space("'")] for i in range(2,len(divider)-2,2): top[i] = Space(':') bottom[i] = Space("'") # Add dividers, and convert into a tuple result = [top] for r in rows: result.append( r ) result.append( divider ) result[-1] = bottom rows = tuple( result ) return apply(table,rows) def test(): s = 'The blue moon is made of green spam. Really.' g1 = paragraph(s) g1.vjustify = 'center' g2 = paragraph(s) g2.hjustify = 'right' t = bordertable([center(text('hi')), g1], [g2, g1]) g = scroll('Title','',row(paragraph('This is a sample scroll'), t, paragraph('with a bordered table'))) print string.join(g.fmt(78),"\n") if __name__ == '__main__': test()