1 """ |
1 """ |
2 Generating HTML tags |
2 Generating HTML tags |
3 """ |
3 """ |
4 |
4 |
5 from cgi import escape |
5 from cgi import escape |
6 |
6 import itertools as _itertools |
7 def tag_attr (name, value) : |
7 |
8 return u'%s="%s"' % (name.rstrip('_'), escape(value, True)) |
8 class IRenderable (object) : |
9 |
9 """ |
10 def tag_subcontent (subcontent) : |
10 Something that's renderable as the contents of a HTML tag |
11 if not subcontent : |
11 """ |
12 # skip |
12 |
13 return |
13 def render_raw_lines (self, indent=u'\t') : |
14 |
14 """ |
15 elif not isinstance(subcontent, basestring) and hasattr(subcontent, '__iter__') : |
15 Render the indented lines for tag and contents, without newlines |
16 # render each sub-item recursively |
16 """ |
17 for sc in subcontent : |
17 |
18 for line in tag_subcontent(sc) : |
18 abstract |
19 yield line |
19 |
|
20 class Tag (IRenderable) : |
|
21 """ |
|
22 A HTML tag, with attributes and contents, which can a mixture of data and other renderables(tags). |
|
23 |
|
24 Provides various kinds of rendering output |
|
25 """ |
|
26 |
|
27 @staticmethod |
|
28 def process_contents (contents) : |
|
29 """ |
|
30 Postprocess contents iterable to return new list. |
|
31 |
|
32 Items that are None will be omitted from the return value. |
|
33 |
|
34 >>> Tag.process_contents([]) |
|
35 [] |
|
36 >>> Tag.process_contents([None]) |
|
37 [] |
|
38 >>> Tag.process_contents([u'foo']) |
|
39 [u'foo'] |
|
40 """ |
|
41 |
|
42 return [c for c in contents if c is not None] |
|
43 |
|
44 @staticmethod |
|
45 def process_attrs (attrs) : |
|
46 """ |
|
47 Postprocess attributes. |
|
48 |
|
49 Key-value pairs where the value is None will be ommitted, and any trailing underscores in the key omitted. |
|
50 |
|
51 TODO: only remove one underscore |
|
52 |
|
53 >>> Tag.process_attrs(dict()) |
|
54 {} |
|
55 >>> Tag.process_attrs(dict(foo='bar')) |
|
56 {'foo': 'bar'} |
|
57 >>> Tag.process_attrs(dict(class_='bar', frob=None)) |
|
58 {'class': 'bar'} |
|
59 """ |
|
60 |
|
61 return dict((k.rstrip('_'), v) for k, v in attrs.iteritems() if v is not None) |
|
62 |
|
63 def __init__ (self, name, *contents, **attrs) : |
|
64 """ |
|
65 Construct tag with given name/attributes or contents. |
|
66 |
|
67 The filtering rules desribed in process_contents/process_attrs apply. |
|
68 |
|
69 >>> Tag('foo') |
|
70 Tag('foo') |
|
71 >>> Tag('foo', 'quux') |
|
72 Tag('foo', 'quux') |
|
73 >>> Tag('foo', 'quux', bar=5) |
|
74 Tag('foo', 'quux', bar=5) |
|
75 >>> Tag('foo', class_='ten') |
|
76 Tag('foo', class='ten') |
|
77 """ |
|
78 |
|
79 self.name = name |
|
80 |
|
81 # store filtered/processed versions |
|
82 self.contents = self.process_contents(contents) |
|
83 self.attrs = self.process_attrs(attrs) |
|
84 |
|
85 def __call__ (self, *contents, **attrs) : |
|
86 """ |
|
87 Return a new Tag with this tag's attributes and contents, as well as the given attributes/contents. |
|
88 |
|
89 The filtering rules desribed in process_contents/process_attrs apply. |
|
90 |
|
91 >>> Tag('foo')('bar') |
|
92 Tag('foo', 'bar') |
|
93 >>> Tag('a', href='index.html')("Home") |
|
94 Tag('a', 'Home', href='index.html') |
|
95 >>> Tag('bar', None)(5, foo=None, class_='bar') |
|
96 Tag('bar', 5, class='bar') |
|
97 >>> Tag('a')('b')('c')(asdf=5) |
|
98 Tag('a', 'b', 'c', asdf=5) |
|
99 >>> t1 = Tag('a'); t2 = t1('b'); t1 |
|
100 Tag('a') |
|
101 """ |
|
102 |
|
103 # merge attrs/contents |
|
104 # XXX: new_attrs is not an iterator... |
|
105 new_attrs = dict(_itertools.chain(self.attrs.iteritems(), attrs.iteritems())) |
|
106 new_contents = _itertools.chain(self.contents, contents) |
|
107 |
|
108 # build new tag |
|
109 return Tag(self.name, *new_contents, **new_attrs) |
|
110 |
|
111 @staticmethod |
|
112 def format_attr (name, value) : |
|
113 """ |
|
114 Format a single HTML tag attribute |
|
115 |
|
116 >>> Tag.format_attr('name', 'value') |
|
117 u'name="value"' |
|
118 >>> Tag.format_attr('this', '<a"b>') |
|
119 u'this="<a"b>"' |
|
120 >>> Tag.format_attr('xx', 1337) |
|
121 u'xx="1337"' |
|
122 """ |
|
123 |
|
124 return u'%s="%s"' % (name, escape(unicode(value), True)) |
|
125 |
|
126 def render_attrs (self) : |
|
127 """ |
|
128 Return the HTML attributes string |
|
129 |
|
130 >>> Tag('x', foo=5, bar='<').render_attrs() |
|
131 u'foo="5" bar="<"' |
|
132 """ |
|
133 |
|
134 return " ".join(self.format_attr(n, v) for n, v in self.attrs.iteritems()) |
|
135 |
|
136 def render_contents (self, **render_opts) : |
|
137 """ |
|
138 Render the contents of the tag as a series of indented lines, with given render_lines options for subtags |
|
139 |
|
140 >>> list(Tag('x', 5).render_contents()) |
|
141 [u'5'] |
|
142 >>> list(Tag('x', 'line1', 'line2').render_contents()) |
|
143 [u'line1', u'line2'] |
|
144 >>> list(Tag('x', 'a', Tag('b', 'bb'), 'c').render_contents()) |
|
145 [u'a', u'<b>', u'\\tbb', u'</b>', u'c'] |
|
146 """ |
|
147 |
|
148 for content in self.contents : |
|
149 if isinstance(content, IRenderable) : |
|
150 # sub-tags |
|
151 for line in content.render_raw_lines(**render_opts) : |
|
152 yield line |
|
153 |
|
154 else : |
|
155 # escape raw values |
|
156 yield escape(unicode(content)) |
|
157 |
|
158 def render_raw_lines (self, indent=u'\t') : |
|
159 """ |
|
160 Render the tag and indented content |
|
161 |
|
162 >>> list(Tag('xx', 'yy', zz='foo').render_raw_lines(indent=' ')) |
|
163 [u'<xx zz="foo">', u' yy', u'</xx>'] |
|
164 """ |
|
165 |
|
166 # render attr string, including preceding space |
|
167 attrs_stuff = (" " + self.render_attrs()) if self.attrs else "" |
|
168 |
|
169 if self.contents : |
|
170 # tag with content |
|
171 yield u"<%s%s>" % (self.name, attrs_stuff) |
|
172 |
|
173 for line in self.render_contents(indent=indent) : |
|
174 yield indent + line |
|
175 |
|
176 yield u"</%s>" % (self.name, ) |
|
177 |
|
178 else : |
|
179 # singleton tag |
|
180 yield u"<%s%s />" % (self.name, attrs_stuff) |
20 |
181 |
21 else : |
182 def render_lines (self, indent=u'\t', newline=u'\n') : |
22 yield subcontent |
183 """ |
23 |
184 Render full output lines with given newlines |
24 def tag_content (content) : |
185 |
25 if not content: |
186 >>> list(Tag('xx', 'yy').render_lines()) |
26 # no output |
187 [u'<xx>\\n', u'\\tyy\\n', u'</xx>\\n'] |
27 return |
188 """ |
|
189 |
|
190 for line in self.render_raw_lines(indent) : |
|
191 yield line + newline |
|
192 |
|
193 def render_unicode (self, **render_opts) : |
|
194 """ |
|
195 Render full tag as a single unicode string |
|
196 |
|
197 >>> Tag('xx', 'yy').render_unicode() |
|
198 u'<xx>\\n\\tyy\\n</xx>\\n' |
|
199 """ |
|
200 |
|
201 return "".join(self.render_lines(**render_opts)) |
|
202 |
|
203 def render_str (self, charset='ascii', **render_opts) : |
|
204 """ |
|
205 Render full tag as an encoded string |
|
206 |
|
207 >>> Tag('xx', 'yy').render_str() |
|
208 '<xx>\\n\\tyy\\n</xx>\\n' |
|
209 """ |
|
210 |
|
211 return self.render_unicode(**render_opts).encode(charset) |
|
212 |
|
213 def render_out (self, stream, charset, **render_opts) : |
|
214 """ |
|
215 Render output into the given stream, encoding using the given charset |
|
216 |
|
217 >>> from StringIO import StringIO; buf = StringIO(); Tag('xx', 'yy').render_out(buf, 'ascii'); buf.getvalue() |
|
218 '<xx>\\n\\tyy\\n</xx>\\n' |
|
219 """ |
|
220 |
|
221 for line in self.render_lines(**render_opts) : |
|
222 stream.write(line.encode(charset)) |
28 |
223 |
29 elif isinstance(content, basestring) : |
224 # default output |
30 # escape raw vlues |
225 __str__ = render_str |
31 yield escape(unicode(content)) |
226 __unicode__ = render_unicode |
32 |
227 |
33 else : |
228 # default .render method |
34 # treat it as subcontent |
229 render = render_unicode |
35 for line in tag_subcontent(content) : |
230 |
36 yield line |
231 def __repr__ (self) : |
37 |
232 return 'Tag(%s)' % ', '.join( |
38 def tag (name, *content, **attrs) : |
233 [ |
39 attr_stuff = " " + " ".join(tag_attr(n, v) for n, v in attrs.iteritems()) if attrs else "" |
234 repr(self.name) |
|
235 ] + [ |
|
236 repr(c) for c in self.contents |
|
237 ] + [ |
|
238 '%s=%r' % (name, value) for name, value in self.attrs.iteritems() |
|
239 ] |
|
240 ) |
|
241 |
|
242 class Text (IRenderable) : |
|
243 """ |
|
244 Raw HTML text |
|
245 """ |
|
246 |
|
247 def __init__ (self, line) : |
|
248 """ |
|
249 Initialize to render as the given lines |
|
250 """ |
|
251 |
|
252 self.lines = [line] |
40 |
253 |
41 if content and all(content) : |
254 def render_raw_lines (self, indent=u'\t') : |
42 # tag with content |
255 return self.lines |
43 yield u"<%s%s>" % (name, attr_stuff) |
|
44 |
|
45 for c in content : |
|
46 for line in tag_content(c) : |
|
47 yield u"\t" + line |
|
48 |
|
49 yield u"</%s>" % (name, ) |
|
50 |
|
51 else : |
|
52 # singleton tag |
|
53 yield u"<%s%s />" % (name, attr_stuff) |
|
54 |
|
55 def raw (data) : |
|
56 yield data |
|
57 |
256 |
58 class TagFactory (object) : |
257 class TagFactory (object) : |
|
258 """ |
|
259 Build Tags with names give as attribute names |
|
260 """ |
|
261 |
59 def __getattr__ (self, name) : |
262 def __getattr__ (self, name) : |
60 def build_tag (*content, **attrs) : |
263 """ |
61 return tag(name, *content, **attrs) |
264 Get a Tag object with the given name, but no contents |
62 |
265 |
63 return build_tag |
266 >>> TagFactory().a(href='bar')('quux') |
64 |
267 Tag('a', 'quux', href='bar') |
|
268 """ |
|
269 |
|
270 return Tag(name) |
|
271 |
|
272 # pretty names |
|
273 tag = Tag |
65 tags = TagFactory() |
274 tags = TagFactory() |
66 |
275 raw = Text |
67 def render_lines (tags, charset=None) : |
276 |
68 for line in tag_content(tags) : |
277 # testing |
69 if charset : |
278 if __name__ == '__main__' : |
70 yield line.encode(charset) |
279 import doctest |
71 |
280 |
72 else : |
281 doctest.testmod() |
73 yield line |
282 |
74 |
|
75 def render (tags, charset=None) : |
|
76 data = u'\n'.join(render_lines(tags)) |
|
77 |
|
78 if charset : |
|
79 return data.encode(charset) |
|
80 |
|
81 else : |
|
82 return data |
|
83 |
|
84 def render_out (tags, out, charset='utf-8') : |
|
85 """ |
|
86 Write the rendered tags into the given output stream using the given encoding |
|
87 """ |
|
88 |
|
89 for line in render_lines(tags, charset) : |
|
90 out.write(line + '\n') |
|
91 |
|