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