418 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			418 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
	
| 
 | |
| """ 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.items():
 | |
|             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 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 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.items()])
 | |
|         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
 | |
| 
 |