/pypy/tool/rest/rst.py
Python | 410 lines | 394 code | 3 blank | 13 comment | 3 complexity | 6b3325783f6957208023513c70f9ab13 MD5 | raw file
- """ 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 = args
- super(Directive, self).__init__()
- self.options = options
-
- def text(self):
- # XXX not very pretty...
- txt = '.. %s::' % (self.name,)
- 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:
- txt += '\n ' + item
-
- return txt