from templess import (nodebase, elnode, textnode, commentnode, cdatanode, templessnode, template) import util # used for getting (X)HTML output from the XML serializers HTMLNONSINGLETONS = ( 'a', 'abbr', 'acronym', 'address', 'applet', 'b', 'bdo', 'big', 'blink', 'blockquote', 'button', 'caption', 'center', 'cite', 'comment', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'font', 'form', 'frameset', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'i', 'iframe', 'ins', 'kbd', 'label', 'legend', 'li', 'listing', 'map', 'marquee', 'menu', 'multicol', 'nobr', 'noembed', 'noframes', 'noscript', 'object', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'script', 'select', 'small', 'span', 'strike', 'strong', 'style', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'title', 'tr', 'tt', 'u', 'ul', 'xmp') # convert/serialize functionality class convertor(object): def __init__(self, node): if isinstance(node, template): node = node.tree self.node = node def convert(self, context): """ process the templess directives returns a copy of element, or a list of copies of element (in case of t:content), or None (in case we have a matching cond or not attribute) can return unexpected results in case of t:replace, use only on root nodes of documents in production situation (iow: if you want to convert a tree, always make sure there's no t:replace on the node you call convert() on) context is a context dict (see docs), parent is an internal helper argument """ self.node = self._convert_node(self.node, context) return self.node def _convert_node(self, node, context, parent=None): # first see if we need to be rendered at all cont = self._handle_cond(node, context) if not cont: return doreplace, attrs, cvalue, ctype = self._process_attrs(node, context) # render our children if cvalue is None: return self._handle_none( node, context, parent, cvalue, doreplace, attrs) elif (ctype in ('iter', 'ctx') or (util.is_iterable_not_string(cvalue) and ctype is None)): return self._handle_list( node, context, parent, cvalue, doreplace, ctype, attrs) else: return self._handle_direct_insert( node, context, parent, cvalue, doreplace, attrs) def _handle_none(self, node, context, parent, cvalue, doreplace, attrs): retnode = parent if not doreplace: retnode = elnode(node.name, attrs, parent, node.charset) for child in node: if isinstance(child, templessnode): self._convert_node(child, context, retnode) elif isinstance(child, elnode): elnode( child.name, child.attrs, retnode, child.charset) elif (isinstance(child, textnode) or isinstance(child, commentnode) or isinstance(child, cdatanode)): child.__class__(child.text, retnode, child.charset) return retnode def _handle_list( self, node, context, parent, cvalue, doreplace, ctype, attrs): ret = [] if (ctype == 'ctx' or isinstance(cvalue, dict) or (isinstance(cvalue, util.objectcontext) and ctype != 'iter')): cvalue = [cvalue] for ccontext in cvalue: if (ctype == 'ctx' or (isinstance(ccontext, dict) and ctype is None) or isinstance(ccontext, util.objectcontext)): newparent = parent if not doreplace: # create a similar node to retnode newparent = elnode(node.name, attrs, parent, node.charset) for child in node: if isinstance(child, templessnode): self._convert_node(child, ccontext, newparent) elif isinstance(child, elnode): elnode( child.name, child.attrs, newparent, child.charset) elif (isinstance(child, textnode) or isinstance(child, commentnode) or isinstance(child, cdatanode)): child.__class__(child.text, newparent, child.charset) # return the last of the new nodes ret.append(newparent) else: newparent = parent if not doreplace: newparent = elnode(node.name, attrs, parent, node.charset) if isinstance(ccontext, nodebase): if ccontext.parent: ccontext.parent.remove(ccontext) ccontext.parent = newparent newparent.append(ccontext) else: textnode( util.strconvert(ccontext, node.charset), newparent, node.charset) ret.append(newparent) return ret def _handle_direct_insert( self, node, context, parent, cvalue, doreplace, attrs): retnode = parent if not doreplace: retnode = elnode(node.name, attrs, parent, node.charset) if isinstance(cvalue, nodebase): if cvalue.parent: cvalue.parent.remove(cvalue) cvalue.parent = retnode retnode.append(cvalue) else: textnode( util.strconvert(cvalue, node.charset), retnode, node.charset) return retnode def _handle_cond(self, node, context): """ handle the 't:cond' and 't:not' directives returns False if there's a t:cond or t:not that doesn't allow rendering, True otherwise """ cond = node.directives.get('cond') if cond: try: return not not context[cond] except KeyError: return False cnot = node.directives.get('not', False) if cnot: try: return not context[cnot] except KeyError: return True return True def _process_attrs(self, node, context): replacekey = self._get_replace(node) contentkey = self._get_content(node) attrs = self._process_attr(node, context) # find out what context we need to use for rendering our children, or # what data we need to interpolate cvalue = None ctype = None if replacekey: if ':' in replacekey: replacekey, ctype = replacekey.rsplit(':', 1) cvalue = context[replacekey] elif contentkey: if ':' in contentkey: contentkey, ctype = contentkey.rsplit(':', 1) cvalue = context[contentkey] return not not replacekey, attrs, cvalue, ctype def _process_attr(self, node, context): strvalue = node.directives.get('attr') attrs = node.attrs.copy() if strvalue: pairs = [v.strip() for v in strvalue.split(';')] for pair in pairs: k, v = pair.split(' ') value = context[v] if not value and value != '': # allow empty attrs by using '' attrs.pop(k, None) else: attrs[k] = value return attrs def _get_replace(self, node): replace = node.directives.get('replace', False) return replace def _get_content(self, node): content = node.directives.get('content', False) return content class xmlserializer(convertor): def unicode(self, html=False): """ convert self.node to an XML unicode object this does _not_ implicitly call convert - you'll need to call that first if self.node is a template or templessnode (or you'll get the original document re-serialized) """ return self._unicode(self.node, html) def _unicode(self, node, html=False): if isinstance(node, elnode): return self._unicode_element(node, html) elif isinstance(node, cdatanode): return u'' % (node.text,) elif isinstance(node, commentnode): return u'' % (node.text,) elif isinstance(node, textnode): return node.text def _start_node_start(self, node, attrs): return u'<' + node.name + self._serialize_attrs(node, attrs) def _end_node(self, node): return u'' def _serialize_attrs(self, node, attrs): """ return the attributes as a string """ if not len(attrs): return '' items = sorted(attrs.items()) return ' ' + ' '.join( [k + '="' + util.strconvert(v, node.charset) + '"' for (k, v) in items]) def _unicode_element(self, node, html=False): """ return a string (XML) representation of ourselves (unrendered!) """ ret = [self._start_node_start(node, node.attrs)] if not len(node) and ( not html or node.name.lower() not in HTMLNONSINGLETONS): ret.append(u' />') else: ret.append(u'>') for child in node: ret.append(self._unicode(child, html=html)) ret.append(self._end_node(node)) return ''.join(ret) class xmlgenerator(xmlserializer): def convert_generate(self, context, html=False): """ process templess directives and yield string result bits this has the advantage over calling convert() and then generate() seperately that it's really lazy - dict access happens only when the part that requires it is generated """ return self._generate_node(self.node, context, html=html) def generate(self, html=False): """ serialize to XML, yield chunks of unicode this does not convert - to get converted XML, use 'xmlgenerator.convert_generate()', or call 'xmlgenerator.convert()' before calling this (although the latter is not entirely lazy) """ return self._generate_el(self.node, html) def _generate_node(self, node, context, html=False, parent=None): if isinstance(node, templessnode): for chunk in self._generate_tnode(node, context, html, parent): yield chunk elif isinstance(node, elnode): for chunk in self._generate_el(node, html): yield chunk elif isinstance(node, commentnode): yield u'' elif isinstance(node, cdatanode): yield u'' elif isinstance(node, textnode): yield node.text def _generate_tnode(self, node, context, html=False, parent=None): cont = self._handle_cond(node, context) if cont: doreplace, attrs, cvalue, ctype = self._process_attrs( node, context) if cvalue is None: # nothing to do, just return copy of self issingle = not len(node) and ( not html or node.name.lower() not in HTMLNONSINGLETONS) if issingle and not doreplace: for s in self._start_node(node, attrs, True): yield s else: retnode = parent yieldstartend = False yieldend = True if not doreplace: retnode = elnode( node.name, attrs, parent, node.charset) # first yield the start node bit up to the closing # > (or /> for singletons) yield self._start_node_start(retnode, attrs) # if this has no children, and it's not an html node # that must not be closed, the closing bit should be # yielded next if html and node.name in HTMLNONSINGLETONS: yield u'>' else: yieldstartend = True for child in node: for s in self._generate_node( child, context, html, retnode): if yieldstartend: yield u'>' yieldstartend = False yield s else: # not yet yielded the closing > of the start tag, so # we don't seem to have children, nor are an HTML # tag that must be closed... render as singleton if yieldstartend: yield u' />' yieldend = False if yieldend and not doreplace: yield self._end_node(node) elif (ctype == 'iter' or ctype == 'ctx' or (util.is_iterable_not_string(cvalue) and ctype is None)): if (ctype == 'ctx' or ((isinstance(cvalue, dict) or isinstance(cvalue, util.objectcontext)) and ctype is None)): cvalue = [cvalue] if cvalue: for ccontext in cvalue: if (ctype == 'ctx' or ((isinstance(ccontext, dict) or isinstance(ccontext, util.objectcontext)) and ctype is None)): if not doreplace: yieldstartend = False yieldend = True yield self._start_node_start(node, attrs) if html and node.name in HTMLNONSINGLETONS: yield u'>' else: yieldstartend = True newparent = parent if not doreplace: # create a similar node to retnode newparent = elnode(node.name, attrs, parent, node.charset) for child in node: for s in self._generate_node( child, ccontext, html, newparent): if not doreplace and yieldstartend: yield u'>' yieldstartend = False yield s if not doreplace and yieldstartend: yield u' />' elif not doreplace: yield self._end_node(node) else: newparent = elnode( node.name, attrs, parent, node.charset) if isinstance(ccontext, nodebase): if ccontext.parent: ccontext.parent.remove(ccontext) ccontext.parent = newparent newparent.append(ccontext) else: ccontext = textnode( util.strconvert(ccontext, node.charset), newparent, node.charset) if doreplace: for child in newparent: for s in self._generate_node( child, None, html): yield s else: for s in self._generate_node( newparent, ccontext, html): yield s else: retnode = parent if not doreplace: retnode = elnode( node.name, attrs, parent, node.charset) if isinstance(cvalue, nodebase): if cvalue.parent: cvalue.parent.remove(cvalue) cvalue.parent = retnode retnode.append(cvalue) else: textnode( util.strconvert(cvalue, node.charset), retnode, node.charset) if doreplace: for child in retnode: for s in self._generate_node(child, context, html): yield s else: for s in self._generate_node(retnode, context, html): yield s def _generate_el(self, node, html=False): """ returns self as a generator (yielding unicode strings) """ issingle = html and node.name.lower() not in HTMLNONSINGLETONS yield self._start_node_start(node, node.attrs) if not issingle: yield u'>' for child in node: if issingle: # not so single after all :) yield u'>' issingle = False for chunk in self._generate_node(child, None, html=html): yield chunk if not issingle: yield self._end_node(node) else: yield u' />' def _start_node(self, node, attrs, single): yield self._start_node_start(node, attrs) if single: yield u' />' else: yield u'>'