#!/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 <http://www.gnu.org/licenses/>.

'''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 = '<!DOCTYPE %(sElem)s%(sExternalID)s%(sInternal)s>'
	
	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 '</' + self.QualifiedName() +  '>'
	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 = '<![CDATA[%(sData)s]]>'

class Comment(Xml):
	
	xmltmpl = '<!-- %(sComment)s -->'		

class ProcessingInst(Xml):
	
	xmltmpl = '<?%(sTarget)s %(sInstruction)s?>'

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': '&copy;',
		'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('&', '&amp;')
		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': ['<!ELEMENT greeting (#PCDATA)>'],
		}, 
		xmlElem = Element(sTag = 'greeting', lContents = [sMsg], nsApplied = \
			Namespace(sPrefix='', uri='http://example.com/HelloWorld')
		)
	)))