"""This module explores a different approach to building doctree elements... The "normal" way of building a DOCUTILS tree involves, well, building a tree structure. So one might do:: section = docutils.nodes.section() section += docutils.nodes.title(text="Title") para = docutils.nodes.paragraph() section += para para += docutils.nodes.Text("Some ") para += docutils.nodes.strong(text="strong text.") That's all very nice, if one is *thinking* in terms of a tree structure, but it is not, for me, a very natural way to construct a text. (OK - I *know* in practice one would also have imported `paragraph`, etc., from `docutils.nodes`, but that is not my point.) This module allows one to use a more LaTex style of construction, with begin and end delimitors for DOCUTILS nodes. Thus the above example becomes:: build.start("section") build.add("title","Title") build.start("paragraph","Some ") build.add("strong","Strong text.") build.end("section") (As a convenience, paragraphs are automatically ended.) A slightly shorter, and possibly more obfuscated, way of writing this would be:: build.start("section",build.make("title","Title") build.start("paragraph","Some ",build.make("strong","Strong text.")) build.end("section") Sometimes I think that sort of approach makes more sense, sometimes not. """ # we need a " to keep [X]Emacs Python mode happy. import string import docutils.nodes import docutils.utils __docformat__ = "reST" # ---------------------------------------------------------------------- class group(docutils.nodes.Element): """Group is a way of grouping together elements. Compare it to HTML
and , or to TeX (?check?) \begingroup and \endgroup. It takes the special attribute `style`, which indicates what sort of thing it is grouping - for instance, "docstring" or "attributes". Although it (should be) supplied by the standard DOCUTILS tree, reST itself does not use `group`. It is solely used by extensions, such as ``pysource``. In the default HTML Writer, `group` renders invisibly (that is, it has no effect at all on the formatted output). """ pass # ---------------------------------------------------------------------- class BuildTree: def __init__(self, with_groups=1, root=None): self.stack = [] """A stack of tuples of the form ("classname",classinstance). """ self.root = root """A memory of the first item on the stack (notionally, the "document") - we need this because if we `start` a document, fill it up, and then `end` it, that final `end` will remove the appropriate instance from the stack, leaving no record. Thus this is that record. """ if root is not None: self._stack_add(root) self.with_groups = with_groups def finish(self): """Call this to indicate we have finished. It will grumble if anything is left unclosed, but will return the "root" instance of the DOCUTILS tree we've been building if all is well... """ if len(self.stack) > 0: raise ValueError,"Items still outstanding on stack: %s"%\ self._stack_as_string() else: return self.root def add(self,thing,*args,**keywords): """Add a `thing` DOCUTILS node at the current level. For instance:: build.add("paragraph","Some simple text.") If `thing` is "text" then it will automagically be converted to "Text" (this makes life easier for the user, as all of the other DOCUTILS node classes they are likely to use start with a lowercase letter, and "Text" is the sole exception). See `make` (which this uses) for more details of the arguments. """ if thing == "group" and not self.with_groups: return instance = self.make(thing,*args,**keywords) self._stack_append(instance) def addsubtree(self,subtree): """Add a DOCUTILS subtree to the current item. """ self._stack_append(subtree) def current(self): """Return the "current" item. That is, return the item to which `add()` will add DOCUTILS nodes. """ return self._stack_current() def start(self,thing,*args,**keywords): """Add a `thing` DOCUTILS node, starting a new level. `thing` should be either the name of a docutils.nodes class, or else a class itself. If `thing` is "text" then it will automagically be converted to "Text" (this makes life easier for the user, as all of the other DOCUTILS node classes they are likely to use start with a lowercase letter, and "Text" is the sole exception). For instance:: build.start("bullet_list") As a convenience, if `thing` is a paragraph, and if the current item is another paragraph, this method will end the old paragraph before starting the new. Note that if `thing` is "document", some extra magic is worked internally. If the keywords `warninglevel` and `errorlevel` are given, they will be passed to a docutils.utils.Reporter instance, as well as being passed down to the `document` class's initialiser. See `make` (which this uses) for more details of the arguments. """ name = self._nameof(thing) if name == "group" and not self.with_groups: return if name == "paragraph" and self._stack_ends("paragraph"): self.end("paragraph") if name == "document": if self.root: return if len(self.stack) > 0: raise ValueError,\ "Cannot insert 'document' except at root of stack" warninglevel = keywords.get("warninglevel",2) errorlevel = keywords.get("errorlevel",4) reporter = docutils.utils.Reporter('fubar', warninglevel, errorlevel) instance = docutils.nodes.document(reporter,"en") else: instance = self.make(thing,*args,**keywords) if len(self.stack) == 0: self.root = instance else: self._stack_append(instance) self._stack_add(instance) def end(self,thing): """End the level started below a `thing` DOCUTILS node. `thing` should be either the name of a docutils.nodes class, or else a class itself. For instance:: build.end("bullet_list") As a convenience, if the last item constructed was actually a paragraph, and `thing` is the container for said paragraph, then the paragraph will be automatically ended. Otherwise, for the moment at least, the `thing` being ended must be the last thing that was begun (in the future, we *might* support automatic "unrolling" of the stack, but not at the moment). """ name = self._nameof(thing) if thing == "group" and not self.with_groups: return if self._stack_ends("paragraph") and name != "paragraph": self.end("paragraph") self._stack_remove(name) def make(self,thing,*args,**keywords): """Return an instance of `docutils.nodes.thing` Attempts to regularise the initialisation of putting initial text into an Element and a TextElement... `thing` should be either the name of a docutils.nodes class, or else a class itself (so, for instance, one might call ``build.make("paragraph")`` or ``build.make(docutils.nodes.paragraph)``), or else None. If `thing` is "text" then it will automagically be converted to "Text" (this makes life easier for the user, as all of the other DOCUTILS node classes they are likely to use start with a lowercase letter, and "Text" is the sole exception). If `thing` is an Element subclass, then the arguments are just passed straight through - any *args list is taken to be children for the element (strings are coerced to Text instances), and any **keywords are taken as attributes. If `thing` is an TextElement subclass, then if the first item in *args is a string, it is passed down as the `text` parameter. Any remaining items from *args are used as child nodes, and any **keywords as attributes. If `thing` is a Text subclass, then a single argument is expected within *args, which must be a string, to be used as the Text's content. For instance:: n1 = build.make("paragraph","Some ", build.make("emphasis","text"), ".",align="center") n2 = build.make(None,"Just plain text") """ #print "make: %s, %s, %s"%(thing,args,keywords) # Temporary special case - since group is not (yet) in docutils.nodes... if thing == "group": thing = group if thing == None: dps_class = docutils.nodes.Text elif type(thing) == type(""): if thing == "text": thing = "Text" try: dps_class = getattr(docutils.nodes,thing) except AttributeError: raise ValueError,"docutils.nodes does not define '%s'"%thing else: dps_class = thing # NB: check for TextElement before checking for Element, # since TextElement is itself a subclass of Element! if issubclass(dps_class,docutils.nodes.TextElement): # Force the use of the argument list as such, by insisting # that the `rawsource` and `text` arguments are empty strings args = self._convert_args(args) dps_instance = dps_class("","",*args,**keywords) elif issubclass(dps_class,docutils.nodes.Element): # Force the use of the argument list as such, by insisting # that the `rawsource` arguments is an empty string args = self._convert_args(args) dps_instance = dps_class("",*args,**keywords) elif issubclass(dps_class,docutils.nodes.Text): if len(args) > 1: raise ValueError,\ "Text subclass %s may only take one argument"%\ self._nameof(thing) elif len(args) == 1: text = args[0] else: text = "" if keywords: raise ValueError,\ "Text subclass %s cannot use keyword arguments"%\ self._nameof(thing) dps_instance = dps_class(text) else: raise ValueError,"%s is not an Element or TextElement"%\ self._nameof(thing) #print " ",dps_instance return dps_instance def _convert_args(self,args): """Return the arguments, with strings converted to Texts. """ newargs = [] for arg in args: if type(arg) == type(""): newargs.append(docutils.nodes.Text(arg)) else: newargs.append(arg) return newargs def __getattr__(self,name): """Return an appropriate DOCUTILS class, for instantiation. """ return getattr(docutils.nodes,name) # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ def _nameof(self,thing): if thing is None: return "Text" elif type(thing) == type(""): return thing else: return thing.__name__ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Stack maintenance def _stack_ends(self,name): """Return true if the stack ends with the named entity. """ return self.stack[-1][0] == name def _stack_add(self,instance): """Add a new level to the stack. """ self.stack.append((instance.__class__.__name__,instance)) def _stack_remove(self,name): """Remove the last level from the stack (but only if it is of the right sort). """ if len(self.stack) == 0: raise ValueError,"Cannot end %s - nothing outstanding to end"%\ (name) if name != self.stack[-1][0]: raise ValueError,"Cannot end %s - last thing begun was %s"%\ (name,self.stack[-1][0]) del self.stack[-1] def _stack_append(self,instance): """Append an instance to the last item on the stack. """ if len(self.stack) > 0: self.stack[-1][1].append(instance) else: raise ValueError,"Cannot add %s to current level" \ " - nothing current"%(instance.__class__.__name__) def _stack_current(self): """Return the "current" element from the stack That is, the element to which we would append any new instances with `_stack_append()` """ return self.stack[-1][1] def _stack_as_string(self): names = [] for name,inst in self.stack: names.append(name) return string.join(names,",") # ---------------------------------------------------------------------- if __name__ == "__main__": build = BuildTree() #print build.make("paragraph",text="fred") #print build.paragraph(text="fred") print "Building a section" build.start("section") build.add("title","Fred") build.start("paragraph") build.add("text","This is some text.") build.add("strong","Really.") build.start("paragraph","Another paragraph") build.end("section") print build.finish() #print "Building a broken section" #build.start("section") #build.add("title","Fred") #build.start("paragraph") #print build.finish()