retrait TODO.
[minwii.git] / src / pgu / html.py
1 """a html renderer
2 """
3
4 import sys
5 import htmllib
6 import re
7 import pygame
8 from pygame.locals import *
9
10 from pgu import gui
11
12 _amap = {'left':-1,'right':1,'center':0,None:None,'':None,}
13 _vamap = {'top':-1,'bottom':1,'center':0,'middle':0,None:None,'':None,}
14
15 # Used by the HTML parser to load external resources (like images). This
16 # class loads content from the local file system. But you can pass your own
17 # resource loader to the HTML parser to find images by other means.
18 class ResourceLoader(object):
19 # Loads an image and returns it as a pygame image
20 def load_image(this, path):
21 return pygame.image.load(path)
22
23 class _dummy:
24 pass
25
26 class _flush:
27 def __init__(self):
28 self.style = _dummy()
29 self.style.font = None
30 self.style.color = None
31 self.cls = None
32 def add(self,w): pass
33 def space(self,v): pass
34
35 class _hr(gui.Color):
36 def __init__(self,**params):
37 gui.Color.__init__(self,(0,0,0),**params)
38 def resize(self,width=None,height=None):
39 w,h = self.style.width,self.style.height
40 #if width != None: self.rect.w = width
41 #else: self.rect.w = 1
42
43 #xt,xr,xb,xl = self.getspacing()
44
45 if width != None: w = max(w,width)
46 if height != None: h = max(h,height)
47 w = max(w,1)
48 h = max(h,1)
49
50 return w,h #self.container.rect.w,h
51
52 #self.rect.w = max(1,width,self.container.rect.w-(xl+xr))
53
54 #print self.rect
55 #self.rect.w = 1
56
57 class _html(htmllib.HTMLParser):
58 def init(self,doc,font,color,_globals,_locals,loader=None):
59 self.mystack = []
60 self.document = doc
61 if (loader):
62 self.loader = loader
63 else:
64 # Use the default resource loader
65 self.loader = ResourceLoader()
66 self.myopen('document',self.document)
67
68 self.myfont = self.font = font
69 self.mycolor = self.color = color
70
71 self.form = None
72
73 self._globals = _globals
74 self._locals = _locals
75
76 def myopen(self,type_,w):
77
78 self.mystack.append((type_,w))
79 self.type,self.item = type_,w
80
81 self.font = self.item.style.font
82 self.color = self.item.style.color
83
84 if not self.font: self.font = self.myfont
85 if not self.color: self.color = self.mycolor
86
87 def myclose(self,type_):
88 t = None
89 self.mydone()
90 while t != type_:
91 #if len(self.mystack)==0: return
92 t,w = self.mystack.pop()
93 t,w = self.mystack.pop()
94 self.myopen(t,w)
95
96 def myback(self,type_):
97 if type(type_) == str: type_ = [type_,]
98 self.mydone()
99 #print 'myback',type_
100 t = None
101 while t not in type_:
102 #if len(self.mystack)==0: return
103 t,w = self.mystack.pop()
104 self.myopen(t,w)
105
106 def mydone(self):
107 #clearing out the last </p>
108 if not hasattr(self.item,'layout'): return
109 if len(self.item.layout._widgets) == 0: return
110 w = self.item.layout._widgets[-1]
111 if type(w) == tuple:
112 del self.item.layout._widgets[-1]
113
114
115 def start_b(self,attrs): self.font.set_bold(1)
116 def end_b(self): self.font.set_bold(0)
117 def start_i(self,attrs): self.font.set_italic(1)
118 def end_i(self): self.font.set_italic(0)
119 def start_u(self,attrs): self.font.set_underline(1)
120 def end_u(self): self.font.set_underline(0)
121 def start_br(self,attrs): self.do_br(attrs)
122 def do_br(self,attrs): self.item.br(self.font.size(" ")[1])
123 def attrs_to_map(self,attrs):
124 k = None
125 r = {}
126 for k,v in attrs: r[k] = v
127 return r
128
129 def map_to_params(self,r):
130 anum = re.compile("\D")
131
132 params = {'style':{}}
133 style = params['style']
134
135 if 'bgcolor' in r:
136 style['background'] = gui.parse_color(r['bgcolor'])
137 if 'background' in r:
138 style['background'] = self.loader.load_image(r['background'])
139 if 'border' in r: style['border'] = int(r['border'])
140
141 for k in ['width','height','colspan','rowspan','size','min','max']:
142 if k in r: params[k] = int(anum.sub("",r[k]))
143
144 for k in ['name','value']:
145 if k in r: params[k] = r[k]
146
147 if 'class' in r: params['cls'] = r['class']
148
149 if 'align' in r:
150 params['align'] = _amap[r['align']]
151 if 'valign' in r:
152 params['valign'] = _vamap[r['valign']]
153
154 if 'style' in r:
155 for st in r['style'].split(";"):
156 #print st
157 if ":" in st:
158 #print st.split(":")
159 k,v = st.split(":")
160 k = k.replace("-","_")
161 k = k.replace(" ","")
162 v = v.replace(" ","")
163 if k == 'color' or k == 'border_color' or k == 'background':
164 v = gui.parse_color(v)
165 else:
166 v = int(anum.sub("",v))
167 style[k] = v
168 return params
169
170 def map_to_connects(self,e,r):
171 for k,evt in [('onclick',gui.CLICK),('onchange',gui.CHANGE)]: #blah blah blah
172
173 if k in r:
174 #print k,r[k]
175 e.connect(evt,self.myexec,(e,r[k]))
176
177 def start_p(self,attrs):
178 r = self.attrs_to_map(attrs)
179 align = r.get("align","left")
180
181 self.check_p()
182 self.item.block(_amap[align])
183
184 def check_p(self):
185 if len(self.item.layout._widgets) == 0: return
186 if type(self.item.layout._widgets[-1]) == tuple:
187 w,h = self.item.layout._widgets[-1]
188 if w == 0: return
189 self.do_br(None)
190
191 def end_p(self):
192 #print 'end p'
193 self.check_p()
194
195
196 def start_block(self,t,attrs,align=-1):
197 r = self.attrs_to_map(attrs)
198 params = self.map_to_params(r)
199 if 'cls' in params: params['cls'] = t+"."+params['cls']
200 else: params['cls'] = t
201 b = gui.Document(**params)
202 b.style.font = self.item.style.font
203 if 'align' in params:
204 align = params['align']
205 self.item.block(align)
206 self.item.add(b)
207 self.myopen(t,b)
208
209
210
211 def end_block(self,t):
212 self.myclose(t)
213 self.item.block(-1)
214
215 def start_div(self,attrs): self.start_block('div',attrs)
216 def end_div(self): self.end_block('div')
217 def start_center(self,attrs): self.start_block('div',attrs,0)
218 def end_center(self): self.end_block('div')
219
220 def start_h1(self,attrs): self.start_block('h1',attrs)
221 def end_h1(self): self.end_block('h1')
222 def start_h2(self,attrs): self.start_block('h2',attrs)
223 def end_h2(self): self.end_block('h2')
224 def start_h3(self,attrs): self.start_block('h3',attrs)
225 def end_h3(self): self.end_block('h3')
226 def start_h4(self,attrs): self.start_block('h4',attrs)
227 def end_h4(self): self.end_block('h4')
228 def start_h5(self,attrs): self.start_block('h5',attrs)
229 def end_h5(self): self.end_block('h5')
230 def start_h6(self,attrs): self.start_block('h6',attrs)
231 def end_h6(self): self.end_block('h6')
232
233 def start_ul(self,attrs): self.start_block('ul',attrs)
234 def end_ul(self): self.end_block('ul')
235 def start_ol(self,attrs):
236 self.start_block('ol',attrs)
237 self.item.counter = 0
238 def end_ol(self): self.end_block('ol')
239 def start_li(self,attrs):
240 self.myback(['ul','ol'])
241 cur = self.item
242 self.start_block('li',attrs)
243 if hasattr(cur,'counter'):
244 cur.counter += 1
245 self.handle_data("%d. "%cur.counter)
246 else:
247 self.handle_data("- ")
248 #def end_li(self): self.end_block('li') #this isn't needed because of how the parser works
249
250 def start_pre(self,attrs): self.start_block('pre',attrs)
251 def end_pre(self): self.end_block('pre')
252 def start_code(self,attrs): self.start_block('code',attrs)
253 def end_code(self): self.end_block('code')
254
255 def start_table(self,attrs):
256 r = self.attrs_to_map(attrs)
257 params = self.map_to_params(r)
258
259 align = r.get("align","left")
260 self.item.block(_amap[align])
261
262 t = gui.Table(**params)
263 self.item.add(t)
264
265 self.myopen('table',t)
266
267 def start_tr(self,attrs):
268 self.myback('table')
269 self.item.tr()
270
271 def _start_td(self,t,attrs):
272 r = self.attrs_to_map(attrs)
273 params = self.map_to_params(r)
274 if 'cls' in params: params['cls'] = t+"."+params['cls']
275 else: params['cls'] = t
276 b = gui.Document(cls=t)
277
278 self.myback('table')
279 self.item.td(b,**params)
280 self.myopen(t,b)
281
282 self.font = self.item.style.font
283 self.color = self.item.style.color
284
285 def start_td(self,attrs):
286 self._start_td('td',attrs)
287
288 def start_th(self,attrs):
289 self._start_td('th',attrs)
290
291 def end_table(self):
292 self.myclose('table')
293 self.item.block(-1)
294
295 def start_form(self,attrs):
296 r = self.attrs_to_map(attrs)
297 e = self.form = gui.Form()
298 e.groups = {}
299
300 self._locals[r.get('id',None)] = e
301
302 def start_input(self,attrs):
303 r = self.attrs_to_map(attrs)
304 params = self.map_to_params(r) #why bother
305 #params = {}
306
307 type_,name,value = r.get('type','text'),r.get('name',None),r.get('value',None)
308 f = self.form
309 if type_ == 'text':
310 e = gui.Input(**params)
311 self.map_to_connects(e,r)
312 self.item.add(e)
313 elif type_ == 'radio':
314 if name not in f.groups:
315 f.groups[name] = gui.Group(name=name)
316 g = f.groups[name]
317 del params['name']
318 e = gui.Radio(group=g,**params)
319 self.map_to_connects(e,r)
320 self.item.add(e)
321 if 'checked' in r: g.value = value
322 elif type_ == 'checkbox':
323 if name not in f.groups:
324 f.groups[name] = gui.Group(name=name)
325 g = f.groups[name]
326 del params['name']
327 e = gui.Checkbox(group=g,**params)
328 self.map_to_connects(e,r)
329 self.item.add(e)
330 if 'checked' in r: g.value = value
331
332 elif type_ == 'button':
333 e = gui.Button(**params)
334 self.map_to_connects(e,r)
335 self.item.add(e)
336 elif type_ == 'submit':
337 e = gui.Button(**params)
338 self.map_to_connects(e,r)
339 self.item.add(e)
340 elif type_ == 'file':
341 e = gui.Input(**params)
342 self.map_to_connects(e,r)
343 self.item.add(e)
344 b = gui.Button(value='Browse...')
345 self.item.add(b)
346 def _browse(value):
347 d = gui.FileDialog();
348 d.connect(gui.CHANGE,gui.action_setvalue,(d,e))
349 d.open();
350 b.connect(gui.CLICK,_browse,None)
351
352 self._locals[r.get('id',None)] = e
353
354 def start_object(self,attrs):
355 r = self.attrs_to_map(attrs)
356 params = self.map_to_params(r)
357 code = "e = %s(**params)"%r['type']
358 #print code
359 #print params
360 exec(code)
361 #print e
362 #print e.style.width,e.style.height
363 self.map_to_connects(e,r)
364 self.item.add(e)
365
366 self._locals[r.get('id',None)] = e
367
368 def start_select(self,attrs):
369 r = self.attrs_to_map(attrs)
370 params = {}
371
372 name,value = r.get('name',None),r.get('value',None)
373 e = gui.Select(name=name,value=value,**params)
374 self.map_to_connects(e,r)
375 self.item.add(e)
376 self.myopen('select',e)
377
378 def start_option(self,attrs):
379 r = self.attrs_to_map(attrs)
380 params = {} #style = self.map_to_style(r)
381
382 self.myback('select')
383 e = gui.Document(**params)
384 self.item.add(e,value=r.get('value',None))
385 self.myopen('option',e)
386
387
388 def end_select(self):
389 self.myclose('select')
390
391 def start_hr(self,attrs):
392 self.do_hr(attrs)
393 def do_hr(self,attrs):
394 h = self.font.size(" ")[1]/2
395
396 r = self.attrs_to_map(attrs)
397 params = self.map_to_params(r)
398 params['style']['padding'] = h
399 print params
400
401 self.item.block(0)
402 self.item.add(_hr(**params))
403 self.item.block(-1)
404
405 def anchor_begin(self,href,name,type_):
406 pass
407
408 def anchor_end(self):
409 pass
410
411 def start_title(self,attrs): self.myopen('title',_flush())
412 def end_title(self): self.myclose('title')
413
414 def myexec(self,value):
415 w,code = value
416 g = self._globals
417 l = self._locals
418 l['self'] = w
419 exec(code,g,l)
420
421 def handle_image(self,src,alt,ismap,align,width,height):
422 try:
423 w = gui.Image(self.loader.load_image(src))
424 if align != '':
425 self.item.add(w,_amap[align])
426 else:
427 self.item.add(w)
428 except:
429 print 'handle_image: missing %s'%src
430
431 def handle_data(self,txt):
432 if self.type == 'table': return
433 elif self.type in ('pre','code'):
434 txt = txt.replace("\t"," ")
435 ss = txt.split("\n")
436 if ss[-1] == "": del ss[-1]
437 for sentence in ss:
438 img = self.font.render(sentence,1,self.color)
439 w = gui.Image(img)
440 self.item.add(w)
441 self.item.block(-1)
442 return
443
444 txt = re.compile("^[\t\r\n]+").sub("",txt)
445 txt = re.compile("[\t\r\n]+$").sub("",txt)
446
447 tst = re.compile("[\t\r\n]+").sub("",txt)
448 if tst == "": return
449
450 txt = re.compile("\s+").sub(" ",txt)
451 if txt == "": return
452
453 if txt == " ":
454 self.item.space(self.font.size(" "))
455 return
456
457 for word in txt.split(" "):
458 word = word.replace(chr(160)," ") #&nbsp;
459 #print self.item.cls
460 w = gui.Image(self.font.render(word,1,self.color))
461 self.item.add(w)
462 self.item.space(self.font.size(" "))
463
464
465 class HTML(gui.Document):
466 """a gui HTML object
467
468 <pre>HTML(data,globals=None,locals=None)</pre>
469
470 <dl>
471 <dt>data <dd>html data
472 <dt>globals <dd>global variables (for scripting)
473 <dt>locals <dd>local variables (for scripting)
474 <dt>loader <dd>the resource loader
475 </dl>
476
477 <p>you may access html elements that have an id via widget[id]</p>
478 """
479 def __init__(self,data,globals=None,locals=None,loader=None,**params):
480 gui.Document.__init__(self,**params)
481 # This ensures that the whole HTML document is left-aligned within
482 # the rendered surface.
483 self.style.align = -1
484
485 _globals,_locals = globals,locals
486
487 if _globals == None: _globals = {}
488 if _locals == None: _locals = {}
489 self._globals = _globals
490 self._locals = _locals
491
492 #font = gui.theme.get("label","","font")
493 p = _html(htmllib.AS_IS,0)
494 p.init(self,self.style.font,self.style.color,_globals,_locals,
495 loader=loader)
496 p.feed(data)
497 p.close()
498 p.mydone()
499
500
501 def __getitem__(self,k):
502 return self._locals[k]
503
504 # Returns a box (pygame rectangle) surrounding all widgets in this document
505 def get_bounding_box(this):
506 minx = miny = sys.maxint
507 maxx = maxy = -sys.maxint
508 for e in this.layout.widgets:
509 minx = min(minx, e.rect.left)
510 miny = min(miny, e.rect.top)
511 maxx = max(maxx, e.rect.right+1)
512 maxy = max(maxy, e.rect.bottom+1)
513 return pygame.Rect(minx, miny, maxx-minx, maxy-miny)
514
515
516 def render_ext(font, rect, text, aa, color, bgcolor=(0,0,0,0), **params):
517 """Renders some html and returns the rendered surface, plus the
518 HTML instance that produced it.
519 """
520
521 htm = HTML(text, font=font, color=color, **params)
522
523 if (rect == -1):
524 # Make the surface large enough to fit the rendered text
525 htm.resize(width=sys.maxint)
526 (width, height) = htm.get_bounding_box().size
527 # Now set the proper document width (computed from the bounding box)
528 htm.resize(width=width)
529 elif (type(rect) == int):
530 # Fix the width of the document, while the height is variable
531 width = rect
532 height = htm.resize(width=width)[1]
533 else:
534 # Otherwise the width and height of the document is fixed
535 (width, height) = rect.size
536 htm.resize(width=width)
537
538 # Now construct a surface and paint to it
539 surf = pygame.Surface((width, height)).convert_alpha()
540 surf.fill(bgcolor)
541 htm.paint(surf)
542 return (surf, htm)
543
544 def render(font, rect, text, aa, color, bgcolor=(0,0,0,0), **params):
545 """Renders some html
546
547 <pre>render(font,rect,text,aa,color,bgcolor=(0,0,0,0))</pre>
548 """
549 return render_ext(font, rect, text, aa, color, bgcolor, **params)[0]
550
551 def rendertrim(font,rect,text,aa,color,bgcolor=(0,0,0,0),**params):
552 """render html, and make sure to trim the size
553
554 rendertrim(font,rect,text,aa,color,bgcolor=(0,0,0,0))
555 """
556 # Render the HTML
557 (surf, htm) = render_ext(font, rect, text, aa, color, bgcolor, **params)
558 return surf.subsurface(htm.get_bounding_box())
559
560
561 def write(s,font,rect,text,aa=0,color=(0,0,0), **params):
562 """write html to a surface
563
564 write(s,font,rect,text,aa=0,color=(0,0,0))
565 """
566 htm = HTML(text, font=font, color=color, **params)
567 htm.resize(width=rect.w)
568 s = s.subsurface(rect)
569 htm.paint(s)
570
571 # vim: set filetype=python sts=4 sw=4 noet si :