remove a lot of deprecated files ;
[monitor.git] / www / HyperText / HTML40.py
1 """HTML40 -- generate HTML conformant to the 4.0 standard. See:
2
3         http://www.w3.org/TR/REC-html40/
4
5 All HTML 4.0 elements are implemented except for a few which are
6 deprecated. All attributes should be implemented. HTML is generally
7 case-insensitive, whereas Python is not. All elements have been coded
8 in UPPER CASE, with attributes in lower case. General usage:
9
10         e = ELEMENT(*content, **attr)
11
12 i.e., the positional arguments become the content of the element, and
13 the keyword arguments set element attributes. All attributes MUST be
14 specified with keyword arguments, and the content MUST be a series of
15 positional arguments; if you use content="spam", it will set this as
16 the attribute content, not as the element content. Multiple content
17 arguments are simply joined with no separator. Example:
18
19 >>> t = TABLE(TR(TH('SPAM'), TH('EGGS')), TR(TD('foo','bar', colspan=2)) )
20 >>> print t
21
22 <TABLE>
23   <TR>
24     <TH>SPAM</TH>
25     <TH>EGGS</TH></TR>
26   <TR>
27     <TD colspan="2">foobar</TD></TR></TABLE>
28
29 As with HTMLgen and other HTML generators, you can print the object
30 and it makes one monster string and writes that to stdout. Unlike
31 HTMLgen, these objects all have a writeto(fp=stdout, indent=0,
32 perlevel=2) method. This method may save memory, and it might be
33 faster possibly (many fewer string joins), plus you get progressive
34 output. If you want to alter the indentation on the string output,
35 try:
36
37 >> print t.__str__(indent=5, perlevel=6)
38
39      <TABLE>
40            <TR>
41                  <TH>SPAM</TH>
42                  <TH>EGGS</TH></TR>
43            <TR>
44                  <TD colspan="2">foobar</TD></TR></TABLE>
45
46 The output from either method (__str__ or writeto) SHOULD be the
47 lexically equivalent, regardless of indentation; not all elements are
48 made pretty, only those which are insensitive to the addition of
49 whitespace before or after the start/end tags. If you don't like the
50 indenting, use writeto(perlevel=0) (or __str__(perlevel=0)).
51
52 Element attributes can be set through the normal Python dictionary
53 operations on the object (they are not Python attributes).
54
55 Note: There are a few HTML attributes with a dash in them. In these
56 cases, substitute an underscore and the output will be corrected. HTML
57 4.0 also defines a class attribute, which conflicts with Python's
58 class statement; use klass instead. The new LABEL element has a for
59 attribute which also conflicts; use label_for instead.
60
61 >>> print META(http_equiv='refresh',content='60;/index2.html')
62 <META http-equiv="refresh" content="60;/index2.html">
63
64 The output order of attributes is indeterminate (based on hash order),
65 but this is of no particular importance.  The extent of attribute
66 checking is limited to checking that the attribute is legal for that
67 element; the values themselves are not checked, but must be
68 convertible to a string. The content items must be convertible to
69 strings and/or have a writeto() method. Some elements may have a few
70 attributes they shouldn't, particularly those which use intrinsic
71 events.
72
73 Valid attributes are defined for each element with dictionaries, with
74 the keys being the attributes. If the value is false, it's a boolean;
75 otherwise the value is printed.
76
77 Subclassing: If all you need to do is have some defaults, override the
78 defaults dictionary. You will also need to set name to the correct
79 element name. Example:
80
81 >>> class Refresh(META): defaults = {'http_equiv': 'refresh'}; name = 'META'
82 ... 
83 >>> print Refresh(content='10; /index2.html')
84 <META http-equiv="refresh" content="10; /index2.html">
85
86 Weirdness with Netscape 4.x: It recognizes a border attribute for the
87 FRAMESET element, though it is not defined in the HTML 4.0 spec. It
88 seems to recognize the frameborder attribute for FRAME, but border
89 only changes from a 3D shaded border to a flat, unresizable grey
90 border. Because of this, there is a border attribute defined for
91 FRAMESET. Similarly, HTML 4.0 does not define a border attribute for
92 INPUT (for use with type="image"), but one has been added anyway.
93
94 Historical notes: My first experience with an HTML generator was with
95 the one which comes with "Internet Programming with Python" by Aaron
96 Watters, Guido van Rossum, and James C. Ahlstrom. I hate to dis it,
97 but the thing really drove me nuts after awhile. Horrible to debug
98 anything, but maybe my understanding of it was incomplete. I then
99 discovered HTMLgen by Robin Friedrich:
100
101 http://starship.skyport.net/crew/friedrich/HTMLgen/html/main.html
102
103 It worked much better, for me at least, good enough for a major
104 project. There were, however, some frustrations: Subclassing could
105 sometimes be difficult (in fairness, I think that was by design), and
106 there were some missing features I wanted. Plus the thing's huge, as
107 Python modules go. These are relatively minor gripes, and if you don't
108 like this module, definitely use HTMLgen.
109
110 Mainly I did this because the methodology to do it just sorta dawned
111 on me. The result is, I think, some pretty clean code. Really, there's
112 hardly any actual code at all. Hey, and when was the last time saw a
113 subclass inherit from only one parent class with only a pass statement
114 and no attributes defined? There's 27 of them here. There's almost no
115 logic to it at all; it's pretty much all driven by dictionaries.
116
117 Yes, there are a number of features missing which are present in
118 HTMLgen, namely the document classes. All the high-level abstractions
119 are going in another module or two.
120
121 """
122
123 __version__ = "$Revision: 1.8 $"[11:-4]
124
125 import string
126 from string import lower, join, replace
127 from sys import stdout
128
129 coreattrs = {'id': 1, 'klass': 1, 'style': 1, 'title': 1}
130 i18n = {'lang': 1, 'dir': 1}
131 intrinsic_events = {'onload': 1, 'onunload': 1, 'onclick': 1,
132                     'ondblclick': 1, 'onmousedown': 1, 'onmouseup': 1,
133                     'onmouseover': 1, 'onmousemove': 1, 'onmouseout': 1,
134                     'onfocus': 1, 'onblur': 1, 'onkeypress': 1,
135                     'onkeydown': 1, 'onkeyup': 1, 'onsubmit': 1,
136                     'onreset': 1, 'onselect': 1, 'onchange': 1 }
137
138 attrs = coreattrs.copy()
139 attrs.update(i18n)
140 attrs.update(intrinsic_events)
141
142 alternate_text = {'alt': 1}
143 image_maps = {'shape': 1, 'coords': 1}
144 anchor_reference = {'href': 1}
145 target_frame_info = {'target': 1}
146 tabbing_navigation = {'tabindex': 1}
147 access_keys = {'accesskey': 1}
148
149 tabbing_and_access = tabbing_navigation.copy()
150 tabbing_and_access.update(access_keys)
151
152 visual_presentation = {'height': 1, 'width': 1, 'border': 1, 'align': 1,
153                        'hspace': 1, 'vspace': 1}
154
155 cellhalign = {'align': 1, 'char': 1, 'charoff': 1}
156 cellvalign = {'valign': 1}
157
158 font_modifiers = {'size': 1, 'color': 1, 'face': 1}
159
160 links_and_anchors = {'href': 1, 'hreflang': 1, 'type': 1, 'rel': 1, 'rev': 1}
161 borders_and_rules = {'frame': 1, 'rules': 1, 'border': 1}
162
163 from SGML import Markup, Comment
164 from XML import XMLPI
165
166 DOCTYPE = Markup("DOCTYPE",
167                  'HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" ' \
168                  '"http://www.w3.org/TR/REC-html40/loose.dtd"')
169 DOCTYPE_frameset = Markup("DOCTYPE",
170                  'HTML PUBLIC "-//W3C//DTD HTML 4.0 Frameset//EN" ' \
171                  '"http://www.w3.org/TR/REC-HTML/frameset.dtd"')
172
173 class Element(XMLPI):
174
175     defaults = {}
176     attr_translations = {'klass': 'class',
177                          'label_for': 'for',
178                          'http_equiv': 'http-equiv',
179                          'accept_charset': 'accept-charset'}
180
181     def __init__(self, *content, **attr):
182         self.dict = {}
183         if not hasattr(self, 'name'): self.name = self.__class__.__name__
184         if self.defaults: self.update(self.defaults)
185         self.update(attr)
186         if not self.content_model and content:
187             raise TypeError, "No content for this element"
188         self.content = list(content)
189
190     def update(self, d2): 
191         for k, v in d2.items(): self[k] = v
192
193     def __setitem__(self, k, v):
194         kl = lower(k)
195         if self.attlist.has_key(kl): self.dict[kl] = v
196         else: raise KeyError, "Invalid attribute for this element"
197
198     start_tag_string = "<%s %s>"
199     start_tag_no_attr_string = "<%s>"
200     end_tag_string = "</%s>"
201
202     def str_attribute(self, k):
203         return self.attlist.get(k, 1) and '%s="%s"' % \
204                (self.attr_translations.get(k, k), str(self[k])) \
205                or self[k] and k or ''
206
207     def start_tag(self):
208         a = self.str_attribute_list()
209         return a and self.start_tag_string % (self.name, a) \
210                or self.start_tag_no_attr_string % self.name
211
212     def end_tag(self):
213         return self.content_model and self.end_tag_string % self.name  or ''
214
215
216 class PrettyTagsMixIn:
217
218     def writeto(self, fp=stdout, indent=0, perlevel=2):
219         myindent = '\n' + " "*indent
220         fp.write(myindent+self.start_tag())
221         for c in self.content:
222             if hasattr(c, 'writeto'):
223                 getattr(c, 'writeto')(fp, indent+perlevel, perlevel)
224             else:
225                 fp.write(str(c))
226         fp.write(self.end_tag())
227
228     def __str__(self, indent=0, perlevel=2):
229         myindent = (perlevel and '\n' or '') + " "*indent
230         s = [myindent, self.start_tag()]
231         for c in self.content:
232             try: s.append(apply(c.__str__, (indent+perlevel, perlevel)))
233             except: s.append(str(c))
234         s.append(self.end_tag())
235         return join(s,'')
236
237 class CommonElement(Element): attlist = attrs
238
239 class PCElement(PrettyTagsMixIn, CommonElement): pass
240
241 class A(CommonElement):
242
243     attlist = {'name': 1, 'charset': 1}
244     attlist.update(CommonElement.attlist)
245     attlist.update(links_and_anchors)
246     attlist.update(image_maps)
247     attlist.update(target_frame_info)
248     attlist.update(tabbing_and_access)
249
250
251 class ABBR(CommonElement): pass
252 class ACRONYM(CommonElement): pass
253 class CITE(CommonElement): pass
254 class CODE(CommonElement): pass
255 class DFN(CommonElement): pass
256 class EM(CommonElement): pass
257 class KBD(CommonElement): pass
258 class PRE(CommonElement): pass
259 class SAMP(CommonElement): pass
260 class STRONG(CommonElement): pass
261 class VAR(CommonElement): pass
262 class ADDRESS(CommonElement): pass
263 class B(CommonElement): pass
264 class BIG(CommonElement): pass
265 class I(CommonElement): pass
266 class S(CommonElement): pass
267 class SMALL(CommonElement): pass
268 class STRIKE(CommonElement): pass
269 class TT(CommonElement): pass
270 class U(CommonElement): pass
271 class SUB(CommonElement): pass
272 class SUP(CommonElement): pass
273  
274 class DD(PCElement): pass
275 class DL(PCElement): pass
276 class DT(PCElement): pass
277 class NOFRAMES(PCElement): pass
278 class NOSCRIPTS(PCElement): pass
279 class P(PCElement): pass
280
281 class AREA(PCElement):
282
283     attlist = {'name': 1, 'nohref': 0}
284     attlist.update(PCElement.attlist)
285     attlist.update(image_maps)
286     attlist.update(anchor_reference)
287     attlist.update(tabbing_and_access)
288     attlist.update(alternate_text)
289
290 class MAP(AREA): pass
291
292 class BASE(PrettyTagsMixIn, Element):
293
294     attlist = anchor_reference.copy()
295     attlist.update(target_frame_info)
296     content_model = None
297
298 class BDO(Element):
299
300     attlist = coreattrs.copy()
301     attlist.update(i18n)
302
303 class BLOCKQUOTE(CommonElement):
304
305     attlist = {'cite': 1}
306     attlist.update(CommonElement.attlist)
307
308 class Q(BLOCKQUOTE): pass
309
310 class BR(PrettyTagsMixIn, Element):
311
312     attlist = coreattrs
313     content_model = None
314
315 class BUTTON(CommonElement):
316
317     attlist = {'name': 1, 'value': 1, 'type': 1, 'disabled': 0}
318     attlist.update(CommonElement.attlist)
319     attlist.update(tabbing_and_access)
320
321 class CAPTION(Element):
322
323     attlist = {'align': 1}
324     attlist.update(attrs)
325
326 class COLGROUP(PCElement):
327
328     attlist = {'span': 1, 'width': 1}
329     attlist.update(PCElement.attlist)
330     attlist.update(cellhalign)
331     attlist.update(cellvalign)
332
333 class COL(COLGROUP): content_model = None
334
335 class DEL(Element):
336
337     attlist = {'cite': 1, 'datetime': 1}
338     attlist.update(attrs)
339
340 class INS(DEL): pass
341
342 class FIELDSET(PCElement): pass
343
344 class LEGEND(PCElement):
345     
346     attlist = {'align': 1}
347     attlist.update(PCElement.attlist)
348     attlist.update(access_keys)
349
350 class BASEFONT(Element):
351
352     attlist = {'id': 1}
353     attlist.update(font_modifiers)
354     content_model = None
355
356 class FONT(Element):
357
358     attlist = font_modifiers.copy()
359     attlist.update(coreattrs)
360     attlist.update(i18n)
361
362 class FORM(PCElement):
363
364     attlist = {'action': 1, 'method': 1, 'enctype': 1, 'accept_charset': 1,
365                'target': 1}
366     attlist.update(PCElement.attlist)
367
368 class FRAME(PrettyTagsMixIn, Element):
369
370     attlist = {'longdesc': 1, 'name': 1, 'src': 1, 'frameborder': 1,
371                'marginwidth': 1, 'marginheight': 1, 'noresize': 0,
372                'scrolling': 1}
373     attlist.update(coreattrs)
374     content_model = None
375
376 class FRAMESET(PrettyTagsMixIn, Element):
377
378     attlist = {'rows': 1, 'cols': 1, 'border': 1}
379     attlist.update(coreattrs)
380     attlist.update(intrinsic_events)
381
382 class Heading(PCElement):
383
384     attlist = {'align': 1}
385     attlist.update(attrs)
386
387     def __init__(self, level, *content, **attr):
388         self.level = level
389         apply(PCElement.__init__, (self,)+content, attr)
390
391     def start_tag(self):
392         a = self.str_attribute_list()
393         return a and "<H%d %s>" % (self.level, a) or "<H%d>" % self.level
394
395     def end_tag(self):
396         return self.content_model and "</H%d>\n" % self.level or ''
397
398 class HEAD(PrettyTagsMixIn, Element):
399
400     attlist = {'profile': 1}
401     attlist.update(i18n)
402
403 class HR(Element):
404
405     attlist = {'align': 1, 'noshade': 0, 'size': 1, 'width': 1}
406     attlist.update(coreattrs)
407     attlist.update(intrinsic_events)
408     content_model = None
409
410 class HTML(PrettyTagsMixIn, Element):
411
412     attlist = i18n
413
414 class TITLE(HTML): pass
415
416 class BODY(PCElement):
417
418     attlist = {'background': 1, 'text': 1, 'link': 1, 'vlink': 1, 'alink': 1,
419                'bgcolor': 1}
420     attlist.update(PCElement.attlist)
421
422 class IFRAME(PrettyTagsMixIn, Element):
423
424     attlist = {'longdesc': 1, 'name': 1, 'src': 1, 'frameborder': 1,
425                'marginwidth': 1, 'marginheight': 1, 'scrolling': 1, 
426                'align': 1, 'height': 1, 'width': 1}
427     attlist.update(coreattrs)
428
429 class IMG(CommonElement):
430
431     attlist = {'src': 1, 'longdesc': 1, 'usemap': 1, 'ismap': 0}
432     attlist.update(PCElement.attlist)
433     attlist.update(visual_presentation)
434     attlist.update(alternate_text)
435     content_model = None
436
437 class INPUT(CommonElement):
438
439     attlist = {'type': 1, 'name': 1, 'value': 1, 'checked': 0, 'disabled': 0,
440                'readonly': 0, 'size': 1, 'maxlength': 1, 'src': 1,
441                'usemap': 1, 'accept': 1, 'border': 1}
442     attlist.update(CommonElement.attlist)
443     attlist.update(tabbing_and_access)
444     attlist.update(alternate_text)
445     content_model = None
446
447 class LABEL(CommonElement):
448
449     attlist = {'label_for': 1}
450     attlist.update(CommonElement.attlist)
451     attlist.update(access_keys)
452
453 class UL(PCElement):
454     
455     attlist = {'compact': 0}
456     attlist.update(CommonElement.attlist)
457
458 class OL(UL):
459
460     attlist = {'start': 1}
461     attlist.update(UL.attlist)
462
463 class LI(UL):
464
465     attlist = {'value': 1, 'type': 1}
466     attlist.update(UL.attlist)
467
468 class LINK(PCElement):
469
470     attlist = {'charset': 1, 'media': 1}
471     attlist.update(PCElement.attlist)
472     attlist.update(links_and_anchors)
473     content_model = None
474
475 class META(PrettyTagsMixIn, Element):
476
477     attlist = {'http_equiv': 1, 'name': 1, 'content': 1, 'scheme': 1}
478     attlist.update(i18n)
479     content_model = None
480
481 class OBJECT(PCElement):
482
483     attlist = {'declare': 0, 'classid': 1, 'codebase': 1, 'data': 1,
484                'type': 1, 'codetype': 1, 'archive': 1, 'standby': 1,
485                'height': 1, 'width': 1, 'usemap': 1}
486     attlist.update(PCElement.attlist)
487     attlist.update(tabbing_navigation)
488
489 class SELECT(PCElement):
490
491     attlist = {'name': 1, 'size': 1, 'multiple': 0, 'disabled': 0}
492     attlist.update(CommonElement.attlist)
493     attlist.update(tabbing_navigation)
494
495 class OPTGROUP(PCElement):
496
497     attlist = {'disabled': 0, 'label': 1}
498     attlist.update(CommonElement.attlist)
499
500 class OPTION(OPTGROUP):
501
502     attlist = {'value': 1, 'selected': 0}
503     attlist.update(OPTGROUP.attlist)
504
505 class PARAM(Element):
506
507     attlist = {'id': 1, 'name': 1, 'value': 1, 'valuetype': 1, 'type': 1}
508
509 class SCRIPT(Element):
510
511     attlist = {'charset': 1, 'type': 1, 'src': 1, 'defer': 0}
512
513 class SPAN(CommonElement):
514
515     attlist = {'align': 1}
516     attlist.update(CommonElement.attlist)
517
518 class DIV(PrettyTagsMixIn, SPAN): pass
519
520 class STYLE(PrettyTagsMixIn, Element):
521
522     attlist = {'type': 1, 'media': 1, 'title': 1}
523     attlist.update(i18n)
524
525 class TABLE(PCElement):
526
527     attlist = {'cellspacing': 1, 'cellpadding': 1, 'summary': 1, 'align': 1,
528                'bgcolor': 1, 'width': 1}
529     attlist.update(CommonElement.attlist)
530     attlist.update(borders_and_rules)
531
532 class TBODY(PCElement):
533
534     attlist = CommonElement.attlist.copy()
535     attlist.update(cellhalign)
536     attlist.update(cellvalign)
537
538 class THEAD(TBODY): pass
539 class TFOOT(TBODY): pass
540 class TR(TBODY): pass
541
542 class TH(TBODY):
543
544     attlist = {'abbv': 1, 'axis': 1, 'headers': 1, 'scope': 1,
545                'rowspan': 1, 'colspan': 1, 'nowrap': 0, 'width': 1, 
546                'height': 1}
547     attlist.update(TBODY.attlist)
548
549 class TD(TH): pass
550
551 class TEXTAREA(CommonElement):
552
553     attlist = {'name': 1, 'rows': 1, 'cols': 1, 'disabled': 0, 'readonly': 0}
554     attlist.update(CommonElement.attlist)
555     attlist.update(tabbing_and_access)
556
557 def CENTER(*content, **attr):
558     c = apply(DIV, content, attr)
559     c['align'] = 'center'
560     return c
561
562 def H1(content=[], **attr): return apply(Heading, (1, content), attr)
563 def H2(content=[], **attr): return apply(Heading, (2, content), attr)
564 def H3(content=[], **attr): return apply(Heading, (3, content), attr)
565 def H4(content=[], **attr): return apply(Heading, (4, content), attr)
566 def H5(content=[], **attr): return apply(Heading, (5, content), attr)
567 def H6(content=[], **attr): return apply(Heading, (6, content), attr)
568
569 class CSSRule(PrettyTagsMixIn, Element):
570
571     attlist = {'font': 1, 'font_family': 1, 'font_face': 1, 'font_size': 1,
572                'border': 1, 'border_width': 1, 'color': 1,
573                'background': 1, 'background_color': 1, 'background_image': 1,
574                'text_align': 1, 'text_decoration': 1, 'text_indent': 1,
575                'line_height': 1, 'margin_left': 1, 'margin_right': 1,
576                'clear': 1, 'list_style_type': 1}
577     content = []
578     content_model = None
579
580     def __init__(self, selector, **decl):
581         self.dict = {}
582         self.update(decl)
583         self.name = selector
584
585     start_tag_string = "%s { %s }"
586
587     def end_tag(self): return ''
588
589     def str_attribute(self, k):
590         kt = replace(k, '_', '-')
591         if self.attlist[k]: return '%s: %s' % (kt, str(self[k]))
592         else: return self[k] and kt or ''
593
594     def str_attribute_list(self):
595         return join(map(self.str_attribute, self.dict.keys()), '; ')
596
597 nbsp = "&nbsp;"
598
599 def quote_body(s):
600     r=replace; return r(r(r(s, '&', '&amp;'), '<', '&lt;'), '>', '&gt;')
601
602 safe = string.letters + string.digits + '_,.-'
603
604 def url_encode(s):
605     l = []
606     for c in s:
607         if c in safe:  l.append(c)
608         elif c == ' ': l.append('+')
609         else:          l.append("%%%02x" % ord(c))
610     return join(l, '')
611
612 def URL(*args, **kwargs):
613     url_path = join(args, '/')
614     a = []
615     for k, v in kwargs.items():
616         a.append("%s=%s" % (url_encode(k), url_encode(v)))
617     url_vals = join(a, '&')
618     return url_vals and join([url_path, url_vals],'?') or url_path
619
620 def Options(options, selected=[], **attrs):
621     opts = []
622     for o, v in options:
623         opt = apply(OPTION, (o,), attrs)
624         opt['value'] = v
625         if v in selected: opt['selected'] = 1
626         opts.append(opt)
627     return opts
628
629 def Select(options, selected=[], **attrs):
630     return apply(SELECT, tuple(apply(Options, (options, selected))), attrs)
631
632 def Href(url, text, **attrs):
633     h = apply(A, (text,), attrs)
634     h['href'] = url
635     return h
636
637 def Mailto(address, text, subject='', **attrs):
638     if subject:
639         url = "mailto:%s?subject=%s" % (address, subject)
640     else:
641         url = "mailto:%s" % address
642     return apply(Href, (url, text), attrs)
643
644 def Image(src, **attrs):
645     i = apply(IMG, (), a)
646     i['src'] = src
647     return i
648
649 def StyledTR(element, row, klasses):
650     r = TR()
651     for i in range(len(row)):
652         r.append(klasses[i] and element(row[i], klass=klasses[i]) \
653                             or  element(row[i]))
654     return r
655
656 def StyledVTable(klasses, *rows, **attrs):
657     t = apply(TABLE, (), attrs)
658     t.append(COL(span=len(klasses)))
659     for row in rows:
660         r = StyledTR(TD, row[1:], klasses[1:])
661         h = klasses[0] and TH(row[0], klass=klasses[0]) \
662                        or  TH(row[0])
663         r.content.insert(0,h)
664         t.append(r)
665     return t
666
667 def VTable(*rows, **attrs):
668     t = apply(TABLE, (), attrs)
669     t.append(COL(span=len(rows[0])))
670     for row in rows:
671         r = apply(TR, tuple(map(TD, row[1:])))
672         r.content.insert(0, TH(row[0]))
673         t.append(r)
674     return t
675
676 def StyledHTable(klasses, headers, *rows, **attrs):
677     t = apply(TABLE, (), attrs)
678     t.append(COL(span=len(headers)))
679     t.append(StyledTR(TH, headers, klasses))
680     for row in rows: t.append(StyledTR(TD, row, klasses))
681     return t
682
683 def HTable(headers, *rows, **attrs):
684     t = apply(TABLE, (), attrs)
685     t.append(COL(span=len(headers)))
686     t.append(TR, tuple(map(TH, headers)))
687     for row in rows: t.append(TR(apply(TD, row)))
688     return t
689
690 def DefinitionList(*items, **attrs):
691     dl = apply(DL, (), attrs)
692     for dt, dd in items: dl.append(DT(dt), DD(dd))
693     return dl
694
695