Viewing file: Text.py (31.13 KB) -rw-r--r-- Select action/file-type: (+) | (+) | (+) | Code (+) | Session (+) | (+) | SDB (+) | (+) | (+) | (+) | (+) | (+) |
# Convert a node representation to text # in some different forms
# o output to an object with an interface subset of Tk Text # o output to ascii-only - best attempt w.o. different fonts # o output to man-page text
# Parameterized on out
# I have to define the roles of responsibility.
# out is a thin layer above something Tk text-like # it isolates some details but doesn't provide real new functionality
# Node2Inter takes care of the general formatting # It needs to know about # o the size of out, i.e. width and height # o the fonts and sizes available # o the special characters available # o the size of the fonts # o we restrict to fixed size fonts # otherwise it would have to ask for size of strings? #
# to be able to do # o line wrapping # o paragraphs # o headers # o item and bullet lists # o tables
# The out can 'record' its commands # and play them on an actual Tk text window. # with a simple translation # o it can operate stack-free # -- so after eg a font change, there is a font change back
# Does the out has a configuration table # such as, # o the fonts to choose # o for different sizes # o whether to make items bold
# In any case there may be default here..
class Node2Inter: def __init__(self, mod, node, out, cnf, width=None): self.mod = mod self.out = out self.cnf = cnf self.width = width
self.span_stack = [] self.attrs_stack = [{}] self.atparbegin = 1 self.inpre = 0 self.ul_level = 0 self.prev_margin_bottom = 0
self.sizescale = cnf.sizescale
if node is not None: self._visit_outer_node(node)
def _visit_outer_node(self, node): cnf = self.cnf self.span_begin(font_family=cnf.textfamily, font_size=cnf.sizeindex) node, attrs = node.split_attrs() for k, v in attrs: k = k.strip() v = v.strip() setattr(self.out, '_gsl_%s' % k, v) node.accept(self) self.span_end()
def _visit_node(self): node, attrs = node.split_attrs() if attrs: self.attrs_stack
def append(self, x): self.out.insert('end', x, self.tags)
def div(self, node, margin_bottom=0, **kwds): self.div_begin(**kwds) node.arg_accept(self) self.div_end(margin_bottom)
def div_begin(self, margin_top=0, **kwds): if self.span_stack: d = self.span_stack[-1] if 'spacing1' in d: if self.atparbegin: margin_top = max(margin_top, d['spacing1']) self.tag_config(self.tag, **d) del d['spacing1']
margin_top = max(margin_top, self.prev_margin_bottom) self.prev_margin_bottom = 0
if not self.atparbegin: self.nl() self.atparbegin = 1
if margin_top: kwds['spacing1'] = margin_top
self.span_begin(**kwds)
def div_end(self, margin_bottom=0): if not self.atparbegin: self.nl() self.span_end() self.atparbegin = 1 self.prev_margin_bottom = margin_bottom
def getopt(self, name, default=0): if self.span_stack and name in self.span_stack[-1]: return self.span_stack[-1][name] else: return getattr(self, name, default)
def nl(self): self.append('\n')
def set_default_tag(self): if self.span_stack: tag = 't%s' % self.mod._root.pickle.dumps(self.span_stack[-1]) else: tag = 'tag' self.tag = tag self.tags = (tag,)
def span(self, node, **kwds): self.span_begin(**kwds) node.arg_accept(self) self.span_end()
def span_begin(self, **kwds): if self.span_stack: d = self.span_stack[-1].copy() d.update(kwds) else: d = kwds self.span_stack.append(d)
self.set_default_tag()
def span_end(self): tag = self.tag self.tag_config(tag, **self.span_stack[-1]) self.span_stack.pop() self.set_default_tag()
def tag_config(self, tag, **kwds): okwds = {} fontspecs = [] for k, v in list(kwds.items()): if not k.startswith('font_'): okwds[k] = v continue else: fontspecs.append((k[5:], v)) if fontspecs: font = [None, None, ''] for k, v in fontspecs: if k == 'size': v = max(0, min(len(self.sizescale)-1, v)) font[1] = self.sizescale[v] elif k == 'family': font[0] = v else: if font[2]: font[2] += ' ' font[2] += k if not font[2]: font.pop() okwds['font'] = tuple(font)
self.out.tag_config(tag, **okwds)
def text(self, text): if not self.inpre: if self.atparbegin: text = text.lstrip() if not text: return
text = text.replace('\n', ' ') text = text.replace('\t', ' ') while ' ' in text: text = text.replace(' ', ' ')
if self.atparbegin and self.prev_margin_bottom: self.tag_config(self.tag, **self.span_stack[-1]) self.span_stack[-1]['spacing1'] = self.prev_margin_bottom self.set_default_tag() self.prev_margin_bottom = 0
self.append(text) else: text = text.expandtabs() idx = text.find('\n') if idx != -1 and 'spacing1' in self.span_stack[-1]: self.append(text[:idx+1]) self.tag_config(self.tag, **self.span_stack[-1]) del self.span_stack[-1]['spacing1'] self.set_default_tag() text = text[idx+1:] if text: self.append(text)
self.atparbegin = 0
def _visit_children(self, node): E = self.mod.ReportedError for ch in node.children: try: ch.accept(self) except E: pass
def _visit_hx(self, node): n = int(node.tag[1:]) font_size = 7 - n margin_top = 12 - 1 * n margin_bottom = 12 - 1 * n
self.div(node, font_size=font_size, font_bold=1, margin_top=margin_top, margin_bottom=margin_bottom)
def visit_big(self, node): self.span(node, font_size=self.getopt('font_size') + 1)
def visit_blockquote(self, node): lmargin = self.getopt('lmargin1') + 36 rmargin = self.getopt('rmargin') + 36 self.div(node, lmargin1=lmargin, lmargin2=lmargin, rmargin=rmargin, margin_top=6, margin_bottom=6, )
def visit_char(self, node): code = node.arg.strip() if code == 'nbsp': self.span_begin(invisible=1) self.append('x') self.span_end() else: self.error( 'I do not know how to render this character code: %r.' % code, node)
def visit_code(self, node): self.span(node, font_family=self.cnf.codefamily)
def visit_comment(self, node): pass
def visit_dl(self, node): self.div(node)
def visit_dt(self, node): self.div(node)
def visit_dd(self, node): lmargin = self.getopt('lmargin1') + 36 self.div(node, lmargin1=lmargin, lmargin2=lmargin)
def visit_define(self, node): # xxx self._visit_children(node)
def visit_div(self, node): self.div(node)
def visit_document(self, node): self._visit_children(node)
def visit_document_lang(self, node): if self.document_lang is not None: self.error('Duplicate document lang directive.', node) self.document_lang = node
def visit_document_title(self, node): if self.document_title is not None: self.error('Duplicate document title directive.', node) self.document_title = node
def visit_em(self, node): self.span(node, font_italic=1)
def visit_file(self, node): self._visit_children(node)
def visit_gsl_title(self, node): self.out._gsl_title = node.arg
def visit_gsl_width(self, node): self.out._gsl_width = int(node.arg)
def visit_gsl_height(self, node): self.out._gsl_height = int(node.arg)
def visit_h1(self, node): self._visit_hx(node)
def visit_h2(self, node): self._visit_hx(node)
def visit_h3(self, node): self._visit_hx(node)
def visit_h4(self, node): self._visit_hx(node)
def visit_h5(self, node): self._visit_hx(node)
def visit_h6(self, node): self._visit_hx(node)
def visit_li(self, node): indent = self.getopt('lmargin1') + 18 self.div_begin( lmargin1=indent, lmargin2=indent )
mode = ['disc', 'square', 'circle'][self.ul_level % 3]
char = {'disc': '*', 'circle': 'O', 'square': '[]'}[mode]
self.span_begin() self.text('%s ' % char) self.span_end() self.span_begin( lmargin1=indent, lmargin2=indent+12 ) node.arg_accept(self) self.span_end() self.div_end()
def visit_p(self, node): self.div(node, margin_top=6, margin_bottom=6)
def visit_pre(self, node): self.inpre += 1 self.div(node, font_family=self.cnf.codefamily, margin_top=6, margin_bottom=6) self.inpre -= 1
def visit_small(self, node): self.span(node, font_size=self.getopt('font_size') - 1)
def visit_span(self, node): self.span(node)
def visit_string(self, node): self._visit_children(node)
def visit_strong(self, node): self.span(node, font_bold=1)
def visit_sub(self, node): self.span(node, font_size=self.getopt('font_size') - 1, offset=self.getopt('offset') - 2 )
def visit_sup(self, node): self.span(node, font_size=self.getopt('font_size') - 1, offset=self.getopt('offset') + 2 )
def visit_table(self, node): Table(self, node) pass
def visit_td(self, node): pass
def visit_th(self, node): pass
def visit_tr(self, node): pass
def visit_text(self, node): self.text(node.arg) self._visit_children(node)
def visit_u(self, node): self.span(node, underline=1)
def visit_ul(self, node): self.ul_level += 1 self.div(node) self.ul_level -= 1
def visit_var(self, node): self.span(node, font_italic=1)
class SimulText: def __init__(self, mod, width=None): self.mod = mod self.width = width
self.lines = [[]] self.tags = {} self.textntags = [] self.fonts = {}
def insert(self, pos, text, tags): assert pos == 'end' lines = text.split('\n') self.lines[-1].append((lines[0], tags)) for line in lines[1:]: self.lines.append([(line, tags)]) self.textntags.append((text, tags))
def tag_config(self, tag, **kwds): if tag in self.tags and kwds == self.tags[tag]: return self.tags[tag] = kwds
##
def finalize(self): if len(self.lines[-1]) == 1 and not self.lines[-1][0][0]: self.lines.pop() if self.width is not None: self.wrap_lines()
def get_width(self): width = 0 for line in self.lines: w = self.text_width(line) if w > width: width = w return width
def replay(self, out, lineidx): if lineidx >= len(self.lines): return line = self.lines[lineidx] for (ch, tags) in line: out.insert('end', ch, tags) for tag in tags: out.tag_config(tag, **self.tags[tag])
def split_word(self, line): words = [[]] for text, tags in line: wtext = text.split(' ') for wt in wtext: if wt: words[-1].append((wt, tags)) if words[-1]: words.append([]) return words
def text_width(self, textntags): font = None subline = None subfonts = [] for ch, tags in textntags: for tag in tags: if tag in self.tags and 'font' in self.tags[tag]: newfont = self.tags[tag]['font'] break else: assert 0
if newfont != font: if subline: subfonts.append((subline, font)) font = newfont subline = [] subline.append(ch) if subline: subfonts.append((subline, font)) width = 0 for (subline, font) in subfonts: f = self.mod.makefont(font) m = f.measure(''.join(subline)) width += m return width
def width_to(self, char): # distance from left margin to first occurence of char # or the width of longest line, if char not found for line in self.lines: w = 0 found = 0 for (text, tags) in line: if char in text: text = text[:text.index(char)] found = 1 w += self.text_width([(text, tags)]) if found: break if found: break if not found: w = self.get_width() return w
def wrap_line(self, line): w = self.text_width(line) if w <= self.width: self.lines.append(line) return
words = self.split_word(line)
i = 0 while i < len(words): pre = list(words[i]) w = self.text_width(pre) while w > self.width: # The word is too long to fit. # I have to cut it off. # xxx this may be somewhat slow # measuring after every character j = 0 # Position j at the chunk that is going to be split while j + 1 < len(pre): w = self.text_width(pre[:j+1]) if w > self.width: break j += 1
# Split this chunk # Allow at least one character k = 2 while k <= len(pre[j][0]): w = self.text_width( pre[:j-1] + [(pre[j][0][:k], pre[j][1])]) if w > self.width: break k += 1 self.lines.append(pre[:j-1] + [(pre[j][0][:k-1], pre[j][1])]) assert self.text_width(self.lines[-1]) <= self.width pre = [(pre[j][0][k-1:], pre[j][1])] w = self.text_width(pre)
i += 1 while i < len(words): space = [(' ', pre[-1][1])] word = words[i] w = self.text_width(pre + space + word) if w > self.width: break else: pre.extend(space + word) i += 1 self.lines.append(pre)
def wrap_lines(self): lines = self.lines self.lines = [] for line in lines: self.wrap_line(line)
class TableCell: def __init__(self, row, node): self.row = row self.table = row.table self.parent = self.table.parent self.cnf = self.parent.cnf self.mod = self.parent.mod
self.attrs = {} self.node = self.set_attributes(node)
self.gen_out()
def align(self, pos, width): align = self.attrs['align'] if align == 'center': self.tabstop = (pos + 0.5*width, 'center') elif align == 'left': self.tabstop = (pos, 'left') elif align == 'right': self.tabstop = (pos+width, 'right') elif align == 'char': w = self.out.width_to(self.attrs['char']) co = float(self.attrs['charoff'].rstrip('%'))/100.0 self.tabstop = (pos + co*width-w, 'left') elif align == 'justify': # XXX I don't know how this works self.tabstop = (pos + 0.5*width, 'center') else: raise ValueError('Invalid align: %s' % align)
def get_edges(self, width): align = self.attrs['align'] mywidth = self.width if align == 'center': l, r = 0.5 * width - 0.5 * mywidth, 0.5 * width + 0.5 * mywidth elif align == 'left': l, r = 0, mywidth elif align == 'right': l, r = width - mywidth, width elif align == 'char': w = self.out.width_to(self.attrs['char']) co = float(self.attrs['charoff'].rstrip('%'))/100.0 l = co * width - w r = l + mywidth elif align == 'justify': # XXX I don't know how this works l, r = 0, width else: raise ValueError('Invalid align: %s' % align) return l, r
def get_width(self): self.width = self.out.get_width() self.numlines = len(self.out.lines) return self.width
def set_attributes(self, node): a = self.attrs if node.tag == 'th': align = 'center' else: align = 'left' a['align'] = align a['char'] = self.cnf.decimal_point a['charoff'] = '50%'
node, attrs = node.split_attrs() for k, v in attrs: a[k] = v
return node
def gen_out(self, width=None):
self.out = SimulText(self.mod, width=width)
n2i = Node2Inter(self.mod, None, self.out, self.cnf, width=width)
kwds = self.parent.span_stack[-1].copy() if self.node.tag == 'th': kwds['font_bold'] = 1
n2i.span_begin(**kwds) self.node.arg_accept(n2i) n2i.span_end() self.out.finalize()
self.get_width()
def wrap_to_width(self, width): if width >= self.width: return self.gen_out(width)
class TableRow: def __init__(self, table, node): self.table = table self.node = node self.numlines = 1
self.cells = []
node, attrs = node.split_attrs() self.attrs = attrs node.children_accept(self)
def new_cell(self, node): cell = TableCell(self, node) self.cells.append(cell)
def visit_td(self, node): self.new_cell(node)
def visit_th(self, node): self.new_cell(node)
class Table: def __init__(self, parent, node): self.parent = parent self.node = node
self.caption = None self.rows = []
parent.div_begin(margin_top=6)
self.lmargin = parent.getopt('lmargin1')
node.children_accept(self)
Width = 400 w = self.columnify() widths = self.widths spacings = self.spacings
if w > Width: # Which one to wrap? # The longest? # All? gw = [Width / len(self.widths)]*len(self.widths) extra = 0 others = list(range(len(self.widths))) for i, w in enumerate(self.widths): if w < gw[i]: extra += gw[i] - w gw[i] = w others.remove(i) extra = int(extra / len(others)) for i in others: gw[i] += extra
widths = self.widths = gw for row in self.rows: col = 0 for cell in row.cells: cell.wrap_to_width(gw[col]) col += 1
for row in self.rows: col = 0 pos = 0 for cell in row.cells: w = widths[col] cell.align(pos+self.lmargin, w) pos += w + spacings[col] col += 1 row.numlines = max(row.numlines, cell.numlines)
for row in self.rows: for i in range(row.numlines): tabstops = [] for cell in row.cells: tabstops.extend(cell.tabstop) tabstops = tuple(tabstops) if i == 0 and row is self.rows[0]: tabkwds = row.cells[0].out.tags[row.cells[0].out.lines[0][0][1][0]] else: tabkwds = {} if row is not self.rows[0] and i == 0: tabkwds['spacing1'] = 6 tabtag = str(tabstops)+str(tabkwds) for cell in row.cells: parent.out.insert('end', '\t', (tabtag,)) cell.out.replay(parent.out, i) parent.out.tag_config(tabtag, tabs=tabstops, **tabkwds) parent.nl() parent.div_end()
def columnify(self): # Make the cells aligned in columns
widths = self.widths = []
for row in self.rows: col = 0 for cell in row.cells: w = cell.get_width() if col >= len(widths): widths.append(w) else: widths[col] = max(w, widths[col]) row.numlines = max(row.numlines, cell.numlines)
col += 1
# Extra spacing after column i spacings = self.spacings = [0] * len(widths)
MINSPACING = 10
for row in self.rows: col = 0 for cell in row.cells[:-1]: rcell = row.cells[col+1] ledge = cell.get_edges(widths[col])[1] redge = rcell.get_edges(widths[col+1])[0]+widths[col] spacing = MINSPACING - (redge - ledge) spacings[col] = max(spacing, spacings[col]) col += 1
width = 0 for row in self.rows: col = 0 pos = 0 for cell in row.cells: w = widths[col] cell.align(pos+self.lmargin, w) pos += w + spacings[col] col += 1
if pos > width: width = pos
self.width = width return width
def visit_tfoot(self, node): node.children_accept(self)
def visit_thead(self, node): node.children_accept(self)
def visit_tr(self, node): row = TableRow(self, node) self.rows.append(row)
class RecordingInter: FLATTEXT = 1 FLATKWDS = 0 lasttext = () lasttag = None def __init__(self):
self.appends = [] self.tag_configs = {} self.lasttext = [] self.clearmemo()
def __str__(self): return 'APPENDS: %s TAG_CONFIGS: %s' % (self.appends, self.tag_configs)
def clearmemo(self): self.memo = {} # Maps any value to it self self.tagmemo = {} # Maps tag to integer tag number
def flush(self): if self.lasttext: tag = self.tagmemo.setdefault(self.lasttag, len(self.tagmemo)) text = ''.join(self.lasttext) text = self.memo.setdefault(text, text) if self.FLATTEXT: self.appends.append(tag) self.appends.append(text) else: tt = tag, text tt = self.memo.setdefault(tt, tt) self.appends.append(tt) self.lasttext = []
def insert(self, pos, text, tags): assert pos == 'end' assert len(tags) == 1
tag = tags[0] if tag != self.lasttag: self.flush() self.lasttag = tag self.lasttext.append(text)
def play(self, out): self.flush() if self.FLATTEXT: i = 0 while i < len(self.appends): tag = self.appends[i] text = self.appends[i+1] out.insert('end', text, (tag,)) i += 2 else: for tag, text in self.appends: out.insert('end', text, (tag,)) for (tag, kwdlist) in list(self.tag_configs.items()):
if self.FLATKWDS: kwds = {} i = 0 while i < len(kwdlist): kwds[kwdlist[i]] = kwdlist[i+1] i += 2 out.tag_config(tag, **kwds) else: out.tag_config(tag, **dict(kwdlist))
for k in self.__dict__: if k.startswith('_gsl_'): setattr(out, k, getattr(self, k))
def prepare_for_pickle(self): # Call this before pickling to reduce space usage. self.flush() for k in list(self.__dict__.keys()): if k not in ('appends', 'tag_configs') and not k.startswith('_gsl_'): delattr(self, k)
def tag_config(self, tag, **kwds): kwdlist = [] for k, v in list(kwds.items()): k = self.memo.setdefault(k, k) v = self.memo.setdefault(v, v) if self.FLATKWDS: kwdlist.append(k) kwdlist.append(v) else: kv = k, v kv = self.memo.setdefault(kv, kv) kwdlist.append(kv) kwdlist = tuple(kwdlist) kwdlist = self.memo.setdefault(kwdlist, kwdlist) tag = self.tagmemo.setdefault(tag, len(self.tagmemo))
if tag in self.tag_configs: assert self.tag_configs[tag] == kwdlist else: self.tag_configs[tag] = kwdlist
class TextInter:
def __init__(self, mod, wid): self.mod = mod self.wid = wid
for name in ( 'config', 'insert', 'tag_delete', ): setattr(self, name, getattr(wid, name))
def tag_config(self, tag, **kwds): if 'invisible' in kwds: del kwds['invisible'] kwds['foreground'] = kwds['background'] = kwds.get( 'background', self.wid['background'])
self.wid.tag_config(tag, **kwds)
class TkConfig: sizeindex = 3 sizescale = (6, 8, 10, 12, 16, 20, 24, 28) textfamily = 'times' codefamily = 'courier' decimal_point = '.' # default CHAR attribute
class _GLUECLAMP_: _imports_ = ( '_parent:SpecNodes', '_parent.SpecNodes:node_of_taci', '_parent.SpecNodes:node_of_string', '_parent.Main:ReportedError', '_parent:Html', '_root:pickle', '_root.md5:md5', '_root:os', '_root:re', '_root:string', '_root:tkinter', )
def _get_makefont(self): fonts = {} root = self.tkinter.Tk() root.withdraw()
def makefont(font):
if font in fonts: return fonts[font] weight = 'normal' slant = 'roman' if len(font) > 2: if 'bold' in font[2]: weight = 'bold' if 'italic' in font[2]: slant = 'italic' f = self.tkinter.font.Font(family=font[0], size=font[1], weight=weight, slant=slant) fonts[font] = f return f return makefont
def _get_tkconfig(self): return TkConfig()
def node2inter(self, node, inter, tkconfig=None): if tkconfig is None: tkconfig = self.tkconfig Node2Inter(self, node, inter, tkconfig)
def gsltextviewer(self, parent=None, filename=None, text=None, node=None, htmloutfile=None, inpickle=0, inrecorder=0, outrecorder=0 ):
# It seems they dont want we mix data and py files in the dist sigh # so these are last minute hacks
pickle = self.pickle
if inpickle: inrecorder = pickle.loads(inpickle)
if node is None: if text is None: if filename is not None: with open(filename) as f: text = f.read() node = self.node_of_string(text, nostrip=1)
if htmloutfile is not None: self.Html.node2file(node, htmloutfile)
if outrecorder: r = RecordingInter() self.node2inter(node, r) r.prepare_for_pickle() return r
cache = None if filename is not None: sp = self.os.path.splitext(filename) if sp[1] == '.gsl': cache = sp[0] + '.gsc'
m = self._root.guppy.etc.textView.TextViewer( parent, 'Untitled', data='') v = m.textView v['state'] = 'normal' v['font'] = 'Times -12' v.bind('<Destroy>', lambda event: m.quit())
if cache or inrecorder: if inrecorder: r = inrecorder else: r = None textdigest = self.md5(text.encode('utf-8')).digest() try: f = open(cache) except IOError: pass else: td = f.read(len(textdigest)) if td == textdigest: r = pickle.load(f) f.close() if r is None: r = RecordingInter() self.node2inter(node, r) r.prepare_for_pickle() f = open(cache, 'w') try: try: f.write(textdigest) except IOError: pass # maybe write protected just ignore for now XXX else: pickle.dump(r, f, 0) finally: f.close() r.play(v)
else: self.node2inter(node, v)
title = getattr(v, '_gsl_title', None) if title: m.title(title) m.iconname(title)
geometry = getattr(v, '_gsl_tk_geometry', None) if geometry: m.geometry(geometry)
v['state'] = 'disabled' return m
def test_string(s=None, name=None): from guppy import Root gsl = Root().guppy.gsl me = gsl.Text if s is None: s = getattr(me._parent.test.testdata, name)
T = me.tkinter node = me.node_of_string(s, nostrip=1)
me._parent.Html.node2file(node, '/tmp/x.html')
t = RecordingInter() me.node2inter(node, t)
t.prepare_for_pickle()
root = T.Tk() root.withdraw() text = me._root.guppy.etc.textView.TextViewer( root, 'test', data='').textView text['state'] = 'normal' text['font'] = 'Times -12' text.bind('<Destroy>', lambda event: root.quit()) ti = TextInter(me, text)
t.play(ti)
text.mainloop()
def test(): name = 'long_wrapping_tables' name = 'html_tables' test_string(name=name)
|