00001 ##################################################################################
00002 # Copyright (c) 2006  Gerard Flanagan
00003 #
00004 # Permission is hereby granted, free of charge, to any person obtaining
00005 # a copy of this software and associated documentation files (the "Software"),
00006 # to deal in the Software without restriction, including without limitation
00007 # the rights to use, copy, modify, merge, publish, distribute, sublicense,
00008 # and/or sell copies of the Software, and to permit persons to whom the
00009 # Software is furnished to do so, subject to the following conditions:
00010 #
00011 #    The above copyright notice and this permission notice shall be included
00012 #    in all copies or substantial portions of the Software.
00013 #
00014 # Requires 'elementtree' which can be obtained from:
00015 #
00016 #   http://www.effbot.org/zone/element-index.htm
00017 #
00018 # The tidywrite() method uses 'tidy-lib':
00019 #
00020 #   http://tidy.sourceforge.net/
00021 #
00022 # via the python binding here:
00023 #
00024 #   http://utidylib.berlios.de/
00025 #
00026 # To include colourised Python within HTML, the following file is required:
00027 #
00028 #    http://effbot.python-hosting.com/file/stuff/sandbox/pythondoc/colorizer.py
00029 #
00030 # Thanks to Fredrik Lundh, Bruno Desthuilliers, Steve Juranich and Daniel Nogradi
00031 #
00032 ##################################################################################
00033 
00034 
00035 HTML4_STRICT = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">'
00036 HTML4_TRANS = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">'
00037 HTML4_FRAMES = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">'
00038 XHTML4_STRICT = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">'
00039 XHTML4_TRANS = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'
00040 XHTML4_FRAMES = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">'
00041 
00042 SELF_CLOSING_TAGS = ['br', 'img', 'hr', 'meta', 'link', 'input']
00043 
00044 from elementtree.SimpleXMLWriter import XMLWriter
00045 from cStringIO import StringIO
00046 
00047 try:
00048     from tidy import parseString
00049 except ImportError:
00050     def parseString(text):
00051         return text
00052 
00053 def _iterelement( element ):
00054     '''
00055     returns all lists from a list of lists
00056     '''
00057     for item in element:
00058         yield item
00059         if len(item) > 0:
00060             for subitem in _iterelement( item ):
00061                 yield subitem
00062 
00063 class _HtmlWriter(XMLWriter, object):
00064 
00065     #The XmlWriter.data method does not call
00066     #XmlWriter.__flush. This means that nothing will be
00067     #written if an instance of XmlWriter only ever
00068     #calls data(), and never calls say, element() or
00069     #start(). Consequence: parentless _HtmlTexts
00070     #and _HtmlTemplates would render as None - so
00071     #override this method to avoid this
00072     def data( self, text ):
00073         '''write htmlentity-encoded text'''
00074         super( _HtmlWriter, self).data(text)
00075         self._XMLWriter__flush()
00076 
00077     #the data() method will escape all HTML entities,
00078     #write() will not - used by _HtmlIncludeFile and
00079     #_HtmlLiteral for example
00080     def write(self, text):
00081         '''write raw, ie. unescaped, text'''
00082         if self._XMLWriter__open:
00083             self._XMLWriter__open = 0
00084             self._XMLWriter__write( '>' )
00085         self._XMLWriter__write(text)
00086 
00087 class _HtmlElement(list):
00088 
00089     def __init__(self, tag):
00090         self.tag = tag
00091         self.innertext = None
00092         self.attrib = {}
00093 
00094     def __call__(self, innertext='', **kwargs):
00095         self.innertext = innertext
00096         self.attrib = kwargs
00097         return self
00098 
00099     def __repr__(self):
00100         return "<_HtmlElement %s at %x>" % (self.tag, id(self))
00101 
00102     def append(self, value):
00103         super(_HtmlElement,self).append(value)
00104         return value
00105 
00106     def insert(self, index, value):
00107         super(_HtmlElement,self).insert(value)
00108         return value
00109 
00110     def __getattr__(self, value):
00111         try:
00112             elmnt = types[value]()
00113         except KeyError:
00114             elmnt = _HtmlElement(value)
00115         return self.append( elmnt )
00116 
00117     def _write(self, writer):
00118         #class is a keyword, so can't use it as an attribute name
00119         #use one of 'css','class_','cssClass' then replace it here
00120         cssClass = None
00121         for key in self.attrib.iterkeys():
00122             if key in ['css','class_','cssClass']:
00123                 cssClass = self.attrib[key]
00124                 del self.attrib[key]
00125                 break
00126         if cssClass is not None:
00127             self.attrib['class'] = cssClass
00128         #now write this element
00129         if self.tag in SELF_CLOSING_TAGS:
00130             writer.element( self.tag, None, self.attrib )
00131             #now write any children
00132             for node in self:
00133                 node._write(writer)
00134         else:
00135             writer.start(self.tag, self.attrib)
00136             if self.innertext is not None:
00137                 writer.data(self.innertext)
00138             #now write any children
00139             for node in self:
00140                 node._write(writer)
00141             writer.end()
00142 
00143     def write(self, outfile ):
00144         w = _HtmlWriter(outfile)
00145         self._write( w )
00146 
00147     def __str__(self):
00148         s = StringIO()
00149         self.write( s )
00150         ret = s.getvalue()
00151         s.close()
00152         return ret
00153 
00154     def tidy(self):
00155         return str(parseString( self.__str__() ))
00156 
00157     def tidywrite(self, outfile):
00158         outfile.write( self.tidy() )
00159 
00160     def fill( self, searchList ):
00161         for t in _iterelement( self ):
00162             if hasattr( t, 'fill' ):#isinstance( t, _HtmlPlaceholder ):
00163                 t.fill( searchList )
00164 
00165     def __mod__(self, searchList):
00166         self.fill(searchList)
00167         return self
00168 
00169     def __imod__(self, searchList):
00170         self.fill(searchList)
00171         return self
00172 
00173 class _HtmlText(_HtmlElement):
00174     def __init__(self, innertext=''):
00175         self.innertext = innertext
00176 
00177     def __call__(self, innertext=''):
00178         self.innertext = innertext
00179         return self
00180 
00181     def __repr__(self):
00182         return "<_HtmlText at %x>" % id(self)
00183 
00184     def _write(self, writer):
00185         writer.data( self.innertext )
00186         for child in self:
00187             child._write(writer)
00188 
00189 class _HtmlLiteral(_HtmlText):
00190 
00191     def __repr__(self):
00192         return "<_HtmlLiteral at %x>" % id(self)
00193 
00194     def _write(self, writer):
00195         writer.write( self.innertext )
00196         for child in self:
00197             child._write(writer)
00198 
00199 class _HtmlComment(_HtmlText):
00200 
00201     def __repr__(self):
00202         return "<_HtmlComment at %x>" % id(self)
00203 
00204     def _write( self, writer, indent=0 ):
00205         writer.write('\n')
00206         writer.comment( self.innertext )
00207         writer.write('\n')
00208         for child in self:
00209             child._write(writer)
00210 
00211 class _HtmlIncludeFile(_HtmlElement):
00212 
00213     def __repr__(self):
00214         return "<_HtmlIncludeFile %s at %x>" % (self.filepath[10:], id(self))
00215 
00216     def __init__(self, path=None):
00217         self.filepath = path
00218 
00219     def __call__( self, path=None ):
00220         self.filepath = path
00221         return self
00222 
00223     def _write(self, writer):
00224         infile = open(self.filepath, 'r')
00225         try:
00226             for line in infile:
00227                 writer.write( line )
00228         finally:
00229             infile.close()
00230         for child in self:
00231             child._write(writer)
00232 
00233 class _HtmlPlaceholder(_HtmlElement):
00234 
00235     def __repr__(self):
00236         return "<_HtmlPlaceholder %s at %x>" % ( self.name, id(self) )
00237 
00238     def __init__(self, name='', format='%s', searchList=None):
00239         self.name = name
00240         self.format = format
00241         self.innertext = None
00242         self.rawtext = None
00243         if searchList:
00244             self.fill( searchList )
00245 
00246     def __call__(self, name, format='%s', searchList=None):
00247         self.__init__(name, format, searchList)
00248         return self
00249 
00250     def fill( self, searchList ):
00251         name = self.name
00252         for item in searchList:
00253             if isinstance( item, dict ):
00254                 value = item.get( name )
00255                 if value is not None:
00256                     if isinstance( value, str ):
00257                         self.innertext = value
00258                         self.rawtext = None
00259                     else:
00260                         #this clause is here to support the possibility of
00261                         #'filling' a placeholder with another _HtmlElement.
00262                         #However, the other element's string representation
00263                         #will already be html-escaped, so don't want to do
00264                         #it again, hence rawtext not innertext
00265                         self.rawtext = self.format % str(value)
00266                         self.innertext = None
00267                     break #found a value, no need to continue
00268 
00269     def __mod__(self, searchList):
00270         self.fill( searchList )
00271         return self
00272 
00273     def __imod__(self, searchList):
00274         return self.__mod__(searchList)
00275 
00276     def _write(self, writer):
00277         if self.innertext is not None:
00278             writer.data(self.innertext)
00279         elif self.rawtext is not None:
00280             writer.write(self.rawtext)
00281         else:
00282             s = ''.join( [ ' %(', self.name, ')', self.format[1:], ' ' ] )
00283             writer.data( s )
00284         for child in self:
00285             child._write(writer)
00286 
00287 class _CheetahTemplate(_HtmlPlaceholder):
00288 
00289     def __repr__(self):
00290         return "<_CheetahTemplate at %x>" % id(self)
00291 
00292     def __init__(self, source=None, searchList=None, file=None, name='CHEETAH'):
00293         super(_CheetahTemplate, self).__init__( name, format='%s' )
00294         self.source = source
00295         self.file = None
00296         if searchList:
00297             self.fill( searchList )
00298 
00299     def __call__(self, source=None, searchList=None, file=None, name='CHEETAH'):
00300         self.__init__(source, searchList, file, name)
00301         return self
00302 
00303     def fill(self, searchList):
00304         from Cheetah.Template import Template
00305         t = Template( source=self.source, searchList=searchList, file = self.file )
00306         self.rawtext = str( t )
00307 
00308 class _HtmlPythonSource(_HtmlIncludeFile):
00309 
00310     def __repr__(self):
00311         return "<_HtmlPythonSource at %x>" % id(self)
00312 
00313     def _write(self, writer):
00314         from colorizer import HtmlColorizer
00315         if self.filepath is not None:
00316             c = HtmlColorizer(self.filepath, writer)
00317             c.colorize()
00318         for child in self:
00319             child._write(writer)
00320 
00321 class _HtmlPage(_HtmlElement):
00322 
00323     def __init__(self, title='', doctype=HTML4_STRICT, styles=[]):
00324         self.title = title
00325         self.stylesheets = styles
00326         self.styleblocks = []
00327         self.doctype = doctype
00328 
00329     def __repr__(self):
00330         return "<_HtmlPage %s at %x>" % (self.title, id(self))
00331 
00332     def __call__(self, title='', doctype=HTML4_STRICT, styles=[]):
00333         self.__init__( title, doctype, styles )
00334         return self
00335 
00336     def _write(self, writer):
00337         writer.write(self.doctype+'\n')
00338         writer.start( "html" )
00339         writer.start( "head" )
00340         writer.element( "title", self.title )
00341         for url in self.stylesheets:
00342             writer.element( "link", rel="stylesheet", type="text/css", href="%s" % url )
00343         for style in self.styleblocks:
00344             writer.start( "style", type="text/css" )
00345             writer.data( style )
00346             writer.end()
00347         writer.end()
00348         writer.start("body")
00349         for node in self:
00350             node._write(writer)
00351         writer.end()
00352         writer.end()
00353 
00354 types = { 'text': _HtmlText,
00355             'literal': _HtmlLiteral,
00356             'comment': _HtmlComment,
00357             'include': _HtmlIncludeFile,
00358             'python': _HtmlPythonSource,
00359             'placeholder': _HtmlPlaceholder,
00360             'cheetah': _CheetahTemplate,
00361             'page': _HtmlPage
00362         }
00363 
00364 class _HtmlBuilder(object):
00365     '''a factory class for creating _HtmlElements '''
00366     def __getattr__(self, value, *args, **kwargs):
00367         try:
00368             return types[value]( *args, **kwargs )
00369         except KeyError:
00370             return _HtmlElement(value)
00371 
00372 
00373 html = _HtmlBuilder()
00374 
00375 
00376 if __name__ == '__main__':
00377     page = html.page('Test Page')
00378     page.styleblocks.append( 'p {color:black}\nbody {font-family:courier}' )
00379     page.comment('Begin Header')
00380     page.placeholder('HEADER')
00381     page.comment('Begin Content')
00382     page.placeholder('CONTENT')
00383     page.comment('Begin Footer')
00384     page.placeholder('FOOTER')
00385 
00386     header = html.h1( 'header here' )
00387     header.literal('<img src="dddd" />')
00388     content = html.div(id='content', css='main')
00389     content.h3('Welcome ').placeholder('USER').text('!!')
00390     content.hr
00391     links = content.ul(css='navbar')
00392     links.li.a( 'Home', href='/home.html' )
00393     links.li.a( 'Find Page', href='/FindPage.html' )
00394     links.li.a( 'Recent Changes', href='/RecentChanges.html' )
00395     #content.python("/usr/home/gerard/python/scratch/transform.py")
00396     footer = html.div(id='footer')
00397     try:
00398         from Cheetah.Template import Template
00399         footer.cheetah('Cheetah says the date is $DATE')
00400     except:
00401         footer.text('The date is ').placeholder('DATE')
00402 
00403     import time
00404     ts = time.asctime(time.localtime())
00405 
00406     content %= [{ 'USER': 'Arthur Dent' }]
00407     footer %= [{ 'DATE': ts }]
00408     page %= [{ 'HEADER': header, 'CONTENT': content, 'FOOTER': footer }]
00409 
00410     print '-' * 80
00411     print 'Using print'
00412     print '-' * 80
00413     print page
00414     print
00415     print '-' * 80
00416     print 'Using write()'
00417     print '-' * 80
00418     import sys
00419     page.write(sys.stdout)
00420     print
00421     print '-' * 80
00422     print 'Using tidywrite()'
00423     print '-' * 80
00424     page.tidywrite(sys.stdout)