paul@69 | 1 | # This code is in the public domain, it comes |
paul@69 | 2 | # with absolutely no warranty and you can do |
paul@69 | 3 | # absolutely whatever you want with it. |
paul@69 | 4 | |
paul@1116 | 5 | __date__ = '16 March 2015' |
paul@1116 | 6 | __version__ = '1.10' |
paul@69 | 7 | __doc__= """ |
paul@69 | 8 | This is markup.py - a Python module that attempts to |
paul@69 | 9 | make it easier to generate HTML/XML from a Python program |
paul@69 | 10 | in an intuitive, lightweight, customizable and pythonic way. |
paul@1116 | 11 | It works with both python 2 and 3. |
paul@69 | 12 | |
paul@69 | 13 | The code is in the public domain. |
paul@69 | 14 | |
paul@69 | 15 | Version: %s as of %s. |
paul@69 | 16 | |
paul@69 | 17 | Documentation and further info is at http://markup.sourceforge.net/ |
paul@69 | 18 | |
paul@69 | 19 | Please send bug reports, feature requests, enhancement |
paul@69 | 20 | ideas or questions to nogradi at gmail dot com. |
paul@69 | 21 | |
paul@69 | 22 | Installation: drop markup.py somewhere into your Python path. |
paul@69 | 23 | """ % ( __version__, __date__ ) |
paul@69 | 24 | |
paul@69 | 25 | try: |
paul@69 | 26 | basestring |
paul@69 | 27 | import string |
paul@69 | 28 | except: |
paul@69 | 29 | # python 3 |
paul@69 | 30 | basestring = str |
paul@69 | 31 | string = str |
paul@1116 | 32 | long = int |
paul@69 | 33 | |
paul@69 | 34 | # tags which are reserved python keywords will be referred |
paul@69 | 35 | # to by a leading underscore otherwise we end up with a syntax error |
paul@69 | 36 | import keyword |
paul@69 | 37 | |
paul@69 | 38 | class element: |
paul@69 | 39 | """This class handles the addition of a new element.""" |
paul@69 | 40 | |
paul@69 | 41 | def __init__( self, tag, case='lower', parent=None ): |
paul@69 | 42 | self.parent = parent |
paul@69 | 43 | |
paul@69 | 44 | if case == 'upper': |
paul@69 | 45 | self.tag = tag.upper( ) |
paul@69 | 46 | elif case == 'lower': |
paul@69 | 47 | self.tag = tag.lower( ) |
paul@69 | 48 | elif case =='given': |
paul@69 | 49 | self.tag = tag |
paul@69 | 50 | else: |
paul@69 | 51 | self.tag = tag |
paul@69 | 52 | |
paul@69 | 53 | def __call__( self, *args, **kwargs ): |
paul@69 | 54 | if len( args ) > 1: |
paul@69 | 55 | raise ArgumentError( self.tag ) |
paul@69 | 56 | |
paul@69 | 57 | # if class_ was defined in parent it should be added to every element |
paul@69 | 58 | if self.parent is not None and self.parent.class_ is not None: |
paul@69 | 59 | if 'class_' not in kwargs: |
paul@69 | 60 | kwargs['class_'] = self.parent.class_ |
paul@69 | 61 | |
paul@69 | 62 | if self.parent is None and len( args ) == 1: |
paul@69 | 63 | x = [ self.render( self.tag, False, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] |
paul@69 | 64 | return '\n'.join( x ) |
paul@69 | 65 | elif self.parent is None and len( args ) == 0: |
paul@69 | 66 | x = [ self.render( self.tag, True, myarg, mydict ) for myarg, mydict in _argsdicts( args, kwargs ) ] |
paul@69 | 67 | return '\n'.join( x ) |
paul@69 | 68 | |
paul@69 | 69 | if self.tag in self.parent.twotags: |
paul@69 | 70 | for myarg, mydict in _argsdicts( args, kwargs ): |
paul@69 | 71 | self.render( self.tag, False, myarg, mydict ) |
paul@69 | 72 | elif self.tag in self.parent.onetags: |
paul@69 | 73 | if len( args ) == 0: |
paul@69 | 74 | for myarg, mydict in _argsdicts( args, kwargs ): |
paul@69 | 75 | self.render( self.tag, True, myarg, mydict ) # here myarg is always None, because len( args ) = 0 |
paul@69 | 76 | else: |
paul@69 | 77 | raise ClosingError( self.tag ) |
paul@69 | 78 | elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: |
paul@69 | 79 | raise DeprecationError( self.tag ) |
paul@69 | 80 | else: |
paul@69 | 81 | raise InvalidElementError( self.tag, self.parent.mode ) |
paul@69 | 82 | |
paul@69 | 83 | def render( self, tag, single, between, kwargs ): |
paul@69 | 84 | """Append the actual tags to content.""" |
paul@69 | 85 | |
paul@69 | 86 | out = "<%s" % tag |
paul@69 | 87 | for key, value in list( kwargs.items( ) ): |
paul@69 | 88 | if value is not None: # when value is None that means stuff like <... checked> |
paul@69 | 89 | key = key.strip('_') # strip this so class_ will mean class, etc. |
paul@69 | 90 | if key == 'http_equiv': # special cases, maybe change _ to - overall? |
paul@69 | 91 | key = 'http-equiv' |
paul@69 | 92 | elif key == 'accept_charset': |
paul@69 | 93 | key = 'accept-charset' |
paul@69 | 94 | out = "%s %s=\"%s\"" % ( out, key, escape( value ) ) |
paul@69 | 95 | else: |
paul@69 | 96 | out = "%s %s" % ( out, key ) |
paul@69 | 97 | if between is not None: |
paul@789 | 98 | out = "%s>%s</%s>" % ( out, escape( between ), tag ) |
paul@69 | 99 | else: |
paul@69 | 100 | if single: |
paul@69 | 101 | out = "%s />" % out |
paul@69 | 102 | else: |
paul@69 | 103 | out = "%s>" % out |
paul@69 | 104 | if self.parent is not None: |
paul@69 | 105 | self.parent.content.append( out ) |
paul@69 | 106 | else: |
paul@69 | 107 | return out |
paul@69 | 108 | |
paul@69 | 109 | def close( self ): |
paul@69 | 110 | """Append a closing tag unless element has only opening tag.""" |
paul@69 | 111 | |
paul@69 | 112 | if self.tag in self.parent.twotags: |
paul@69 | 113 | self.parent.content.append( "</%s>" % self.tag ) |
paul@69 | 114 | elif self.tag in self.parent.onetags: |
paul@69 | 115 | raise ClosingError( self.tag ) |
paul@69 | 116 | elif self.parent.mode == 'strict_html' and self.tag in self.parent.deptags: |
paul@69 | 117 | raise DeprecationError( self.tag ) |
paul@69 | 118 | |
paul@69 | 119 | def open( self, **kwargs ): |
paul@69 | 120 | """Append an opening tag.""" |
paul@69 | 121 | |
paul@69 | 122 | if self.tag in self.parent.twotags or self.tag in self.parent.onetags: |
paul@69 | 123 | self.render( self.tag, False, None, kwargs ) |
paul@69 | 124 | elif self.mode == 'strict_html' and self.tag in self.parent.deptags: |
paul@69 | 125 | raise DeprecationError( self.tag ) |
paul@69 | 126 | |
paul@69 | 127 | class page: |
paul@69 | 128 | """This is our main class representing a document. Elements are added |
paul@69 | 129 | as attributes of an instance of this class.""" |
paul@69 | 130 | |
paul@69 | 131 | def __init__( self, mode='strict_html', case='lower', onetags=None, twotags=None, separator='\n', class_=None ): |
paul@69 | 132 | """Stuff that effects the whole document. |
paul@69 | 133 | |
paul@69 | 134 | mode -- 'strict_html' for HTML 4.01 (default) |
paul@69 | 135 | 'html' alias for 'strict_html' |
paul@69 | 136 | 'loose_html' to allow some deprecated elements |
paul@69 | 137 | 'xml' to allow arbitrary elements |
paul@69 | 138 | |
paul@69 | 139 | case -- 'lower' element names will be printed in lower case (default) |
paul@69 | 140 | 'upper' they will be printed in upper case |
paul@69 | 141 | 'given' element names will be printed as they are given |
paul@69 | 142 | |
paul@69 | 143 | onetags -- list or tuple of valid elements with opening tags only |
paul@69 | 144 | twotags -- list or tuple of valid elements with both opening and closing tags |
paul@69 | 145 | these two keyword arguments may be used to select |
paul@69 | 146 | the set of valid elements in 'xml' mode |
paul@69 | 147 | invalid elements will raise appropriate exceptions |
paul@69 | 148 | |
paul@69 | 149 | separator -- string to place between added elements, defaults to newline |
paul@69 | 150 | |
paul@69 | 151 | class_ -- a class that will be added to every element if defined""" |
paul@69 | 152 | |
paul@69 | 153 | valid_onetags = [ "AREA", "BASE", "BR", "COL", "FRAME", "HR", "IMG", "INPUT", "LINK", "META", "PARAM" ] |
paul@69 | 154 | valid_twotags = [ "A", "ABBR", "ACRONYM", "ADDRESS", "B", "BDO", "BIG", "BLOCKQUOTE", "BODY", "BUTTON", |
paul@69 | 155 | "CAPTION", "CITE", "CODE", "COLGROUP", "DD", "DEL", "DFN", "DIV", "DL", "DT", "EM", "FIELDSET", |
paul@69 | 156 | "FORM", "FRAMESET", "H1", "H2", "H3", "H4", "H5", "H6", "HEAD", "HTML", "I", "IFRAME", "INS", |
paul@69 | 157 | "KBD", "LABEL", "LEGEND", "LI", "MAP", "NOFRAMES", "NOSCRIPT", "OBJECT", "OL", "OPTGROUP", |
paul@69 | 158 | "OPTION", "P", "PRE", "Q", "SAMP", "SCRIPT", "SELECT", "SMALL", "SPAN", "STRONG", "STYLE", |
paul@69 | 159 | "SUB", "SUP", "TABLE", "TBODY", "TD", "TEXTAREA", "TFOOT", "TH", "THEAD", "TITLE", "TR", |
paul@69 | 160 | "TT", "UL", "VAR" ] |
paul@69 | 161 | deprecated_onetags = [ "BASEFONT", "ISINDEX" ] |
paul@69 | 162 | deprecated_twotags = [ "APPLET", "CENTER", "DIR", "FONT", "MENU", "S", "STRIKE", "U" ] |
paul@69 | 163 | |
paul@69 | 164 | self.header = [ ] |
paul@69 | 165 | self.content = [ ] |
paul@69 | 166 | self.footer = [ ] |
paul@69 | 167 | self.case = case |
paul@69 | 168 | self.separator = separator |
paul@69 | 169 | |
paul@69 | 170 | # init( ) sets it to True so we know that </body></html> has to be printed at the end |
paul@69 | 171 | self._full = False |
paul@69 | 172 | self.class_= class_ |
paul@69 | 173 | |
paul@69 | 174 | if mode == 'strict_html' or mode == 'html': |
paul@69 | 175 | self.onetags = valid_onetags |
paul@69 | 176 | self.onetags += list( map( string.lower, self.onetags ) ) |
paul@69 | 177 | self.twotags = valid_twotags |
paul@69 | 178 | self.twotags += list( map( string.lower, self.twotags ) ) |
paul@69 | 179 | self.deptags = deprecated_onetags + deprecated_twotags |
paul@69 | 180 | self.deptags += list( map( string.lower, self.deptags ) ) |
paul@69 | 181 | self.mode = 'strict_html' |
paul@69 | 182 | elif mode == 'loose_html': |
paul@69 | 183 | self.onetags = valid_onetags + deprecated_onetags |
paul@69 | 184 | self.onetags += list( map( string.lower, self.onetags ) ) |
paul@69 | 185 | self.twotags = valid_twotags + deprecated_twotags |
paul@69 | 186 | self.twotags += list( map( string.lower, self.twotags ) ) |
paul@69 | 187 | self.mode = mode |
paul@69 | 188 | elif mode == 'xml': |
paul@69 | 189 | if onetags and twotags: |
paul@69 | 190 | self.onetags = onetags |
paul@69 | 191 | self.twotags = twotags |
paul@69 | 192 | elif ( onetags and not twotags ) or ( twotags and not onetags ): |
paul@69 | 193 | raise CustomizationError( ) |
paul@69 | 194 | else: |
paul@69 | 195 | self.onetags = russell( ) |
paul@69 | 196 | self.twotags = russell( ) |
paul@69 | 197 | self.mode = mode |
paul@69 | 198 | else: |
paul@69 | 199 | raise ModeError( mode ) |
paul@69 | 200 | |
paul@69 | 201 | def __getattr__( self, attr ): |
paul@69 | 202 | |
paul@69 | 203 | # tags should start with double underscore |
paul@69 | 204 | if attr.startswith("__") and attr.endswith("__"): |
paul@69 | 205 | raise AttributeError( attr ) |
paul@69 | 206 | # tag with single underscore should be a reserved keyword |
paul@69 | 207 | if attr.startswith( '_' ): |
paul@69 | 208 | attr = attr.lstrip( '_' ) |
paul@69 | 209 | if attr not in keyword.kwlist: |
paul@69 | 210 | raise AttributeError( attr ) |
paul@69 | 211 | |
paul@69 | 212 | return element( attr, case=self.case, parent=self ) |
paul@69 | 213 | |
paul@69 | 214 | def __str__( self ): |
paul@69 | 215 | |
paul@69 | 216 | if self._full and ( self.mode == 'strict_html' or self.mode == 'loose_html' ): |
paul@69 | 217 | end = [ '</body>', '</html>' ] |
paul@69 | 218 | else: |
paul@69 | 219 | end = [ ] |
paul@69 | 220 | |
paul@69 | 221 | return self.separator.join( self.header + self.content + self.footer + end ) |
paul@69 | 222 | |
paul@69 | 223 | def __call__( self, escape=False ): |
paul@69 | 224 | """Return the document as a string. |
paul@69 | 225 | |
paul@69 | 226 | escape -- False print normally |
paul@69 | 227 | True replace < and > by < and > |
paul@69 | 228 | the default escape sequences in most browsers""" |
paul@69 | 229 | |
paul@69 | 230 | if escape: |
paul@69 | 231 | return _escape( self.__str__( ) ) |
paul@69 | 232 | else: |
paul@69 | 233 | return self.__str__( ) |
paul@69 | 234 | |
paul@69 | 235 | def add( self, text ): |
paul@69 | 236 | """This is an alias to addcontent.""" |
paul@69 | 237 | self.addcontent( text ) |
paul@69 | 238 | |
paul@69 | 239 | def addfooter( self, text ): |
paul@69 | 240 | """Add some text to the bottom of the document""" |
paul@793 | 241 | self.footer.append( escape( text ) ) |
paul@69 | 242 | |
paul@69 | 243 | def addheader( self, text ): |
paul@69 | 244 | """Add some text to the top of the document""" |
paul@793 | 245 | self.header.append( escape( text ) ) |
paul@69 | 246 | |
paul@69 | 247 | def addcontent( self, text ): |
paul@69 | 248 | """Add some text to the main part of the document""" |
paul@793 | 249 | self.content.append( escape( text ) ) |
paul@69 | 250 | |
paul@69 | 251 | |
paul@69 | 252 | def init( self, lang='en', css=None, metainfo=None, title=None, header=None, |
paul@69 | 253 | footer=None, charset=None, encoding=None, doctype=None, bodyattrs=None, script=None, base=None ): |
paul@69 | 254 | """This method is used for complete documents with appropriate |
paul@69 | 255 | doctype, encoding, title, etc information. For an HTML/XML snippet |
paul@69 | 256 | omit this method. |
paul@69 | 257 | |
paul@69 | 258 | lang -- language, usually a two character string, will appear |
paul@69 | 259 | as <html lang='en'> in html mode (ignored in xml mode) |
paul@69 | 260 | |
paul@69 | 261 | css -- Cascading Style Sheet filename as a string or a list of |
paul@69 | 262 | strings for multiple css files (ignored in xml mode) |
paul@69 | 263 | |
paul@69 | 264 | metainfo -- a dictionary in the form { 'name':'content' } to be inserted |
paul@69 | 265 | into meta element(s) as <meta name='name' content='content'> |
paul@69 | 266 | (ignored in xml mode) |
paul@69 | 267 | |
paul@69 | 268 | base -- set the <base href="..."> tag in <head> |
paul@69 | 269 | |
paul@69 | 270 | bodyattrs --a dictionary in the form { 'key':'value', ... } which will be added |
paul@69 | 271 | as attributes of the <body> element as <body key='value' ... > |
paul@69 | 272 | (ignored in xml mode) |
paul@69 | 273 | |
paul@69 | 274 | script -- dictionary containing src:type pairs, <script type='text/type' src=src></script> |
paul@69 | 275 | or a list of [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for all |
paul@69 | 276 | |
paul@69 | 277 | title -- the title of the document as a string to be inserted into |
paul@69 | 278 | a title element as <title>my title</title> (ignored in xml mode) |
paul@69 | 279 | |
paul@69 | 280 | header -- some text to be inserted right after the <body> element |
paul@69 | 281 | (ignored in xml mode) |
paul@69 | 282 | |
paul@69 | 283 | footer -- some text to be inserted right before the </body> element |
paul@69 | 284 | (ignored in xml mode) |
paul@69 | 285 | |
paul@69 | 286 | charset -- a string defining the character set, will be inserted into a |
paul@69 | 287 | <meta http-equiv='Content-Type' content='text/html; charset=myset'> |
paul@69 | 288 | element (ignored in xml mode) |
paul@69 | 289 | |
paul@69 | 290 | encoding -- a string defining the encoding, will be put into to first line of |
paul@69 | 291 | the document as <?xml version='1.0' encoding='myencoding' ?> in |
paul@69 | 292 | xml mode (ignored in html mode) |
paul@69 | 293 | |
paul@69 | 294 | doctype -- the document type string, defaults to |
paul@69 | 295 | <!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'> |
paul@69 | 296 | in html mode (ignored in xml mode)""" |
paul@69 | 297 | |
paul@69 | 298 | self._full = True |
paul@69 | 299 | |
paul@69 | 300 | if self.mode == 'strict_html' or self.mode == 'loose_html': |
paul@69 | 301 | if doctype is None: |
paul@69 | 302 | doctype = "<!DOCTYPE HTML PUBLIC '-//W3C//DTD HTML 4.01 Transitional//EN'>" |
paul@69 | 303 | self.header.append( doctype ) |
paul@69 | 304 | self.html( lang=lang ) |
paul@69 | 305 | self.head( ) |
paul@69 | 306 | if charset is not None: |
paul@69 | 307 | self.meta( http_equiv='Content-Type', content="text/html; charset=%s" % charset ) |
paul@69 | 308 | if metainfo is not None: |
paul@69 | 309 | self.metainfo( metainfo ) |
paul@69 | 310 | if css is not None: |
paul@69 | 311 | self.css( css ) |
paul@69 | 312 | if title is not None: |
paul@69 | 313 | self.title( title ) |
paul@69 | 314 | if script is not None: |
paul@69 | 315 | self.scripts( script ) |
paul@69 | 316 | if base is not None: |
paul@69 | 317 | self.base( href='%s' % base ) |
paul@69 | 318 | self.head.close() |
paul@69 | 319 | if bodyattrs is not None: |
paul@69 | 320 | self.body( **bodyattrs ) |
paul@69 | 321 | else: |
paul@69 | 322 | self.body( ) |
paul@69 | 323 | if header is not None: |
paul@69 | 324 | self.content.append( header ) |
paul@69 | 325 | if footer is not None: |
paul@69 | 326 | self.footer.append( footer ) |
paul@69 | 327 | |
paul@69 | 328 | elif self.mode == 'xml': |
paul@69 | 329 | if doctype is None: |
paul@69 | 330 | if encoding is not None: |
paul@69 | 331 | doctype = "<?xml version='1.0' encoding='%s' ?>" % encoding |
paul@69 | 332 | else: |
paul@69 | 333 | doctype = "<?xml version='1.0' ?>" |
paul@69 | 334 | self.header.append( doctype ) |
paul@69 | 335 | |
paul@69 | 336 | def css( self, filelist ): |
paul@69 | 337 | """This convenience function is only useful for html. |
paul@69 | 338 | It adds css stylesheet(s) to the document via the <link> element.""" |
paul@69 | 339 | |
paul@69 | 340 | if isinstance( filelist, basestring ): |
paul@69 | 341 | self.link( href=filelist, rel='stylesheet', type='text/css', media='all' ) |
paul@69 | 342 | else: |
paul@69 | 343 | for file in filelist: |
paul@69 | 344 | self.link( href=file, rel='stylesheet', type='text/css', media='all' ) |
paul@69 | 345 | |
paul@69 | 346 | def metainfo( self, mydict ): |
paul@69 | 347 | """This convenience function is only useful for html. |
paul@69 | 348 | It adds meta information via the <meta> element, the argument is |
paul@69 | 349 | a dictionary of the form { 'name':'content' }.""" |
paul@69 | 350 | |
paul@69 | 351 | if isinstance( mydict, dict ): |
paul@69 | 352 | for name, content in list( mydict.items( ) ): |
paul@69 | 353 | self.meta( name=name, content=content ) |
paul@69 | 354 | else: |
paul@69 | 355 | raise TypeError( "Metainfo should be called with a dictionary argument of name:content pairs." ) |
paul@69 | 356 | |
paul@69 | 357 | def scripts( self, mydict ): |
paul@69 | 358 | """Only useful in html, mydict is dictionary of src:type pairs or a list |
paul@69 | 359 | of script sources [ 'src1', 'src2', ... ] in which case 'javascript' is assumed for type. |
paul@69 | 360 | Will be rendered as <script type='text/type' src=src></script>""" |
paul@69 | 361 | |
paul@69 | 362 | if isinstance( mydict, dict ): |
paul@69 | 363 | for src, type in list( mydict.items( ) ): |
paul@69 | 364 | self.script( '', src=src, type='text/%s' % type ) |
paul@69 | 365 | else: |
paul@69 | 366 | try: |
paul@69 | 367 | for src in mydict: |
paul@69 | 368 | self.script( '', src=src, type='text/javascript' ) |
paul@69 | 369 | except: |
paul@69 | 370 | raise TypeError( "Script should be given a dictionary of src:type pairs or a list of javascript src's." ) |
paul@69 | 371 | |
paul@69 | 372 | |
paul@69 | 373 | class _oneliner: |
paul@69 | 374 | """An instance of oneliner returns a string corresponding to one element. |
paul@69 | 375 | This class can be used to write 'oneliners' that return a string |
paul@69 | 376 | immediately so there is no need to instantiate the page class.""" |
paul@69 | 377 | |
paul@69 | 378 | def __init__( self, case='lower' ): |
paul@69 | 379 | self.case = case |
paul@69 | 380 | |
paul@69 | 381 | def __getattr__( self, attr ): |
paul@69 | 382 | |
paul@69 | 383 | # tags should start with double underscore |
paul@69 | 384 | if attr.startswith("__") and attr.endswith("__"): |
paul@69 | 385 | raise AttributeError( attr ) |
paul@69 | 386 | # tag with single underscore should be a reserved keyword |
paul@69 | 387 | if attr.startswith( '_' ): |
paul@69 | 388 | attr = attr.lstrip( '_' ) |
paul@69 | 389 | if attr not in keyword.kwlist: |
paul@69 | 390 | raise AttributeError( attr ) |
paul@69 | 391 | |
paul@69 | 392 | return element( attr, case=self.case, parent=None ) |
paul@69 | 393 | |
paul@69 | 394 | oneliner = _oneliner( case='lower' ) |
paul@69 | 395 | upper_oneliner = _oneliner( case='upper' ) |
paul@69 | 396 | given_oneliner = _oneliner( case='given' ) |
paul@69 | 397 | |
paul@69 | 398 | def _argsdicts( args, mydict ): |
paul@69 | 399 | """A utility generator that pads argument list and dictionary values, will only be called with len( args ) = 0, 1.""" |
paul@69 | 400 | |
paul@69 | 401 | if len( args ) == 0: |
paul@69 | 402 | args = None, |
paul@69 | 403 | elif len( args ) == 1: |
paul@69 | 404 | args = _totuple( args[0] ) |
paul@69 | 405 | else: |
paul@69 | 406 | raise Exception( "We should have never gotten here." ) |
paul@69 | 407 | |
paul@69 | 408 | mykeys = list( mydict.keys( ) ) |
paul@69 | 409 | myvalues = list( map( _totuple, list( mydict.values( ) ) ) ) |
paul@69 | 410 | |
paul@69 | 411 | maxlength = max( list( map( len, [ args ] + myvalues ) ) ) |
paul@69 | 412 | |
paul@69 | 413 | for i in range( maxlength ): |
paul@69 | 414 | thisdict = { } |
paul@69 | 415 | for key, value in zip( mykeys, myvalues ): |
paul@69 | 416 | try: |
paul@69 | 417 | thisdict[ key ] = value[i] |
paul@69 | 418 | except IndexError: |
paul@69 | 419 | thisdict[ key ] = value[-1] |
paul@69 | 420 | try: |
paul@69 | 421 | thisarg = args[i] |
paul@69 | 422 | except IndexError: |
paul@69 | 423 | thisarg = args[-1] |
paul@69 | 424 | |
paul@69 | 425 | yield thisarg, thisdict |
paul@69 | 426 | |
paul@69 | 427 | def _totuple( x ): |
paul@69 | 428 | """Utility stuff to convert string, int, long, float, None or anything to a usable tuple.""" |
paul@69 | 429 | |
paul@69 | 430 | if isinstance( x, basestring ): |
paul@69 | 431 | out = x, |
paul@69 | 432 | elif isinstance( x, ( int, long, float ) ): |
paul@69 | 433 | out = str( x ), |
paul@69 | 434 | elif x is None: |
paul@69 | 435 | out = None, |
paul@69 | 436 | else: |
paul@69 | 437 | out = tuple( x ) |
paul@69 | 438 | |
paul@69 | 439 | return out |
paul@69 | 440 | |
paul@69 | 441 | def escape( text, newline=False ): |
paul@69 | 442 | """Escape special html characters.""" |
paul@69 | 443 | |
paul@69 | 444 | if isinstance( text, basestring ): |
paul@69 | 445 | if '&' in text: |
paul@69 | 446 | text = text.replace( '&', '&' ) |
paul@69 | 447 | if '>' in text: |
paul@69 | 448 | text = text.replace( '>', '>' ) |
paul@69 | 449 | if '<' in text: |
paul@69 | 450 | text = text.replace( '<', '<' ) |
paul@69 | 451 | if '\"' in text: |
paul@69 | 452 | text = text.replace( '\"', '"' ) |
paul@69 | 453 | if '\'' in text: |
paul@270 | 454 | text = text.replace( '\'', ''' ) |
paul@69 | 455 | if newline: |
paul@69 | 456 | if '\n' in text: |
paul@69 | 457 | text = text.replace( '\n', '<br>' ) |
paul@69 | 458 | |
paul@69 | 459 | return text |
paul@69 | 460 | |
paul@69 | 461 | _escape = escape |
paul@69 | 462 | |
paul@69 | 463 | def unescape( text ): |
paul@69 | 464 | """Inverse of escape.""" |
paul@69 | 465 | |
paul@69 | 466 | if isinstance( text, basestring ): |
paul@69 | 467 | if '&' in text: |
paul@69 | 468 | text = text.replace( '&', '&' ) |
paul@69 | 469 | if '>' in text: |
paul@69 | 470 | text = text.replace( '>', '>' ) |
paul@69 | 471 | if '<' in text: |
paul@69 | 472 | text = text.replace( '<', '<' ) |
paul@69 | 473 | if '"' in text: |
paul@69 | 474 | text = text.replace( '"', '\"' ) |
paul@69 | 475 | |
paul@69 | 476 | return text |
paul@69 | 477 | |
paul@69 | 478 | class dummy: |
paul@69 | 479 | """A dummy class for attaching attributes.""" |
paul@69 | 480 | pass |
paul@69 | 481 | |
paul@69 | 482 | doctype = dummy( ) |
paul@69 | 483 | doctype.frameset = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">""" |
paul@69 | 484 | doctype.strict = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">""" |
paul@69 | 485 | doctype.loose = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">""" |
paul@69 | 486 | |
paul@69 | 487 | class russell: |
paul@69 | 488 | """A dummy class that contains anything.""" |
paul@69 | 489 | |
paul@69 | 490 | def __contains__( self, item ): |
paul@69 | 491 | return True |
paul@69 | 492 | |
paul@69 | 493 | |
paul@69 | 494 | class MarkupError( Exception ): |
paul@69 | 495 | """All our exceptions subclass this.""" |
paul@69 | 496 | def __str__( self ): |
paul@69 | 497 | return self.message |
paul@69 | 498 | |
paul@69 | 499 | class ClosingError( MarkupError ): |
paul@69 | 500 | def __init__( self, tag ): |
paul@69 | 501 | self.message = "The element '%s' does not accept non-keyword arguments (has no closing tag)." % tag |
paul@69 | 502 | |
paul@69 | 503 | class OpeningError( MarkupError ): |
paul@69 | 504 | def __init__( self, tag ): |
paul@69 | 505 | self.message = "The element '%s' can not be opened." % tag |
paul@69 | 506 | |
paul@69 | 507 | class ArgumentError( MarkupError ): |
paul@69 | 508 | def __init__( self, tag ): |
paul@69 | 509 | self.message = "The element '%s' was called with more than one non-keyword argument." % tag |
paul@69 | 510 | |
paul@69 | 511 | class InvalidElementError( MarkupError ): |
paul@69 | 512 | def __init__( self, tag, mode ): |
paul@69 | 513 | self.message = "The element '%s' is not valid for your mode '%s'." % ( tag, mode ) |
paul@69 | 514 | |
paul@69 | 515 | class DeprecationError( MarkupError ): |
paul@69 | 516 | def __init__( self, tag ): |
paul@69 | 517 | self.message = "The element '%s' is deprecated, instantiate markup.page with mode='loose_html' to allow it." % tag |
paul@69 | 518 | |
paul@69 | 519 | class ModeError( MarkupError ): |
paul@69 | 520 | def __init__( self, mode ): |
paul@69 | 521 | self.message = "Mode '%s' is invalid, possible values: strict_html, html (alias for strict_html), loose_html, xml." % mode |
paul@69 | 522 | |
paul@69 | 523 | class CustomizationError( MarkupError ): |
paul@69 | 524 | def __init__( self ): |
paul@69 | 525 | self.message = "If you customize the allowed elements, you must define both types 'onetags' and 'twotags'." |
paul@69 | 526 | |
paul@69 | 527 | if __name__ == '__main__': |
paul@69 | 528 | import sys |
paul@69 | 529 | sys.stdout.write( __doc__ ) |