|
|
The other day I was complaining about writing html, forms, etc., for Python cgi and/or web programming. I had pointed out a selection of three examples, the first of which ended up being very much like Nevow.stan . Thinking a bit about it, I realized that stan had issues in that you couldn't really re-use pre-defined tags with attributes via map, and keyword arguments were just too darn convenient to swap the calling and getitem syntax.
Instead, I hacked together a mechanism that supports:
T.tagname("content", T.tagname(...), ..., attr1='value', ...)
T.tagname(attr1='value', ...)("content", T.tagname(...), ...)
x = T.tagname(attr1='value', ...)
y = T.tagname(*map(x, ['content', ...]))
... and many other options.
Essentially, you can mix and match calls as much as you want, with three memory and sanity saving semantics:
1. Creating a new tag object via T.tagname, or any call of such, will create a shallow copy of the object you are accessing.
2. smallred = T.font(size='-1', color='red');bigred = smallred(size='+1') Works exactly the way you expect it to. If it doesn't work the way you expect it to, then your expectations are confused.
3. If you are adding content that sites within the tag, the content is replaced, not updated, like attributes.
This simple version handles auto-indentation of content as necessary (or desireable), auto-escaping of text elements, and includes an (I believe) nearly complete listing of entities which don't require closing tags.
I don't know where this is going, whether it can or will expand into something more, or what, but I believe what I have managed to hack together is better than other similar packages available elsewhere (including this recipe over here http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/366000 , which I discovered after writing my own). Funny how these things work out. Astute observers will note that I borrow nevow.stan's meme of using T.tagname for generating tag objects.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196 | import sys
import cStringIO
#handle character escaping...
import re
from htmlentitydefs import codepoint2name
character2name = {}
for i,j in codepoint2name.iteritems():
if i <= 127:
character2name[chr(i)] = '&%s;'%j
else:
character2name[unichr(i)] = '&#%d;'%i
del i;del j;del codepoint2name
escape = re.compile('(%s)'%('|'.join(list(character2name))))
def repl(matchobj):
return character2name.get(matchobj.group(0), '?')
#handle special tags
no_ends = dict.fromkeys(('br p input img area base basefont col '
'frame hr isindex link meta param iframe').split())
no_escape = dict.fromkeys('script raw'.split())
raw = dict.fromkeys('pre'.split())
#the base tag generator
class T(object):
def __getattr__(self, tagname):
return tag(tagname)
T = T()
class tag(object):
__slots__ = ['name', 'attrs', 'contents']
def __init__(self, name, attrs=None, contents=None):
self.name = name.lower()
self.attrs = attrs
self.contents = contents
def __call__(self, *args, **kwargs):
if kwargs and self.attrs:
d = dict(self.attrs)
d.update(kwargs)
kwargs = d
__klass = kwargs.pop('klass', None)
if __klass:
kwargs['class'] = __klass
if args and kwargs:
return tag(self.name, kwargs, args)
elif kwargs:
return tag(self.name, kwargs, self.contents)
elif args:
return tag(self.name, self.attrs, args)
return self
def __setitem__(self, key, value):
if isinstance(key, basestring):
if self.attrs is None:
self.attrs = {}
self.attrs[key] = value
else:
raise TypeError('attribute assignments must only be to named attributes')
def __getitem__(self, key):
if isinstance(key, (int, long)):
if not self.contents:
raise IndexError('tuple index out of range')
return self.contents[key]
raise TypeError('content fetch must only be from indexed attributes')
def render(self, where=None, called=0):
if where is None:
x = cStringIO.StringIO()
self.render(x)
x.seek(0)
return x.read()
if self.name != 'raw':
if self.attrs:
x = []
for key, value in self.attrs.iteritems():
x.append("%s='%s'"%(key, value))
where.write('\n' + called*' ' + '<%s %s>'%(self.name, ' '.join(x).encode('utf-8')))
else:
where.write('\n' + called*' ' + '<%s>'%self.name)
x = where.tell()
if self.contents:
c2n = character2name
for i in self.contents:
if hasattr(i, 'render'):
i.render(where, called+1)
elif self.name in no_escape:
where.write(str(i).encode('utf-8'))
else:
st = str(i)
chrs = dict.fromkeys(st)
for i in chrs:
if i in c2n:
break
else:
chrs = None
if chrs:
#we found something that needs to be translated
st = escape.sub(repl, st)
where.write(st.encode('utf-8'))
if self.name != 'raw' and self.name not in no_ends:
if self.name not in raw and where.tell()-x > 25:
where.write('\n' + called*' ' +'</%s>'%self.name)
else:
where.write('</%s>'%self.name)
if not called:
where.write('\n')
'''
>>> print T.html(
... T.body(
... "hello world", T.br, "how are you?", T.br,
... T.table(*[T.tr(*map(T.td, map(str, range(0+i, 3+i)))) for i in xrang
e(3)])
... )).render()
<html>
<body>hello world
<br>how are you?
<br>
<table>
<tr>
<td>0</td>
<td>1</td>
<td>2</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td>4</td>
</tr>
</table>
</body>
</html>
>>>
>>> x = T.html(
... T.body(bgcolor='red')(
... T.font(size='+1')('Welcome to this wonderful web page!'),
... T.br, "How are you doing today?",
... T.br, T.input(type='text', size='25', value='say something')
... )).render()
>>> print x
<html>
<body bgcolor='red'>
<font size='+1'>Welcome to this wonderful web page!
</font>
<br>How are you doing today?
<br>
<input type='text' value='say something' size='25'>
</body>
</html>
>>>
>>> print T.html(
... T.body(
... T.pre(x))).render()
<html>
<body>
<pre>
<html>
<body bgcolor='red'>
<font size='+1'>Welcome to this wonderful web page!
</font>
<br>How are you doing today?
<br>
<input type='text' value='say something' size='25'>
</body>
</html>
</pre>
</body>
</html>
>>>
>>> def generate_something():
... return T.b("I was generated from a function")
...
>>> print T.html(T.body(generate_something())).render()
<html>
<body>
<b>I was generated from a function
</b>
</body>
</html>
>>>
'''
|
After describing a similar syntax to the above, and seeing Nevow.stan, I took some time and hacked together the above. After finishing, I took a wander through the cookbook and found a few recipes, links, etc., many of whom implement a very similar method, though none really manage to capture multiple calling semantics, and/or the very convenient re-use of pre-attributed tags as I do.
With the use of the non-XHTML tag of 'raw', one can pass through pre-generated html (perhaps embedded ReST -> html, etc.), sets of containers of objects, and various other interesting things. One could even signal to a form processor or somesuch that a particular input needs to be bounds checked on return, etc.
|
Attributes that are Python keywords. I'd like to use the HTML generated from this recipe with CSS classes. The problem is that you can't do T.tag(class="whatevercssclass") because "class" is a Python keyword and using it this way generates an error. Is there a clean way to get around this?
to add attribute 'class' add this chunk of code after line 46 of the code:
then just do this:
spelt with a 'k' so Python does not catch it as a reserved word, and it will output with the correct class="aclass" attribute
I've added this modification to the code. Thank you.
This is a useful bit of code. I really like this recipe, and I even used it in a internal project of mine that never really went anywhere.
Before I mothball my project I wanted to share back with you my rendition of your code.
I made some small changes to the general code to add some extra name-spacing. I also dropped the need for using tell() to determine if the renderer should wrap, instead relying on counting the number of children in an element and whether those children are themselves tags.
With the dropped dependency on tell(), render() can now be used with sys.stdout passed in as the file object.
I also made some other slight changes to the HTML that is generated to make it more XHTML like (although I do no such verification of this).
Anyway, here is the code. I figure it better to post it in a comment, rather than a recipe (even though comment code formatting sucks), since this is really not a new recipe.
(comment continued...)
(...continued from previous comment)
(comment continued...)
(...continued from previous comment)