#!/usr/local/bin/pythonw # coding: iso-8859-1 # Copyright © 2009 by Amos Newcombe # # This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along with this program. If not, see . '''Given an XML document in the form of a Python structure using the classes herein, return the xml code text as a string. ''' from operator import add # to operate on lists class Xml(dict): # tag: xml '''the parent class for all xml-producing classes ''' # xmltmpl = '%s' # KLUGE: this value triggers an infinite loop in Xml.__str__() chIndent = ' ' def __init__(self, **d): # print repr(self) # or to indent: self.__repr__(n) dict.__init__(self, **d) def clone(self): return self.__class__(**dict(self).copy()) def __str__(self): return self.xmltmpl % self # def __repr__(self, nIndent=0): # return '%s(**%s)' % (self.__class__.__name__, self.legibleD(self, nIndent)) def legibleD(self, d, nIndent=0): return '\n'.join(['{'] + [ '%s%s: %s,' % (self.chIndent*(nIndent+1), repr(tp[0]), self.make_legible(tp[1], nIndent+1)) for tp in d.items() ] + [self.chIndent*nIndent + '}']) def legibleL(self, l, nIndent=0): return '\n'.join(['['] + [ '%s%s,' % (self.chIndent*(nIndent+1), self.make_legible(o, nIndent+1)) for o in l ] + [self.chIndent*nIndent + ']']) def make_legible(self, o, nIndent): return '\'\'\'%s\'\'\'' % o if isinstance(o, Xml) \ else (self.legibleL(o, nIndent) if isinstance(o, list) \ else (self.legibleD(o, nIndent) if isinstance(o, dict) \ else repr(o) ) ) class Document(Xml): # tag: doc '''an entire XML document Write str(doc) to a text file. ''' xmltmpl = '''\ %(xmlProlog)s %(xmlElem)s%(xmlEpilog)s ''' def __init__(self, **d): # print '%s(**%s)' % (self.__class__.__name__, self.legibleD(d)) Xml.__init__(self, xmlProlog = Prolog(**d), xmlEpilog = '\n'.join(d.get('lxmlEpilog', [])), **d ) # self['xmlElem'].DeclareNamespaces(self.get('lnsNamespaces', [])) Prefix(self, 'xmlEpilog', '\n') def PropagateNamespace(self): self['xmlElem'].PropagateNamespace() return self class Prolog(Xml): xmltmpl = '%(xmlDecl)s%(sMisc)s%(xmlDoctype)s' def __init__(self, **d): # print '%s(**%s)' % (self.__class__.__name__, self.legibleD(d)) dT = { 'xmlDecl' : XmlDeclaration(**d.get('dDecl', {})), 'sMisc' : '\n'.join([str(xml) for xml in d.get('lxmlMisc', [])]), 'xmlDoctype' : Doctype(**d.get('dDoctype', {})), } if not d.get('isExcluded') else {} Xml.__init__(self, **dT) Prefix(self, 'sMisc' , '\n') Prefix(self, 'xmlDoctype', '\n') class Doctype(Xml): xmltmpl = '' def __init__(self, **d): # print '%s(**%s)' % (self.__class__.__name__, self.legible(d)) Xml.__init__(self, **d) if self.has_key('uriSystem'): self['sExternalID'] = (( 'PUBLIC "%(idPublic)s"' if self.has_key('idPublic') else 'SYSTEM' ) + ' "%(uriSystem)s"') % self Prefix(self, 'sExternalID', ' ') if isinstance(self.get('lsInternal'), (str, unicode)): self['lsInternal'] = [self['lsInternal']] self['lsInternal'] = ['\t' + s for s in self.get('lsInternal', [])] if self.get('lsInternal'): self['sInternal'] = '[\n' + '\n'.join(self['lsInternal']) + '\n]' Prefix(self, 'sInternal', ' ') def __nonzero__(self): return 1 if self['sExternalID'] or self['sInternal'] else 0 class Element(Xml): # ¤ Creation sIndent = ' ' * 2 def __init__(self, **d): d.setdefault('sTag' , '') d.setdefault('ltpAttr', []) if isinstance(d['ltpAttr'], dict): d['ltpAttr'] = d['ltpAttr'].items() Xml.__init__(self, **d) self.ltpAttr = d['ltpAttr'] self.lContents = [] self.AddContents(*d.get('lContents', [])) try: self['nsApplied'].Declare(self) except KeyError: pass def AddContents(self, *tp): # print '%s.AddContents([%d elements])' % (self, len(tp)) for xml in tp: if isinstance(xml, Element): xml['_parent'] = self self.lContents.extend(tp) def ApplyNamespace(self, ns): # print '%s.ApplyNamespace(%s)' % (self['sTag'], ns) self['nsApplied'] = ns def PropagateNamespace(self, ns=None): if ns == None: ns = self.get('nsApplied') if ns == None: return for xml in self.lContents: if isinstance(xml, Element): if xml.get('nsApplied'): xml.PropagateNamespace(xml['nsApplied']) else: xml.ApplyNamespace(ns) xml.PropagateNamespace(ns) # ¤ Description def __str__(self, xmlContent=None, nIndent=0, sIndent=None): 'Apply namespaces and return the xml code for this tag instance.' # print 'Element<%s>.__str__(%s, %d, %s)' % \ # (self['sTag'], xmlContent, nIndent, repr(sIndent)) for xml in self.lContents: if isinstance(xml, Element): try: xml.ApplyNamespace(self['nsApplied']) except KeyError: pass return '\n'.join([ self.StrLine(o, nIndent, sIndent) for o in (self.XmlCode() if xmlContent == None else xmlContent) ]) def StrLine(self, o, nIndent=0, sIndent=None): 'Given a nested list of strings, return them properly indented for printing.' if sIndent == None: sIndent = self.sIndent if isinstance(o, (str, unicode)): return sIndent*nIndent + o elif isinstance(o, list): return self.__str__(o, nIndent+1, sIndent) else: raise BadTypeError(o) def XmlCode(self): '''self's contents as a nested list of (unindented) strings of xml This function is indirectly recursive through XmlLine(). I factor the code like this because the Xhtml class (in Xhtml.py) needs a specialized definition both of XmlCodeEmpty() to facilitate compatibility with legacy html browsers, and of XmlCodeNonempty() to avoid awkward line breaks in the code. ''' # print 'Element<%s>.XmlCode()' % self['sTag'] if self.lContents: return self.XmlCodeNonempty() else : return self.XmlCodeEmpty () def XmlCodeNonempty(self): # print 'Element<%s>.XmlCodeNonempty()' % self['sTag'] if self.OneLiner() : # Write short tags in-line. return [self.OpenTag() + str(self.lContents[0]) + self.CloseTag()] else: # Write longer tags nested on multiple lines. return [ self.OpenTag(), reduce(add, self.XmlContent()), self.CloseTag(), ] def XmlContent(self, lxml=None): # print 'Element<%s>.XmlContent(%s)' % (self['sTag'], lxml) return [self.XmlLine(o) for o in (lxml or self.lContents)] def OneLiner(self): 'An element is a one-liner if it has a single content element that is a string or another one-liner, or it is empty.' return (len(self.lContents) == 1 and ( isinstance(self.lContents[0], (str, unicode)) or ( isinstance(self.lContents[0], Element) and self.lContents[0].OneLiner() ) )) or not self.lContents def XmlLine(self, o): '''Turn a string|instance into a list of strings. This function is indirectly recursive through XmlCode(). ''' # print 'Element<%s>.XmlLine(<%s>)' % \ # (self['sTag'], (o['sTag'] if isinstance(o, Element) else o)) if isinstance(o, (str, unicode)): return [o] elif isinstance(o, Element): return o.XmlCode() elif isinstance(o, Xml): return [str(o)] else: raise BadTypeError(o) def XmlCodeEmpty(self): return [self.EmptyTag()] # ¤ Description: XML tags def OpenTag(self): return '<' + self.TagAttr() + '>' def CloseTag(self): return '' def EmptyTag(self): return '<' + self.TagAttr() + '/>' def TagAttr(self): # print '<%s>.TagAttr()' % self['sTag'] return self.QualifiedName() + ''.join([' %s="%s"' % tp for tp in self.ltpAttr]) def QualifiedName(self): try : sPrefix = self['nsApplied']['sPrefix'] except KeyError: sPrefix = '' sTag = self['sTag'] if sPrefix: sTag = sPrefix + ':' + sTag return sTag class CData(Xml): xmltmpl = '' class Comment(Xml): xmltmpl = '' class ProcessingInst(Xml): xmltmpl = '' class Declaration(ProcessingInst): def __init__(self, **d): 'If ltpArgs, calculate & replace sInstruction.' if isinstance(d['ltpArgs'], dict): d['ltpArgs'] = d['ltpArgs'].items() if isinstance(d['ltpArgs'], list): d['sInstruction'] = ' '.join(['%s="%s"' % tp for tp in d['ltpArgs']]) ProcessingInst.__init__(self, **d) class XmlDeclaration(Declaration): def __init__(self, **d): 'Replace sTarget, and build ltpArgs from sVersion, sEncoding and sStandalone.' d['sTarget'] = 'xml' d['ltpArgs'] = [('version' ,d.get('sVersion', '1.0'))] try: d['ltpArgs'].append(('encoding', d['sEncoding' ])) except KeyError: pass try: d['ltpArgs'].append(('standalone', d['sStandalone'])) except KeyError: pass Declaration.__init__(self, **d) class SSDeclaration(Declaration): def __init__(self, **d): 'Replace sTarget, and build ltpArgs from sHref and sType' d.update({ 'sTarget': 'xml-stylesheet', 'ltpArgs': [('href', d['sHref']), ('type', d.get('sType', 'text/css'))], }) Declaration.__init__(self, **d) class Namespace(dict): def __init__(self, **d): dict.__init__(self, **d) self.setdefault('sPrefix', '') if dnsNames.get(self.get('suri')): self.setdefault('uri', dnsNames[self['suri']]) self.isDeclared = False def Declare(self, xml=None): if self.isDeclared == False: if xml and not hasattr(self, 'xmlDecl'): self.xmlDecl = xml self.xmlDecl.setdefault('ltpAttr', []).append(self.Attr()) self.isDeclared = True # def Tag(self, sTag): # self.setdefault('sPrefix', '') # if self['sPrefix']: sTag = self['sPrefix'] + ':' + sTag # return sTag def Attr(self): return ('xmlns' + (':' + self['sPrefix'] if self['sPrefix'] else ''), self['uri']) dnsNames = { 'svg' : 'http://www.w3.org/2000/svg' , 'xhtml': 'http://www.w3.org/1999/xhtml', } # ¤ Utilities def Prefix(d, sName, sPrefix, sSuffix=None): 'Surround str(d[sName]) with prefix and optional suffix, unless it\'s nonexistent or empty.' # print 'Prefix(d, %s, %s)' % (repr(sName), repr(sPrefix), repr(sSuffix)) o = d.get(sName) # print repr(o) d[sName] = (sPrefix + str(o) + (sSuffix or '')) if o else '' from time import localtime def Copyright(**d): dT = { 'symbol': '©', 'year' : localtime().tm_year, 'owner' : 'Amos Newcombe', 'prefix': '', } dT.update(d) if dT['prefix'] and dT['prefix'][-1] != ' ': dT['prefix'] += ' ' if dT['symbol'][0] == '&': try: dT['owner'] = dT['owner'].replace('&', '&') except AttributeError: pass return '%(prefix)sCopyright %(symbol)s %(year)d by %(owner)s' % dT class BadTypeError(ValueError): def __init__(self, o): self.o = o def __str__(self): return 'bad type (%s): %s' % (type(self.o).__name__, `self.o`) if __name__ == '__main__': sMsg = 'Hello, World!' open('HelloWorld.xml', 'w').write(str(Document( dDoctype = { 'sElem' : 'greeting', 'lsInternal': [''], }, xmlElem = Element(sTag = 'greeting', lContents = [sMsg], nsApplied = \ Namespace(sPrefix='', uri='http://example.com/HelloWorld') ) )))