1 """XHTML10 -- generate XHTML conformant to the 1.0 standard. See:
3 http://www.w3.org/TR/xhtml1/
5 Implemented similarly to HTML40; see the docs in that module.
9 __version__ = "$Revision: 1.4 $"[11:-4]
12 from string import lower, join, replace
13 from sys import stdout
15 coreattrs = {'id': 1, 'klass': 1, 'style': 1, 'title': 1}
16 i18n = {'lang': 1, 'dir': 1}
17 intrinsic_events = {'onload': 1, 'onunload': 1, 'onclick': 1,
18 'ondblclick': 1, 'onmousedown': 1, 'onmouseup': 1,
19 'onmouseover': 1, 'onmousemove': 1, 'onmouseout': 1,
20 'onfocus': 1, 'onblur': 1, 'onkeypress': 1,
21 'onkeydown': 1, 'onkeyup': 1, 'onsubmit': 1,
22 'onreset': 1, 'onselect': 1, 'onchange': 1 }
24 attrs = coreattrs.copy()
26 attrs.update(intrinsic_events)
28 alternate_text = {'alt': 1}
29 image_maps = {'shape': 1, 'coords': 1}
30 anchor_reference = {'href': 1}
31 target_frame_info = {'target': 1}
32 tabbing_navigation = {'tabindex': 1}
33 access_keys = {'accesskey': 1}
35 tabbing_and_access = tabbing_navigation.copy()
36 tabbing_and_access.update(access_keys)
38 visual_presentation = {'height': 1, 'width': 1, 'border': 1, 'align': 1,
39 'hspace': 1, 'vspace': 1}
41 cellhalign = {'align': 1, 'char': 1, 'charoff': 1}
42 cellvalign = {'valign': 1}
44 font_modifiers = {'size': 1, 'color': 1, 'face': 1}
46 links_and_anchors = {'href': 1, 'hreflang': 1, 'type': 1, 'rel': 1, 'rev': 1}
47 borders_and_rules = {'frame': 1, 'rules': 1, 'border': 1}
49 from SGML import Markup, Comment
52 DOCTYPE = Markup("DOCTYPE",
53 'html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" ' \
54 '"DTD/xhtml1-transitional.dtd"')
55 DOCTYPE_frameset = Markup("DOCTYPE",
56 'html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" ' \
57 '"DTD/xhtml1-frameset.dtd"')
63 def __init__(self, *content, **attr):
65 if not hasattr(self, 'name'): self.name = lower(self.__class__.__name__)
66 if self.defaults: self.update(self.defaults)
68 if not self.content_model and content:
69 raise TypeError, "No content for this element"
70 self.content = list(content)
73 for k, v in d2.items(): self[k] = v
75 def __setitem__(self, k, v):
77 if self.attlist.has_key(kl): self.dict[kl] = v
78 else: raise KeyError, "Invalid attribute for this element"
80 start_tag_string = "<%s %s>"
81 start_tag_no_attr_string = "<%s>"
82 end_tag_string = "</%s>"
85 a = self.str_attribute_list()
86 return a and self.start_tag_string % (self.name, a) \
87 or self.start_tag_no_attr_string % self.name
90 return self.end_tag_string % self.name
93 class PrettyTagsMixIn:
95 def writeto(self, fp=stdout, indent=0, perlevel=2):
96 myindent = '\n' + " "*indent
97 fp.write(myindent+self.start_tag())
98 for c in self.content:
99 if hasattr(c, 'writeto'):
100 getattr(c, 'writeto')(fp, indent+perlevel, perlevel)
103 fp.write(self.end_tag())
105 def __str__(self, indent=0, perlevel=2):
106 myindent = (perlevel and '\n' or '') + " "*indent
107 s = [myindent, self.start_tag()]
108 for c in self.content:
109 try: s.append(apply(c.__str__, (indent+perlevel, perlevel)))
110 except: s.append(str(c))
111 s.append(self.end_tag())
114 class CommonElement(Element): attlist = attrs
116 class PCElement(PrettyTagsMixIn, CommonElement): pass
118 class A(CommonElement):
120 attlist = {'charset': 1}
121 attlist.update(CommonElement.attlist)
122 attlist.update(links_and_anchors)
123 attlist.update(image_maps)
124 attlist.update(target_frame_info)
125 attlist.update(tabbing_and_access)
128 class ABBR(CommonElement): pass
129 class ACRONYM(CommonElement): pass
130 class CITE(CommonElement): pass
131 class CODE(CommonElement): pass
132 class DFN(CommonElement): pass
133 class EM(CommonElement): pass
134 class KBD(CommonElement): pass
135 class PRE(CommonElement): pass
136 class SAMP(CommonElement): pass
137 class STRONG(CommonElement): pass
138 class VAR(CommonElement): pass
139 class ADDRESS(CommonElement): pass
140 class B(CommonElement): pass
141 class BIG(CommonElement): pass
142 class I(CommonElement): pass
143 class S(CommonElement): pass
144 class SMALL(CommonElement): pass
145 class STRIKE(CommonElement): pass
146 class TT(CommonElement): pass
147 class U(CommonElement): pass
148 class SUB(CommonElement): pass
149 class SUP(CommonElement): pass
151 class DD(PCElement): pass
152 class DL(PCElement): pass
153 class DT(PCElement): pass
154 class NOFRAMES(PCElement): pass
155 class NOSCRIPTS(PCElement): pass
156 class P(PCElement): pass
158 class AREA(PCElement):
160 attlist = {'nohref': 0}
161 attlist.update(PCElement.attlist)
162 attlist.update(image_maps)
163 attlist.update(anchor_reference)
164 attlist.update(tabbing_and_access)
165 attlist.update(alternate_text)
167 class MAP(AREA): pass
169 class BASE(PrettyTagsMixIn, Element):
171 attlist = anchor_reference.copy()
172 attlist.update(target_frame_info)
177 attlist = coreattrs.copy()
180 class BLOCKQUOTE(CommonElement):
182 attlist = {'cite': 1}
183 attlist.update(CommonElement.attlist)
185 class Q(BLOCKQUOTE): pass
187 class BR(PrettyTagsMixIn, Element):
192 class BUTTON(CommonElement):
194 attlist = {'value': 1, 'type': 1, 'disabled': 0}
195 attlist.update(CommonElement.attlist)
196 attlist.update(tabbing_and_access)
198 class CAPTION(Element):
200 attlist = {'align': 1}
201 attlist.update(attrs)
203 class COLGROUP(PCElement):
205 attlist = {'span': 1, 'width': 1}
206 attlist.update(PCElement.attlist)
207 attlist.update(cellhalign)
208 attlist.update(cellvalign)
210 class COL(COLGROUP): content_model = None
214 attlist = {'cite': 1, 'datetime': 1}
215 attlist.update(attrs)
219 class FIELDSET(PCElement): pass
221 class LEGEND(PCElement):
223 attlist = {'align': 1}
224 attlist.update(PCElement.attlist)
225 attlist.update(access_keys)
227 class BASEFONT(Element):
230 attlist.update(font_modifiers)
235 attlist = font_modifiers.copy()
236 attlist.update(coreattrs)
239 class FORM(PCElement):
241 attlist = {'action': 1, 'method': 1, 'enctype': 1, 'accept_charset': 1,
243 attlist.update(PCElement.attlist)
245 class FRAME(PrettyTagsMixIn, Element):
247 attlist = {'longdesc': 1, 'src': 1, 'frameborder': 1,
248 'marginwidth': 1, 'marginheight': 1, 'noresize': 0,
250 attlist.update(coreattrs)
253 class FRAMESET(PrettyTagsMixIn, Element):
255 attlist = {'rows': 1, 'cols': 1, 'border': 1}
256 attlist.update(coreattrs)
257 attlist.update(intrinsic_events)
259 class Heading(PCElement):
261 attlist = {'align': 1}
262 attlist.update(attrs)
264 def __init__(self, level, *content, **attr):
266 apply(PCElement.__init__, (self,)+content, attr)
269 a = self.str_attribute_list()
270 return a and "<H%d %s>" % (self.level, a) or "<H%d>" % self.level
273 return self.content_model and "</H%d>\n" % self.level or ''
275 class HEAD(PrettyTagsMixIn, Element):
277 attlist = {'profile': 1}
282 attlist = {'align': 1, 'noshade': 0, 'size': 1, 'width': 1}
283 attlist.update(coreattrs)
284 attlist.update(intrinsic_events)
287 class HTML(PrettyTagsMixIn, Element):
291 class TITLE(HTML): pass
293 class BODY(PCElement):
295 attlist = {'background': 1, 'text': 1, 'link': 1, 'vlink': 1, 'alink': 1,
297 attlist.update(PCElement.attlist)
299 class IFRAME(PrettyTagsMixIn, Element):
301 attlist = {'longdesc': 1, 'src': 1, 'frameborder': 1,
302 'marginwidth': 1, 'marginheight': 1, 'scrolling': 1,
303 'align': 1, 'height': 1, 'width': 1}
304 attlist.update(coreattrs)
306 class IMG(CommonElement):
308 attlist = {'src': 1, 'longdesc': 1, 'usemap': 1, 'ismap': 0}
309 attlist.update(PCElement.attlist)
310 attlist.update(visual_presentation)
311 attlist.update(alternate_text)
314 class INPUT(CommonElement):
316 attlist = {'type': 1, 'value': 1, 'checked': 0, 'disabled': 0,
317 'readonly': 0, 'size': 1, 'maxlength': 1, 'src': 1,
318 'usemap': 1, 'accept': 1, 'border': 1}
319 attlist.update(CommonElement.attlist)
320 attlist.update(tabbing_and_access)
321 attlist.update(alternate_text)
324 class LABEL(CommonElement):
326 attlist = {'label_for': 1}
327 attlist.update(CommonElement.attlist)
328 attlist.update(access_keys)
332 attlist = {'compact': 0}
333 attlist.update(CommonElement.attlist)
337 attlist = {'start': 1}
338 attlist.update(UL.attlist)
342 attlist = {'value': 1, 'type': 1}
343 attlist.update(UL.attlist)
345 class LINK(PCElement):
347 attlist = {'charset': 1, 'media': 1}
348 attlist.update(PCElement.attlist)
349 attlist.update(links_and_anchors)
352 class META(PrettyTagsMixIn, Element):
354 attlist = {'http_equiv': 1, 'content': 1, 'scheme': 1}
358 class OBJECT(PCElement):
360 attlist = {'declare': 0, 'classid': 1, 'codebase': 1, 'data': 1,
361 'type': 1, 'codetype': 1, 'archive': 1, 'standby': 1,
362 'height': 1, 'width': 1, 'usemap': 1}
363 attlist.update(PCElement.attlist)
364 attlist.update(tabbing_navigation)
366 class SELECT(PCElement):
368 attlist = {'size': 1, 'multiple': 0, 'disabled': 0}
369 attlist.update(CommonElement.attlist)
370 attlist.update(tabbing_navigation)
372 class OPTGROUP(PCElement):
374 attlist = {'disabled': 0, 'label': 1}
375 attlist.update(CommonElement.attlist)
377 class OPTION(OPTGROUP):
379 attlist = {'value': 1, 'selected': 0}
380 attlist.update(OPTGROUP.attlist)
382 class PARAM(Element):
384 attlist = {'id': 1, 'value': 1, 'valuetype': 1, 'type': 1}
386 class SCRIPT(Element):
388 attlist = {'charset': 1, 'type': 1, 'src': 1, 'defer': 0}
390 class SPAN(CommonElement):
392 attlist = {'align': 1}
393 attlist.update(CommonElement.attlist)
395 class DIV(PrettyTagsMixIn, SPAN): pass
397 class STYLE(PrettyTagsMixIn, Element):
399 attlist = {'type': 1, 'media': 1, 'title': 1}
402 class TABLE(PCElement):
404 attlist = {'cellspacing': 1, 'cellpadding': 1, 'summary': 1, 'align': 1,
405 'bgcolor': 1, 'width': 1}
406 attlist.update(CommonElement.attlist)
407 attlist.update(borders_and_rules)
409 class TBODY(PCElement):
411 attlist = CommonElement.attlist.copy()
412 attlist.update(cellhalign)
413 attlist.update(cellvalign)
415 class THEAD(TBODY): pass
416 class TFOOT(TBODY): pass
417 class TR(TBODY): pass
421 attlist = {'abbv': 1, 'axis': 1, 'headers': 1, 'scope': 1,
422 'rowspan': 1, 'colspan': 1, 'nowrap': 0, 'width': 1,
424 attlist.update(TBODY.attlist)
428 class TEXTAREA(CommonElement):
430 attlist = {'rows': 1, 'cols': 1, 'disabled': 0, 'readonly': 0}
431 attlist.update(CommonElement.attlist)
432 attlist.update(tabbing_and_access)
434 def CENTER(*content, **attr):
435 c = apply(DIV, content, attr)
436 c['align'] = 'center'
439 def H1(content=[], **attr): return apply(Heading, (1, content), attr)
440 def H2(content=[], **attr): return apply(Heading, (2, content), attr)
441 def H3(content=[], **attr): return apply(Heading, (3, content), attr)
442 def H4(content=[], **attr): return apply(Heading, (4, content), attr)
443 def H5(content=[], **attr): return apply(Heading, (5, content), attr)
444 def H6(content=[], **attr): return apply(Heading, (6, content), attr)
446 class CSSRule(PrettyTagsMixIn, Element):
448 attlist = {'font': 1, 'font_family': 1, 'font_face': 1, 'font_size': 1,
449 'border': 1, 'border_width': 1, 'color': 1,
450 'background': 1, 'background_color': 1, 'background_image': 1,
451 'text_align': 1, 'text_decoration': 1, 'text_indent': 1,
452 'line_height': 1, 'margin_left': 1, 'margin_right': 1,
453 'clear': 1, 'list_style_type': 1}
457 def __init__(self, selector, **decl):
462 start_tag_string = "%s { %s }"
464 def end_tag(self): return ''
466 def str_attribute(self, k):
467 kt = replace(k, '_', '-')
468 if self.attlist[k]: return '%s: %s' % (kt, str(self[k]))
469 else: return self[k] and kt or ''
471 def str_attribute_list(self):
472 return join(map(self.str_attribute, self.dict.keys()), '; ')
477 r=replace; return r(r(r(s, '&', '&'), '<', '<'), '>', '>')
479 safe = string.letters + string.digits + '_,.-'
484 if c in safe: l.append(c)
485 elif c == ' ': l.append('+')
486 else: l.append("%%%02x" % ord(c))
489 def URL(*args, **kwargs):
490 url_path = join(args, '/')
492 for k, v in kwargs.items():
493 a.append("%s=%s" % (url_encode(k), url_encode(v)))
494 url_vals = join(a, '&')
495 return url_vals and join([url_path, url_vals],'?') or url_path
497 def Options(options, selected=[], **attrs):
500 opt = apply(OPTION, (o,), attrs)
502 if v in selected: opt['selected'] = 1
506 def Select(options, selected=[], **attrs):
507 return apply(SELECT, tuple(apply(Options, (options, selected))), attrs)
509 def Href(url, text, **attrs):
510 h = apply(A, (text,), attrs)
514 def Mailto(address, text, subject='', **attrs):
516 url = "mailto:%s?subject=%s" % (address, subject)
518 url = "mailto:%s" % address
519 return apply(Href, (url, text), attrs)
521 def Image(src, **attrs):
522 i = apply(IMG, (), a)
526 def StyledTR(element, row, klasses):
528 for i in range(len(row)):
529 r.append(klasses[i] and element(row[i], klass=klasses[i]) \
533 def StyledVTable(klasses, *rows, **attrs):
534 t = apply(TABLE, (), attrs)
535 t.append(COL(span=len(klasses)))
537 r = StyledTR(TD, row[1:], klasses[1:])
538 h = klasses[0] and TH(row[0], klass=klasses[0]) \
540 r.content.insert(0,h)
544 def VTable(*rows, **attrs):
545 t = apply(TABLE, (), attrs)
546 t.append(COL(span=len(rows[0])))
548 r = apply(TR, tuple(map(TD, row[1:])))
549 r.content.insert(0, TH(row[0]))
553 def StyledHTable(klasses, headers, *rows, **attrs):
554 t = apply(TABLE, (), attrs)
555 t.append(COL(span=len(headers)))
556 t.append(StyledTR(TH, headers, klasses))
557 for row in rows: t.append(StyledTR(TD, row, klasses))
560 def HTable(headers, *rows, **attrs):
561 t = apply(TABLE, (), attrs)
562 t.append(COL(span=len(headers)))
563 t.append(TR, tuple(map(TH, headers)))
564 for row in rows: t.append(TR(apply(TD, row)))
567 def DefinitionList(*items, **attrs):
568 dl = apply(DL, (), attrs)
569 for dt, dd in items: dl.append(DT(dt), DD(dd))