""" reStructuredText generation tools provides an api to build a tree from nodes, which can be converted to ReStructuredText on demand note that not all of ReST is supported, a usable subset is offered, but certain features aren't supported, and also certain details (like how links are generated, or how escaping is done) can not be controlled """ from __future__ import generators import py def escape(txt): """escape ReST markup""" if not isinstance(txt, str) and not isinstance(txt, unicode): txt = str(txt) # XXX this takes a very naive approach to escaping, but it seems to be # sufficient... for c in '\\*`|:_': txt = txt.replace(c, '\\%s' % (c,)) return txt class RestError(Exception): """ raised on containment errors (wrong parent) """ class AbstractMetaclass(type): def __new__(cls, *args): obj = super(AbstractMetaclass, cls).__new__(cls, *args) parent_cls = obj.parentclass if parent_cls is None: return obj if not isinstance(parent_cls, list): class_list = [parent_cls] else: class_list = parent_cls if obj.allow_nesting: class_list.append(obj) for _class in class_list: if not _class.allowed_child: _class.allowed_child = {obj:True} else: _class.allowed_child[obj] = True return obj class AbstractNode(object): """ Base class implementing rest generation """ sep = '' __metaclass__ = AbstractMetaclass parentclass = None # this exists to allow parent to know what # children can exist allow_nesting = False allowed_child = {} defaults = {} _reg_whitespace = py.std.re.compile('\s+') def __init__(self, *args, **kwargs): self.parent = None self.children = [] for child in args: self._add(child) for arg in kwargs: setattr(self, arg, kwargs[arg]) def join(self, *children): """ add child nodes returns a reference to self """ for child in children: self._add(child) return self def add(self, child): """ adds a child node returns a reference to the child """ self._add(child) return child def _add(self, child): if child.__class__ not in self.allowed_child: raise RestError("%r cannot be child of %r" % \ (child.__class__, self.__class__)) self.children.append(child) child.parent = self def __getitem__(self, item): return self.children[item] def __setitem__(self, item, value): self.children[item] = value def text(self): """ return a ReST string representation of the node """ return self.sep.join([child.text() for child in self.children]) def wordlist(self): """ return a list of ReST strings for this node and its children """ return [self.text()] class Rest(AbstractNode): """ Root node of a document """ sep = "\n\n" def __init__(self, *args, **kwargs): AbstractNode.__init__(self, *args, **kwargs) self.links = {} def render_links(self, check=False): """render the link attachments of the document""" assert not check, "Link checking not implemented" if not self.links: return "" link_texts = [] # XXX this could check for duplicates and remove them... for link, target in self.links.iteritems(): link_texts.append(".. _`%s`: %s" % (escape(link), target)) return "\n" + "\n".join(link_texts) + "\n\n" def text(self): outcome = [] if (isinstance(self.children[0], Transition) or isinstance(self.children[-1], Transition)): raise ValueError, ('document must not begin or end with a ' 'transition') for child in self.children: outcome.append(child.text()) # always a trailing newline text = self.sep.join([i for i in outcome if i]) + "\n" return text + self.render_links() class Transition(AbstractNode): """ a horizontal line """ parentclass = Rest def __init__(self, char='-', width=80, *args, **kwargs): self.char = char self.width = width super(Transition, self).__init__(*args, **kwargs) def text(self): return (self.width - 1) * self.char class Paragraph(AbstractNode): """ simple paragraph """ parentclass = Rest sep = " " indent = "" width = 80 def __init__(self, *args, **kwargs): # make shortcut args = list(args) for num, arg in py.builtin.enumerate(args): if isinstance(arg, str): args[num] = Text(arg) super(Paragraph, self).__init__(*args, **kwargs) def text(self): texts = [] for child in self.children: texts += child.wordlist() buf = [] outcome = [] lgt = len(self.indent) def grab(buf): outcome.append(self.indent + self.sep.join(buf)) texts.reverse() while texts: next = texts[-1] if not next: texts.pop() continue if lgt + len(self.sep) + len(next) <= self.width or not buf: buf.append(next) lgt += len(next) + len(self.sep) texts.pop() else: grab(buf) lgt = len(self.indent) buf = [] grab(buf) return "\n".join(outcome) class SubParagraph(Paragraph): """ indented sub paragraph """ indent = " " class Title(Paragraph): """ title element """ parentclass = Rest belowchar = "=" abovechar = "" def text(self): txt = self._get_text() lines = [] if self.abovechar: lines.append(self.abovechar * len(txt)) lines.append(txt) if self.belowchar: lines.append(self.belowchar * len(txt)) return "\n".join(lines) def _get_text(self): txt = [] for node in self.children: txt += node.wordlist() return ' '.join(txt) class AbstractText(AbstractNode): parentclass = [Paragraph, Title] start = "" end = "" def __init__(self, _text): self._text = _text def text(self): text = self.escape(self._text) return self.start + text + self.end def escape(self, text): if not isinstance(text, str) and not isinstance(text, unicode): text = str(text) if self.start: text = text.replace(self.start, '\\%s' % (self.start,)) if self.end and self.end != self.start: text = text.replace(self.end, '\\%s' % (self.end,)) return text class Text(AbstractText): def wordlist(self): text = escape(self._text) return self._reg_whitespace.split(text) class LiteralBlock(AbstractText): parentclass = Rest start = '::\n\n' def text(self): if not self._text.strip(): return '' text = self.escape(self._text).split('\n') for i, line in py.builtin.enumerate(text): if line.strip(): text[i] = ' %s' % (line,) return self.start + '\n'.join(text) class Em(AbstractText): start = "*" end = "*" class Strong(AbstractText): start = "**" end = "**" class Quote(AbstractText): start = '``' end = '``' class Anchor(AbstractText): start = '_`' end = '`' class Footnote(AbstractText): def __init__(self, note, symbol=False): raise NotImplemented('XXX') class Citation(AbstractText): def __init__(self, text, cite): raise NotImplemented('XXX') class ListItem(Paragraph): allow_nesting = True item_chars = '*+-' def text(self): idepth = self.get_indent_depth() indent = self.indent + (idepth + 1) * ' ' txt = '\n\n'.join(self.render_children(indent)) ret = [] item_char = self.item_chars[idepth] ret += [indent[len(item_char)+1:], item_char, ' ', txt[len(indent):]] return ''.join(ret) def render_children(self, indent): txt = [] buffer = [] def render_buffer(fro, to): if not fro: return p = Paragraph(indent=indent, *fro) p.parent = self.parent to.append(p.text()) for child in self.children: if isinstance(child, AbstractText): buffer.append(child) else: if buffer: render_buffer(buffer, txt) buffer = [] txt.append(child.text()) render_buffer(buffer, txt) return txt def get_indent_depth(self): depth = 0 current = self while (current.parent is not None and isinstance(current.parent, ListItem)): depth += 1 current = current.parent return depth class OrderedListItem(ListItem): item_chars = ["#."] * 5 class DListItem(ListItem): item_chars = None def __init__(self, term, definition, *args, **kwargs): self.term = term super(DListItem, self).__init__(definition, *args, **kwargs) def text(self): idepth = self.get_indent_depth() indent = self.indent + (idepth + 1) * ' ' txt = '\n\n'.join(self.render_children(indent)) ret = [] ret += [indent[2:], self.term, '\n', txt] return ''.join(ret) class Link(AbstractText): start = '`' end = '`_' def __init__(self, _text, target): self._text = _text self.target = target self.rest = None def text(self): if self.rest is None: self.rest = self.find_rest() if self.rest.links.get(self._text, self.target) != self.target: raise ValueError('link name %r already in use for a different ' 'target' % (self.target,)) self.rest.links[self._text] = self.target return AbstractText.text(self) def find_rest(self): # XXX little overkill, but who cares... next = self while next.parent is not None: next = next.parent return next class InternalLink(AbstractText): start = '`' end = '`_' class LinkTarget(Paragraph): def __init__(self, name, target): self.name = name self.target = target def text(self): return ".. _`%s`:%s\n" % (self.name, self.target) class Substitution(AbstractText): def __init__(self, text, **kwargs): raise NotImplemented('XXX') class Directive(Paragraph): indent = ' ' def __init__(self, name, *args, **options): self.name = name self.content = options.pop('content', []) children = list(args) super(Directive, self).__init__(*children) self.options = options def text(self): # XXX not very pretty... namechunksize = len(self.name) + 2 self.children.insert(0, Text('X' * namechunksize)) txt = super(Directive, self).text() txt = '.. %s::%s' % (self.name, txt[namechunksize + 3:],) options = '\n'.join([' :%s: %s' % (k, v) for (k, v) in self.options.iteritems()]) if options: txt += '\n%s' % (options,) if self.content: txt += '\n' for item in self.content: assert item.parentclass == Rest, 'only top-level items allowed' assert not item.indent item.indent = ' ' txt += '\n' + item.text() return txt