2.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000
2.2 +++ b/markup.py Sun Oct 26 00:45:58 2014 +0200
2.3 @@ -0,0 +1,527 @@
2.4 +# This code is in the public domain, it comes
2.5 +# with absolutely no warranty and you can do
2.6 +# absolutely whatever you want with it.
2.7 +
2.8 +__date__ = '1 October 2012'
2.9 +__version__ = '1.9'
2.10 +__doc__= """
2.11 +This is markup.py - a Python module that attempts to
2.12 +make it easier to generate HTML/XML from a Python program
2.13 +in an intuitive, lightweight, customizable and pythonic way.
2.14 +
2.15 +The code is in the public domain.
2.16 +
2.17 +Version: %s as of %s.
2.18 +
2.19 +Documentation and further info is at http://markup.sourceforge.net/
2.20 +
2.21 +Please send bug reports, feature requests, enhancement
2.22 +ideas or questions to nogradi at gmail dot com.
2.23 +
2.24 +Installation: drop markup.py somewhere into your Python path.
2.25 +""" % ( __version__, __date__ )
2.26 +
2.27 +try:
2.28 + basestring
2.29 + import string
2.30 +except:
2.31 + # python 3
2.32 + basestring = str
2.33 + string = str
2.34 +
2.35 +# tags which are reserved python keywords will be referred
2.36 +# to by a leading underscore otherwise we end up with a syntax error
2.37 +import keyword
2.38 +
2.39 +class element:
2.40 + """This class handles the addition of a new element."""
2.41 +
2.42 + def __init__( self, tag, case='lower', parent=None ):
2.43 + self.parent = parent
2.44 +
2.45 + if case == 'upper':
2.46 + self.tag = tag.upper( )
2.47 + elif case == 'lower':
2.48 + self.tag = tag.lower( )
2.49 + elif case =='given':
2.50 + self.tag = tag
2.51 + else:
2.52 + self.tag = tag
2.53 +
2.54 + def __call__( self, *args, **kwargs ):
2.55 + if len( args ) > 1:
2.56 + raise ArgumentError( self.tag )
2.57 +
2.58 + # if class_ was defined in parent it should be added to every element
2.59 + if self.parent is not None and self.parent.class_ is not None:
2.60 + if 'class_' not in kwargs:
2.61 + kwargs['class_'] = self.parent.class_
2.62 +
2.63 + if self.parent is None and len( args ) == 1:
2.64 + x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
2.65 + return '\n'.join( x )
2.66 + elif self.parent is None and len( args ) == 0:
2.67 + x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ]
2.68 + return '\n'.join( x )
2.69 +
2.70 + if self.tag in self.parent.twotags:
2.71 + for myarg, mydict in _argsdicts( args, kwargs ):
2.72 + self.render( self.tag, False, myarg, mydict )
2.73 + elif self.tag in self.parent.onetags:
2.74 + if len( args ) == 0:
2.75 + for myarg, mydict in _argsdicts( args, kwargs ):
2.76 + self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0
2.77 + else:
2.78 + raise ClosingError( self.tag )
2.79 + elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
2.80 + raise DeprecationError( self.tag )
2.81 + else:
2.82 + raise InvalidElementError( self.tag, self.parent.mode )
2.83 +
2.84 + def render( self, tag, single, between, kwargs ):
2.85 + """Append the actual tags to content."""
2.86 +
2.87 + out = "<%s" % tag
2.88 + for key, value in list( kwargs.items( ) ):
2.89 + if value is not None: # when value is None that means stuff like <... checked>
2.90 + key = key.strip('_') # strip this so class_ will mean class, etc.
2.91 + if key == 'http_equiv': # special cases, maybe change _ to - overall?
2.92 + key = 'http-equiv'
2.93 + elif key == 'accept_charset':
2.94 + key = 'accept-charset'
2.95 + out = "%s %s=\"%s\"" % ( out, key, escape( value ) )
2.96 + else:
2.97 + out = "%s %s" % ( out, key )
2.98 + if between is not None:
2.99 + out = "%s>%s</%s>" % ( out, between, tag )
2.100 + else:
2.101 + if single:
2.102 + out = "%s />" % out
2.103 + else:
2.104 + out = "%s>" % out
2.105 + if self.parent is not None:
2.106 + self.parent.content.append( out )
2.107 + else:
2.108 + return out
2.109 +
2.110 + def close( self ):
2.111 + """Append a closing tag unless element has only opening tag."""
2.112 +
2.113 + if self.tag in self.parent.twotags:
2.114 + self.parent.content.append( "</%s>" % self.tag )
2.115 + elif self.tag in self.parent.onetags:
2.116 + raise ClosingError( self.tag )
2.117 + elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags:
2.118 + raise DeprecationError( self.tag )
2.119 +
2.120 + def open( self, **kwargs ):
2.121 + """Append an opening tag."""
2.122 +
2.123 + if self.tag in self.parent.twotags or self.tag in self.parent.onetags:
2.124 + self.render( self.tag, False, None, kwargs )
2.125 + elif self.mode == 'strict_html' and self.tag in self.parent.deptags:
2.126 + raise DeprecationError( self.tag )
2.127 +
2.128 +class page:
2.129 + """This is our main class representing a document. Elements are added
2.130 + as attributes of an instance of this class."""
2.131 +
2.132 + def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ):
2.133 + """Stuff that effects the whole document.
2.134 +
2.135 + mode -- 'strict_html' for HTML 4.01 (default)
2.136 + 'html' alias for 'strict_html'
2.137 + 'loose_html' to allow some deprecated elements
2.138 + 'xml' to allow arbitrary elements
2.139 +
2.140 + case -- 'lower' element names will be printed in lower case (default)
2.141 + 'upper' they will be printed in upper case
2.142 + 'given' element names will be printed as they are given
2.143 +
2.144 + onetags -- list or tuple of valid elements with opening tags only
2.145 + twotags -- list or tuple of valid elements with both opening and closing tags
2.146 + these two keyword arguments may be used to select
2.147 + the set of valid elements in 'xml' mode
2.148 + invalid elements will raise appropriate exceptions
2.149 +
2.150 + separator -- string to place between added elements, defaults to newline
2.151 +
2.152 + class_ -- a class that will be added to every element if defined"""
2.153 +
2.154 + valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ]
2.155 + valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON",
2.156 + "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET",
2.157 + "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS",
2.158 + "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP",
2.159 + "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE",
2.160 + "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR",
2.161 + "TT", "UL", "VAR" ]
2.162 + deprecated_onetags = [ "BASEFONT", "ISINDEX" ]
2.163 + deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ]
2.164 +
2.165 + self.header = [ ]
2.166 + self.content = [ ]
2.167 + self.footer = [ ]
2.168 + self.case = case
2.169 + self.separator = separator
2.170 +
2.171 + # init( ) sets it to True so we know that </body></html> has to be printed at the end
2.172 + self._full = False
2.173 + self.class_= class_
2.174 +
2.175 + if mode == 'strict_html' or mode == 'html':
2.176 + self.onetags = valid_onetags
2.177 + self.onetags += list( map( string.lower, self.onetags ) )
2.178 + self.twotags = valid_twotags
2.179 + self.twotags += list( map( string.lower, self.twotags ) )
2.180 + self.deptags = deprecated_onetags + deprecated_twotags
2.181 + self.deptags += list( map( string.lower, self.deptags ) )
2.182 + self.mode = 'strict_html'
2.183 + elif mode == 'loose_html':
2.184 + self.onetags = valid_onetags + deprecated_onetags
2.185 + self.onetags += list( map( string.lower, self.onetags ) )
2.186 + self.twotags = valid_twotags + deprecated_twotags
2.187 + self.twotags += list( map( string.lower, self.twotags ) )
2.188 + self.mode = mode
2.189 + elif mode == 'xml':
2.190 + if onetags and twotags:
2.191 + self.onetags = onetags
2.192 + self.twotags = twotags
2.193 + elif ( onetags and not twotags ) or ( twotags and not onetags ):
2.194 + raise CustomizationError( )
2.195 + else:
2.196 + self.onetags = russell( )
2.197 + self.twotags = russell( )
2.198 + self.mode = mode
2.199 + else:
2.200 + raise ModeError( mode )
2.201 +
2.202 + def __getattr__( self, attr ):
2.203 +
2.204 + # tags should start with double underscore
2.205 + if attr.startswith("__") and attr.endswith("__"):
2.206 + raise AttributeError( attr )
2.207 + # tag with single underscore should be a reserved keyword
2.208 + if attr.startswith( '_' ):
2.209 + attr = attr.lstrip( '_' )
2.210 + if attr not in keyword.kwlist:
2.211 + raise AttributeError( attr )
2.212 +
2.213 + return element( attr, case=self.case, parent=self )
2.214 +
2.215 + def __str__( self ):
2.216 +
2.217 + if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ):
2.218 + end = [ '</body>', '</html>' ]
2.219 + else:
2.220 + end = [ ]
2.221 +
2.222 + return self.separator.join( self.header + self.content + self.footer + end )
2.223 +
2.224 + def __call__( self, escape=False ):
2.225 + """Return the document as a string.
2.226 +
2.227 + escape -- False print normally
2.228 + True replace < and > by < and >
2.229 + the default escape sequences in most browsers"""
2.230 +
2.231 + if escape:
2.232 + return _escape( self.__str__( ) )
2.233 + else:
2.234 + return self.__str__( )
2.235 +
2.236 + def add( self, text ):
2.237 + """This is an alias to addcontent."""
2.238 + self.addcontent( text )
2.239 +
2.240 + def addfooter( self, text ):
2.241 + """Add some text to the bottom of the document"""
2.242 + self.footer.append( text )
2.243 +
2.244 + def addheader( self, text ):
2.245 + """Add some text to the top of the document"""
2.246 + self.header.append( text )
2.247 +
2.248 + def addcontent( self, text ):
2.249 + """Add some text to the main part of the document"""
2.250 + self.content.append( text )
2.251 +
2.252 +
2.253 + def init( self, lang='en', css=None, metainfo=None, title=None, header=None,
2.254 + footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ):
2.255 + """This method is used for complete documents with appropriate
2.256 + doctype, encoding, title, etc information. For an HTML/XML snippet
2.257 + omit this method.
2.258 +
2.259 + lang -- language, usually a two character string, will appear
2.260 + as <html lang='en'> in html mode (ignored in xml mode)
2.261 +
2.262 + css -- Cascading Style Sheet filename as a string or a list of
2.263 + strings for multiple css files (ignored in xml mode)
2.264 +
2.265 + metainfo -- a dictionary in the form { 'name':'content' } to be inserted
2.266 + into meta element(s) as <meta name='name' content='content'>
2.267 + (ignored in xml mode)
2.268 +
2.269 + base -- set the <base href="..."> tag in <head>
2.270 +
2.271 + bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added
2.272 + as attributes of the <body> element as <body key='value' ... >
2.273 + (ignored in xml mode)
2.274 +
2.275 + script -- dictionary containing src:type pairs, <script type='text/type' src=src></script>
2.276 + or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all
2.277 +
2.278 + title -- the title of the document as a string to be inserted into
2.279 + a title element as <title>my title</title> (ignored in xml mode)
2.280 +
2.281 + header -- some text to be inserted right after the <body> element
2.282 + (ignored in xml mode)
2.283 +
2.284 + footer -- some text to be inserted right before the </body> element
2.285 + (ignored in xml mode)
2.286 +
2.287 + charset -- a string defining the character set, will be inserted into a
2.288 + <meta http-equiv='Content-Type' content='text/html; charset=myset'>
2.289 + element (ignored in xml mode)
2.290 +
2.291 + encoding -- a string defining the encoding, will be put into to first line of
2.292 + the document as <?xml version='1.0' encoding='myencoding' ?> in
2.293 + xml mode (ignored in html mode)
2.294 +
2.295 + doctype -- the document type string, defaults to
2.296 + <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>
2.297 + in html mode (ignored in xml mode)"""
2.298 +
2.299 + self._full = True
2.300 +
2.301 + if self.mode == 'strict_html' or self.mode == 'loose_html':
2.302 + if doctype is None:
2.303 + doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>"
2.304 + self.header.append( doctype )
2.305 + self.html( lang=lang )
2.306 + self.head( )
2.307 + if charset is not None:
2.308 + self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset )
2.309 + if metainfo is not None:
2.310 + self.metainfo( metainfo )
2.311 + if css is not None:
2.312 + self.css( css )
2.313 + if title is not None:
2.314 + self.title( title )
2.315 + if script is not None:
2.316 + self.scripts( script )
2.317 + if base is not None:
2.318 + self.base( href='%s' % base )
2.319 + self.head.close()
2.320 + if bodyattrs is not None:
2.321 + self.body( **bodyattrs )
2.322 + else:
2.323 + self.body( )
2.324 + if header is not None:
2.325 + self.content.append( header )
2.326 + if footer is not None:
2.327 + self.footer.append( footer )
2.328 +
2.329 + elif self.mode == 'xml':
2.330 + if doctype is None:
2.331 + if encoding is not None:
2.332 + doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding
2.333 + else:
2.334 + doctype = "<?xml version='1.0' ?>"
2.335 + self.header.append( doctype )
2.336 +
2.337 + def css( self, filelist ):
2.338 + """This convenience function is only useful for html.
2.339 + It adds css stylesheet(s) to the document via the <link> element."""
2.340 +
2.341 + if isinstance( filelist, basestring ):
2.342 + self.link( href=filelist, rel='stylesheet', type='text/css', media='all' )
2.343 + else:
2.344 + for file in filelist:
2.345 + self.link( href=file, rel='stylesheet', type='text/css', media='all' )
2.346 +
2.347 + def metainfo( self, mydict ):
2.348 + """This convenience function is only useful for html.
2.349 + It adds meta information via the <meta> element, the argument is
2.350 + a dictionary of the form { 'name':'content' }."""
2.351 +
2.352 + if isinstance( mydict, dict ):
2.353 + for name, content in list( mydict.items( ) ):
2.354 + self.meta( name=name, content=content )
2.355 + else:
2.356 + raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." )
2.357 +
2.358 + def scripts( self, mydict ):
2.359 + """Only useful in html, mydict is dictionary of src:type pairs or a list
2.360 + of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type.
2.361 + Will be rendered as <script type='text/type' src=src></script>"""
2.362 +
2.363 + if isinstance( mydict, dict ):
2.364 + for src, type in list( mydict.items( ) ):
2.365 + self.script( '', src=src, type='text/%s' % type )
2.366 + else:
2.367 + try:
2.368 + for src in mydict:
2.369 + self.script( '', src=src, type='text/javascript' )
2.370 + except:
2.371 + raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." )
2.372 +
2.373 +
2.374 +class _oneliner:
2.375 + """An instance of oneliner returns a string corresponding to one element.
2.376 + This class can be used to write 'oneliners' that return a string
2.377 + immediately so there is no need to instantiate the page class."""
2.378 +
2.379 + def __init__( self, case='lower' ):
2.380 + self.case = case
2.381 +
2.382 + def __getattr__( self, attr ):
2.383 +
2.384 + # tags should start with double underscore
2.385 + if attr.startswith("__") and attr.endswith("__"):
2.386 + raise AttributeError( attr )
2.387 + # tag with single underscore should be a reserved keyword
2.388 + if attr.startswith( '_' ):
2.389 + attr = attr.lstrip( '_' )
2.390 + if attr not in keyword.kwlist:
2.391 + raise AttributeError( attr )
2.392 +
2.393 + return element( attr, case=self.case, parent=None )
2.394 +
2.395 +oneliner = _oneliner( case='lower' )
2.396 +upper_oneliner = _oneliner( case='upper' )
2.397 +given_oneliner = _oneliner( case='given' )
2.398 +
2.399 +def _argsdicts( args, mydict ):
2.400 + """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1."""
2.401 +
2.402 + if len( args ) == 0:
2.403 + args = None,
2.404 + elif len( args ) == 1:
2.405 + args = _totuple( args[0] )
2.406 + else:
2.407 + raise Exception( "We should have never gotten here." )
2.408 +
2.409 + mykeys = list( mydict.keys( ) )
2.410 + myvalues = list( map( _totuple, list( mydict.values( ) ) ) )
2.411 +
2.412 + maxlength = max( list( map( len, [ args ] + myvalues ) ) )
2.413 +
2.414 + for i in range( maxlength ):
2.415 + thisdict = { }
2.416 + for key, value in zip( mykeys, myvalues ):
2.417 + try:
2.418 + thisdict[ key ] = value[i]
2.419 + except IndexError:
2.420 + thisdict[ key ] = value[-1]
2.421 + try:
2.422 + thisarg = args[i]
2.423 + except IndexError:
2.424 + thisarg = args[-1]
2.425 +
2.426 + yield thisarg, thisdict
2.427 +
2.428 +def _totuple( x ):
2.429 + """Utility stuff to convert string, int, long, float, None or anything to a usable tuple."""
2.430 +
2.431 + if isinstance( x, basestring ):
2.432 + out = x,
2.433 + elif isinstance( x, ( int, long, float ) ):
2.434 + out = str( x ),
2.435 + elif x is None:
2.436 + out = None,
2.437 + else:
2.438 + out = tuple( x )
2.439 +
2.440 + return out
2.441 +
2.442 +def escape( text, newline=False ):
2.443 + """Escape special html characters."""
2.444 +
2.445 + if isinstance( text, basestring ):
2.446 + if '&' in text:
2.447 + text = text.replace( '&', '&' )
2.448 + if '>' in text:
2.449 + text = text.replace( '>', '>' )
2.450 + if '<' in text:
2.451 + text = text.replace( '<', '<' )
2.452 + if '\"' in text:
2.453 + text = text.replace( '\"', '"' )
2.454 + if '\'' in text:
2.455 + text = text.replace( '\'', '"' )
2.456 + if newline:
2.457 + if '\n' in text:
2.458 + text = text.replace( '\n', '<br>' )
2.459 +
2.460 + return text
2.461 +
2.462 +_escape = escape
2.463 +
2.464 +def unescape( text ):
2.465 + """Inverse of escape."""
2.466 +
2.467 + if isinstance( text, basestring ):
2.468 + if '&' in text:
2.469 + text = text.replace( '&', '&' )
2.470 + if '>' in text:
2.471 + text = text.replace( '>', '>' )
2.472 + if '<' in text:
2.473 + text = text.replace( '<', '<' )
2.474 + if '"' in text:
2.475 + text = text.replace( '"', '\"' )
2.476 +
2.477 + return text
2.478 +
2.479 +class dummy:
2.480 + """A dummy class for attaching attributes."""
2.481 + pass
2.482 +
2.483 +doctype = dummy( )
2.484 +doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">"""
2.485 +doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">"""
2.486 +doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">"""
2.487 +
2.488 +class russell:
2.489 + """A dummy class that contains anything."""
2.490 +
2.491 + def __contains__( self, item ):
2.492 + return True
2.493 +
2.494 +
2.495 +class MarkupError( Exception ):
2.496 + """All our exceptions subclass this."""
2.497 + def __str__( self ):
2.498 + return self.message
2.499 +
2.500 +class ClosingError( MarkupError ):
2.501 + def __init__( self, tag ):
2.502 + self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag
2.503 +
2.504 +class OpeningError( MarkupError ):
2.505 + def __init__( self, tag ):
2.506 + self.message = "The element '%s' can not be opened." % tag
2.507 +
2.508 +class ArgumentError( MarkupError ):
2.509 + def __init__( self, tag ):
2.510 + self.message = "The element '%s' was called with more than one non-keyword argument." % tag
2.511 +
2.512 +class InvalidElementError( MarkupError ):
2.513 + def __init__( self, tag, mode ):
2.514 + self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode )
2.515 +
2.516 +class DeprecationError( MarkupError ):
2.517 + def __init__( self, tag ):
2.518 + self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag
2.519 +
2.520 +class ModeError( MarkupError ):
2.521 + def __init__( self, mode ):
2.522 + self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode
2.523 +
2.524 +class CustomizationError( MarkupError ):
2.525 + def __init__( self ):
2.526 + self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'."
2.527 +
2.528 +if __name__ == '__main__':
2.529 + import sys
2.530 + sys.stdout.write( __doc__ )