| # This code is in the public domain, it comes |
| # with absolutely no warranty and you can do |
| # absolutely whatever you want with it. |
| |
| __date__ = '1 October 2012' |
| __version__ = '1.9' |
| __doc__= """ |
| This is markup.py - a Python module that attempts to |
| make it easier to generate HTML/XML from a Python program |
| in an intuitive, lightweight, customizable and pythonic way. |
| |
| The code is in the public domain. |
| |
| Version: %s as of %s. |
| |
| Documentation and further info is at http://markup.sourceforge.net/ |
| |
| Please send bug reports, feature requests, enhancement |
| ideas or questions to nogradi at gmail dot com. |
| |
| Installation: drop markup.py somewhere into your Python path. |
| """ % ( __version__, __date__ ) |
| |
| try: |
| basestring |
| import string |
| except: |
| # python 3 |
| basestring = str |
| string = str |
| |
| # tags which are reserved python keywords will be referred |
| # to by a leading underscore otherwise we end up with a syntax error |
| import keyword |
| |
| class element: |
| """This class handles the addition of a new element.""" |
| |
| def __init__( self, tag, case='lower', parent=None ): |
| self.parent = parent |
| |
| if case == 'upper': |
| self.tag = tag.upper( ) |
| elif case == 'lower': |
| self.tag = tag.lower( ) |
| elif case =='given': |
| self.tag = tag |
| else: |
| self.tag = tag |
| |
| def __call__( self, *args, **kwargs ): |
| if len( args ) > 1: |
| raise ArgumentError( self.tag ) |
| |
| # if class_ was defined in parent it should be added to every element |
| if self.parent is not None and self.parent.class_ is not None: |
| if 'class_' not in kwargs: |
| kwargs['class_'] = self.parent.class_ |
| |
| if self.parent is None and len( args ) == 1: |
| x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] |
| return '\n'.join( x ) |
| elif self.parent is None and len( args ) == 0: |
| x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] |
| return '\n'.join( x ) |
| |
| if self.tag in self.parent.twotags: |
| for myarg, mydict in _argsdicts( args, kwargs ): |
| self.render( self.tag, False, myarg, mydict ) |
| elif self.tag in self.parent.onetags: |
| if len( args ) == 0: |
| for myarg, mydict in _argsdicts( args, kwargs ): |
| self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0 |
| else: |
| raise ClosingError( self.tag ) |
| elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: |
| raise DeprecationError( self.tag ) |
| else: |
| raise InvalidElementError( self.tag, self.parent.mode ) |
| |
| def render( self, tag, single, between, kwargs ): |
| """Append the actual tags to content.""" |
| |
| out = "<%s" % tag |
| for key, value in list( kwargs.items( ) ): |
| if value is not None: # when value is None that means stuff like <... checked> |
| key = key.strip('_') # strip this so class_ will mean class, etc. |
| if key == 'http_equiv': # special cases, maybe change _ to - overall? |
| key = 'http-equiv' |
| elif key == 'accept_charset': |
| key = 'accept-charset' |
| out = "%s %s=\"%s\"" % ( out, key, escape( value ) ) |
| else: |
| out = "%s %s" % ( out, key ) |
| if between is not None: |
| out = "%s>%s</%s>" % ( out, between, tag ) |
| else: |
| if single: |
| out = "%s />" % out |
| else: |
| out = "%s>" % out |
| if self.parent is not None: |
| self.parent.content.append( out ) |
| else: |
| return out |
| |
| def close( self ): |
| """Append a closing tag unless element has only opening tag.""" |
| |
| if self.tag in self.parent.twotags: |
| self.parent.content.append( "</%s>" % self.tag ) |
| elif self.tag in self.parent.onetags: |
| raise ClosingError( self.tag ) |
| elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: |
| raise DeprecationError( self.tag ) |
| |
| def open( self, **kwargs ): |
| """Append an opening tag.""" |
| |
| if self.tag in self.parent.twotags or self.tag in self.parent.onetags: |
| self.render( self.tag, False, None, kwargs ) |
| elif self.mode == 'strict_html' and self.tag in self.parent.deptags: |
| raise DeprecationError( self.tag ) |
| |
| class page: |
| """This is our main class representing a document. Elements are added |
| as attributes of an instance of this class.""" |
| |
| def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ): |
| """Stuff that effects the whole document. |
| |
| mode -- 'strict_html' for HTML 4.01 (default) |
| 'html' alias for 'strict_html' |
| 'loose_html' to allow some deprecated elements |
| 'xml' to allow arbitrary elements |
| |
| case -- 'lower' element names will be printed in lower case (default) |
| 'upper' they will be printed in upper case |
| 'given' element names will be printed as they are given |
| |
| onetags -- list or tuple of valid elements with opening tags only |
| twotags -- list or tuple of valid elements with both opening and closing tags |
| these two keyword arguments may be used to select |
| the set of valid elements in 'xml' mode |
| invalid elements will raise appropriate exceptions |
| |
| separator -- string to place between added elements, defaults to newline |
| |
| class_ -- a class that will be added to every element if defined""" |
| |
| valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ] |
| valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON", |
| "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET", |
| "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS", |
| "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", |
| "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE", |
| "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR", |
| "TT", "UL", "VAR" ] |
| deprecated_onetags = [ "BASEFONT", "ISINDEX" ] |
| deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ] |
| |
| self.header = [ ] |
| self.content = [ ] |
| self.footer = [ ] |
| self.case = case |
| self.separator = separator |
| |
| # init( ) sets it to True so we know that </body></html> has to be printed at the end |
| self._full = False |
| self.class_= class_ |
| |
| if mode == 'strict_html' or mode == 'html': |
| self.onetags = valid_onetags |
| self.onetags += list( map( string.lower, self.onetags ) ) |
| self.twotags = valid_twotags |
| self.twotags += list( map( string.lower, self.twotags ) ) |
| self.deptags = deprecated_onetags + deprecated_twotags |
| self.deptags += list( map( string.lower, self.deptags ) ) |
| self.mode = 'strict_html' |
| elif mode == 'loose_html': |
| self.onetags = valid_onetags + deprecated_onetags |
| self.onetags += list( map( string.lower, self.onetags ) ) |
| self.twotags = valid_twotags + deprecated_twotags |
| self.twotags += list( map( string.lower, self.twotags ) ) |
| self.mode = mode |
| elif mode == 'xml': |
| if onetags and twotags: |
| self.onetags = onetags |
| self.twotags = twotags |
| elif ( onetags and not twotags ) or ( twotags and not onetags ): |
| raise CustomizationError( ) |
| else: |
| self.onetags = russell( ) |
| self.twotags = russell( ) |
| self.mode = mode |
| else: |
| raise ModeError( mode ) |
| |
| def __getattr__( self, attr ): |
| |
| # tags should start with double underscore |
| if attr.startswith("__") and attr.endswith("__"): |
| raise AttributeError( attr ) |
| # tag with single underscore should be a reserved keyword |
| if attr.startswith( '_' ): |
| attr = attr.lstrip( '_' ) |
| if attr not in keyword.kwlist: |
| raise AttributeError( attr ) |
| |
| return element( attr, case=self.case, parent=self ) |
| |
| def __str__( self ): |
| |
| if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ): |
| end = [ '</body>', '</html>' ] |
| else: |
| end = [ ] |
| |
| return self.separator.join( self.header + self.content + self.footer + end ) |
| |
| def __call__( self, escape=False ): |
| """Return the document as a string. |
| |
| escape -- False print normally |
| True replace < and > by < and > |
| the default escape sequences in most browsers""" |
| |
| if escape: |
| return _escape( self.__str__( ) ) |
| else: |
| return self.__str__( ) |
| |
| def add( self, text ): |
| """This is an alias to addcontent.""" |
| self.addcontent( text ) |
| |
| def addfooter( self, text ): |
| """Add some text to the bottom of the document""" |
| self.footer.append( text ) |
| |
| def addheader( self, text ): |
| """Add some text to the top of the document""" |
| self.header.append( text ) |
| |
| def addcontent( self, text ): |
| """Add some text to the main part of the document""" |
| self.content.append( text ) |
| |
| |
| def init( self, lang='en', css=None, metainfo=None, title=None, header=None, |
| footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ): |
| """This method is used for complete documents with appropriate |
| doctype, encoding, title, etc information. For an HTML/XML snippet |
| omit this method. |
| |
| lang -- language, usually a two character string, will appear |
| as <html lang='en'> in html mode (ignored in xml mode) |
| |
| css -- Cascading Style Sheet filename as a string or a list of |
| strings for multiple css files (ignored in xml mode) |
| |
| metainfo -- a dictionary in the form { 'name':'content' } to be inserted |
| into meta element(s) as <meta name='name' content='content'> |
| (ignored in xml mode) |
| |
| base -- set the <base href="..."> tag in <head> |
| |
| bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added |
| as attributes of the <body> element as <body key='value' ... > |
| (ignored in xml mode) |
| |
| script -- dictionary containing src:type pairs, <script type='text/type' src=src></script> |
| or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all |
| |
| title -- the title of the document as a string to be inserted into |
| a title element as <title>my title</title> (ignored in xml mode) |
| |
| header -- some text to be inserted right after the <body> element |
| (ignored in xml mode) |
| |
| footer -- some text to be inserted right before the </body> element |
| (ignored in xml mode) |
| |
| charset -- a string defining the character set, will be inserted into a |
| <meta http-equiv='Content-Type' content='text/html; charset=myset'> |
| element (ignored in xml mode) |
| |
| encoding -- a string defining the encoding, will be put into to first line of |
| the document as <?xml version='1.0' encoding='myencoding' ?> in |
| xml mode (ignored in html mode) |
| |
| doctype -- the document type string, defaults to |
| <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'> |
| in html mode (ignored in xml mode)""" |
| |
| self._full = True |
| |
| if self.mode == 'strict_html' or self.mode == 'loose_html': |
| if doctype is None: |
| doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>" |
| self.header.append( doctype ) |
| self.html( lang=lang ) |
| self.head( ) |
| if charset is not None: |
| self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset ) |
| if metainfo is not None: |
| self.metainfo( metainfo ) |
| if css is not None: |
| self.css( css ) |
| if title is not None: |
| self.title( title ) |
| if script is not None: |
| self.scripts( script ) |
| if base is not None: |
| self.base( href='%s' % base ) |
| self.head.close() |
| if bodyattrs is not None: |
| self.body( **bodyattrs ) |
| else: |
| self.body( ) |
| if header is not None: |
| self.content.append( header ) |
| if footer is not None: |
| self.footer.append( footer ) |
| |
| elif self.mode == 'xml': |
| if doctype is None: |
| if encoding is not None: |
| doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding |
| else: |
| doctype = "<?xml version='1.0' ?>" |
| self.header.append( doctype ) |
| |
| def css( self, filelist ): |
| """This convenience function is only useful for html. |
| It adds css stylesheet(s) to the document via the <link> element.""" |
| |
| if isinstance( filelist, basestring ): |
| self.link( href=filelist, rel='stylesheet', type='text/css', media='all' ) |
| else: |
| for file in filelist: |
| self.link( href=file, rel='stylesheet', type='text/css', media='all' ) |
| |
| def metainfo( self, mydict ): |
| """This convenience function is only useful for html. |
| It adds meta information via the <meta> element, the argument is |
| a dictionary of the form { 'name':'content' }.""" |
| |
| if isinstance( mydict, dict ): |
| for name, content in list( mydict.items( ) ): |
| self.meta( name=name, content=content ) |
| else: |
| raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." ) |
| |
| def scripts( self, mydict ): |
| """Only useful in html, mydict is dictionary of src:type pairs or a list |
| of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type. |
| Will be rendered as <script type='text/type' src=src></script>""" |
| |
| if isinstance( mydict, dict ): |
| for src, type in list( mydict.items( ) ): |
| self.script( '', src=src, type='text/%s' % type ) |
| else: |
| try: |
| for src in mydict: |
| self.script( '', src=src, type='text/javascript' ) |
| except: |
| raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." ) |
| |
| |
| class _oneliner: |
| """An instance of oneliner returns a string corresponding to one element. |
| This class can be used to write 'oneliners' that return a string |
| immediately so there is no need to instantiate the page class.""" |
| |
| def __init__( self, case='lower' ): |
| self.case = case |
| |
| def __getattr__( self, attr ): |
| |
| # tags should start with double underscore |
| if attr.startswith("__") and attr.endswith("__"): |
| raise AttributeError( attr ) |
| # tag with single underscore should be a reserved keyword |
| if attr.startswith( '_' ): |
| attr = attr.lstrip( '_' ) |
| if attr not in keyword.kwlist: |
| raise AttributeError( attr ) |
| |
| return element( attr, case=self.case, parent=None ) |
| |
| oneliner = _oneliner( case='lower' ) |
| upper_oneliner = _oneliner( case='upper' ) |
| given_oneliner = _oneliner( case='given' ) |
| |
| def _argsdicts( args, mydict ): |
| """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1.""" |
| |
| if len( args ) == 0: |
| args = None, |
| elif len( args ) == 1: |
| args = _totuple( args[0] ) |
| else: |
| raise Exception( "We should have never gotten here." ) |
| |
| mykeys = list( mydict.keys( ) ) |
| myvalues = list( map( _totuple, list( mydict.values( ) ) ) ) |
| |
| maxlength = max( list( map( len, [ args ] + myvalues ) ) ) |
| |
| for i in range( maxlength ): |
| thisdict = { } |
| for key, value in zip( mykeys, myvalues ): |
| try: |
| thisdict[ key ] = value[i] |
| except IndexError: |
| thisdict[ key ] = value[-1] |
| try: |
| thisarg = args[i] |
| except IndexError: |
| thisarg = args[-1] |
| |
| yield thisarg, thisdict |
| |
| def _totuple( x ): |
| """Utility stuff to convert string, int, long, float, None or anything to a usable tuple.""" |
| |
| if isinstance( x, basestring ): |
| out = x, |
| elif isinstance( x, ( int, long, float ) ): |
| out = str( x ), |
| elif x is None: |
| out = None, |
| else: |
| out = tuple( x ) |
| |
| return out |
| |
| def escape( text, newline=False ): |
| """Escape special html characters.""" |
| |
| if isinstance( text, basestring ): |
| if '&' in text: |
| text = text.replace( '&', '&' ) |
| if '>' in text: |
| text = text.replace( '>', '>' ) |
| if '<' in text: |
| text = text.replace( '<', '<' ) |
| if '\"' in text: |
| text = text.replace( '\"', '"' ) |
| if '\'' in text: |
| text = text.replace( '\'', '"' ) |
| if newline: |
| if '\n' in text: |
| text = text.replace( '\n', '<br>' ) |
| |
| return text |
| |
| _escape = escape |
| |
| def unescape( text ): |
| """Inverse of escape.""" |
| |
| if isinstance( text, basestring ): |
| if '&' in text: |
| text = text.replace( '&', '&' ) |
| if '>' in text: |
| text = text.replace( '>', '>' ) |
| if '<' in text: |
| text = text.replace( '<', '<' ) |
| if '"' in text: |
| text = text.replace( '"', '\"' ) |
| |
| return text |
| |
| class dummy: |
| """A dummy class for attaching attributes.""" |
| pass |
| |
| doctype = dummy( ) |
| doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">""" |
| doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">""" |
| doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">""" |
| |
| class russell: |
| """A dummy class that contains anything.""" |
| |
| def __contains__( self, item ): |
| return True |
| |
| |
| class MarkupError( Exception ): |
| """All our exceptions subclass this.""" |
| def __str__( self ): |
| return self.message |
| |
| class ClosingError( MarkupError ): |
| def __init__( self, tag ): |
| self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag |
| |
| class OpeningError( MarkupError ): |
| def __init__( self, tag ): |
| self.message = "The element '%s' can not be opened." % tag |
| |
| class ArgumentError( MarkupError ): |
| def __init__( self, tag ): |
| self.message = "The element '%s' was called with more than one non-keyword argument." % tag |
| |
| class InvalidElementError( MarkupError ): |
| def __init__( self, tag, mode ): |
| self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode ) |
| |
| class DeprecationError( MarkupError ): |
| def __init__( self, tag ): |
| self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag |
| |
| class ModeError( MarkupError ): |
| def __init__( self, mode ): |
| self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode |
| |
| class CustomizationError( MarkupError ): |
| def __init__( self ): |
| self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'." |
| |
| if __name__ == '__main__': |
| import sys |
| sys.stdout.write( __doc__ ) |