1 """Code for html rendering
6 # Import the html parser code, maintaing compatibility with older versions of python
7 if (sys.version_info[0] < 3):
8 # Import the old style htmllib parser
10 from htmllib import HTMLParser
12 # Import the new html.parser module
13 from html.parser import HTMLParser
18 from pygame.locals import *
22 _amap = {'left':-1,'right':1,'center':0,None:None,'':None,}
23 _vamap = {'top':-1,'bottom':1,'center':0,'middle':0,None:None,'':None,}
25 # Used by the HTML parser to load external resources (like images). This
26 # class loads content from the local file system. But you can pass your own
27 # resource loader to the HTML parser to find images by other means.
28 class ResourceLoader(object):
29 # Loads an image and returns it as a pygame image
30 def load_image(this, path):
31 return pygame.image.load(path)
39 self.style.font = None
40 self.style.color = None
43 def space(self,v): pass
46 def __init__(self,**params):
47 gui.Color.__init__(self,(0,0,0),**params)
48 def resize(self,width=None,height=None):
49 w,h = self.style.width,self.style.height
50 #if width != None: self.rect.w = width
51 #else: self.rect.w = 1
53 #xt,xr,xb,xl = self.getspacing()
55 if width != None: w = max(w,width)
56 if height != None: h = max(h,height)
60 return w,h #self.container.rect.w,h
62 #self.rect.w = max(1,width,self.container.rect.w-(xl+xr))
67 class _html(HTMLParser):
68 def init(self,doc,font,color,_globals,_locals,loader=None):
74 # Use the default resource loader
75 self.loader = ResourceLoader()
76 self.myopen('document',self.document)
78 self.myfont = self.font = font
79 self.mycolor = self.color = color
83 self._globals = _globals
84 self._locals = _locals
86 def myopen(self,type_,w):
88 self.mystack.append((type_,w))
89 self.type,self.item = type_,w
91 self.font = self.item.style.font
92 self.color = self.item.style.color
94 if not self.font: self.font = self.myfont
95 if not self.color: self.color = self.mycolor
97 def myclose(self, tag):
99 n = len(self.mystack)-1
101 (t, w) = self.mystack[n]
103 # Found the tag in the stack. Drop everything from that tag onwards
105 self.mystack = self.mystack[0:n]
106 # Pop off the parent element, then add it back on to set the
109 t,w = self.mystack.pop()
116 # if len(self.mystack)==0:
117 # # Closing a tag that was never opened
119 # t,w = self.mystack.pop()
120 # t,w = self.mystack.pop()
123 def myback(self,type_):
124 if type(type_) == str: type_ = [type_,]
126 #print 'myback',type_
128 while t not in type_:
129 #if len(self.mystack)==0: return
130 t,w = self.mystack.pop()
134 #clearing out the last </p>
135 if not hasattr(self.item,'layout'): return
136 if len(self.item.layout._widgets) == 0: return
137 w = self.item.layout._widgets[-1]
139 del self.item.layout._widgets[-1]
142 def start_b(self,attrs): self.font.set_bold(1)
143 def end_b(self): self.font.set_bold(0)
144 def start_i(self,attrs): self.font.set_italic(1)
145 def end_i(self): self.font.set_italic(0)
146 def start_u(self,attrs): self.font.set_underline(1)
147 def end_u(self): self.font.set_underline(0)
148 def start_br(self,attrs): self.do_br(attrs)
149 def do_br(self,attrs): self.item.br(self.font.size(" ")[1])
150 def attrs_to_map(self,attrs):
153 for k,v in attrs: r[k] = v
156 def map_to_params(self,r):
157 anum = re.compile("\D")
159 params = {'style':{}}
160 style = params['style']
163 style['background'] = gui.parse_color(r['bgcolor'])
164 if 'background' in r:
165 style['background'] = self.loader.load_image(r['background'])
166 if 'border' in r: style['border'] = int(r['border'])
168 for k in ['width','height','colspan','rowspan','size','min','max']:
169 if k in r: params[k] = int(anum.sub("",r[k]))
171 for k in ['name','value']:
172 if k in r: params[k] = r[k]
174 if 'class' in r: params['cls'] = r['class']
177 params['align'] = _amap[r['align']]
179 params['valign'] = _vamap[r['valign']]
182 for st in r['style'].split(";"):
187 k = k.replace("-","_")
188 k = k.replace(" ","")
189 v = v.replace(" ","")
190 if k == 'color' or k == 'border_color' or k == 'background':
191 v = gui.parse_color(v)
193 v = int(anum.sub("",v))
197 def map_to_connects(self,e,r):
198 for k,evt in [('onclick',gui.CLICK),('onchange',gui.CHANGE)]: #blah blah blah
202 e.connect(evt,self.myexec,(e,r[k]))
204 def start_p(self,attrs):
205 r = self.attrs_to_map(attrs)
206 align = r.get("align","left")
209 self.item.block(_amap[align])
212 if len(self.item.layout._widgets) == 0: return
213 if type(self.item.layout._widgets[-1]) == tuple:
214 w,h = self.item.layout._widgets[-1]
223 def start_block(self,t,attrs,align=-1):
224 r = self.attrs_to_map(attrs)
225 params = self.map_to_params(r)
226 if 'cls' in params: params['cls'] = t+"."+params['cls']
227 else: params['cls'] = t
228 b = gui.Document(**params)
229 b.style.font = self.item.style.font
230 if 'align' in params:
231 align = params['align']
232 self.item.block(align)
238 def end_block(self,t):
242 def start_div(self,attrs): self.start_block('div',attrs)
243 def end_div(self): self.end_block('div')
244 def start_center(self,attrs): self.start_block('div',attrs,0)
245 def end_center(self): self.end_block('div')
247 def start_h1(self,attrs): self.start_block('h1',attrs)
248 def end_h1(self): self.end_block('h1')
249 def start_h2(self,attrs): self.start_block('h2',attrs)
250 def end_h2(self): self.end_block('h2')
251 def start_h3(self,attrs): self.start_block('h3',attrs)
252 def end_h3(self): self.end_block('h3')
253 def start_h4(self,attrs): self.start_block('h4',attrs)
254 def end_h4(self): self.end_block('h4')
255 def start_h5(self,attrs): self.start_block('h5',attrs)
256 def end_h5(self): self.end_block('h5')
257 def start_h6(self,attrs): self.start_block('h6',attrs)
258 def end_h6(self): self.end_block('h6')
260 def start_ul(self,attrs): self.start_block('ul',attrs)
261 def end_ul(self): self.end_block('ul')
262 def start_ol(self,attrs):
263 self.start_block('ol',attrs)
264 self.item.counter = 0
265 def end_ol(self): self.end_block('ol')
266 def start_li(self,attrs):
267 self.myback(['ul','ol'])
269 self.start_block('li',attrs)
270 if hasattr(cur,'counter'):
272 self.handle_data("%d. "%cur.counter)
274 self.handle_data("- ")
275 #def end_li(self): self.end_block('li') #this isn't needed because of how the parser works
277 def start_pre(self,attrs): self.start_block('pre',attrs)
278 def end_pre(self): self.end_block('pre')
279 def start_code(self,attrs): self.start_block('code',attrs)
280 def end_code(self): self.end_block('code')
282 def start_table(self,attrs):
283 r = self.attrs_to_map(attrs)
284 params = self.map_to_params(r)
286 align = r.get("align","left")
287 self.item.block(_amap[align])
289 t = gui.Table(**params)
292 self.myopen('table',t)
294 def start_tr(self,attrs):
298 def _start_td(self,t,attrs):
299 r = self.attrs_to_map(attrs)
300 params = self.map_to_params(r)
301 if 'cls' in params: params['cls'] = t+"."+params['cls']
302 else: params['cls'] = t
303 b = gui.Document(cls=t)
306 self.item.td(b,**params)
309 self.font = self.item.style.font
310 self.color = self.item.style.color
312 def start_td(self,attrs):
313 self._start_td('td',attrs)
315 def start_th(self,attrs):
316 self._start_td('th',attrs)
319 self.myclose('table')
322 def start_form(self,attrs):
323 r = self.attrs_to_map(attrs)
324 e = self.form = gui.Form()
327 self._locals[r.get('id',None)] = e
329 def start_input(self,attrs):
330 r = self.attrs_to_map(attrs)
331 params = self.map_to_params(r) #why bother
334 type_,name,value = r.get('type','text'),r.get('name',None),r.get('value',None)
337 e = gui.Input(**params)
338 self.map_to_connects(e,r)
340 elif type_ == 'radio':
341 if name not in f.groups:
342 f.groups[name] = gui.Group(name=name)
345 e = gui.Radio(group=g,**params)
346 self.map_to_connects(e,r)
348 if 'checked' in r: g.value = value
349 elif type_ == 'checkbox':
350 if name not in f.groups:
351 f.groups[name] = gui.Group(name=name)
354 e = gui.Checkbox(group=g,**params)
355 self.map_to_connects(e,r)
357 if 'checked' in r: g.value = value
359 elif type_ == 'button':
360 e = gui.Button(**params)
361 self.map_to_connects(e,r)
363 elif type_ == 'submit':
364 e = gui.Button(**params)
365 self.map_to_connects(e,r)
367 elif type_ == 'file':
368 e = gui.Input(**params)
369 self.map_to_connects(e,r)
371 b = gui.Button(value='Browse...')
374 d = gui.FileDialog();
375 d.connect(gui.CHANGE,gui.action_setvalue,(d,e))
377 b.connect(gui.CLICK,_browse,None)
379 self._locals[r.get('id',None)] = e
381 def start_object(self,attrs):
382 r = self.attrs_to_map(attrs)
383 params = self.map_to_params(r)
384 # Use eval to automagically get the class being refered
385 cls = eval(r["type"])
387 self.map_to_connects(e,r)
389 self._locals[r.get('id',None)] = e
391 def start_select(self,attrs):
392 r = self.attrs_to_map(attrs)
395 name,value = r.get('name',None),r.get('value',None)
396 e = gui.Select(name=name,value=value,**params)
397 self.map_to_connects(e,r)
399 self.myopen('select',e)
401 def start_option(self,attrs):
402 r = self.attrs_to_map(attrs)
403 params = {} #style = self.map_to_style(r)
405 self.myback('select')
406 e = gui.Document(**params)
407 self.item.add(e,value=r.get('value',None))
408 self.myopen('option',e)
411 def end_select(self):
412 self.myclose('select')
414 def start_hr(self,attrs):
416 def do_hr(self,attrs):
417 h = self.font.size(" ")[1]/2
419 r = self.attrs_to_map(attrs)
420 params = self.map_to_params(r)
421 params['style']['padding'] = h
424 self.item.add(_hr(**params))
427 def anchor_begin(self,href,name,type_):
430 def anchor_end(self):
433 def start_title(self,attrs): self.myopen('title',_flush())
434 def end_title(self): self.myclose('title')
436 def myexec(self,value):
443 def handle_image(self,src,alt,ismap,align,width,height):
445 w = gui.Image(self.loader.load_image(src))
447 self.item.add(w,_amap[align])
451 print('handle_image: missing %s'%src)
453 def handle_data(self,txt):
454 if self.type == 'table': return
455 elif self.type in ('pre','code'):
456 txt = txt.replace("\t"," ")
458 if ss[-1] == "": del ss[-1]
460 img = self.font.render(sentence,1,self.color)
466 txt = re.compile("^[\t\r\n]+").sub("",txt)
467 txt = re.compile("[\t\r\n]+$").sub("",txt)
469 tst = re.compile("[\t\r\n]+").sub("",txt)
472 txt = re.compile("\s+").sub(" ",txt)
476 self.item.space(self.font.size(" "))
479 for word in txt.split(" "):
480 word = word.replace(chr(160)," ") #
482 w = gui.Image(self.font.render(word,1,self.color))
484 self.item.space(self.font.size(" "))
486 if (sys.version_info[0] >= 3):
487 # These functions are for compatibility with python 3, where it seems that HTMLParser
488 # was rewritten to be more general. There is a problem though, since python pre 3
489 # defines these same functions with an extra argument. So we have to include them
490 # conditionally, depending on whether we're using python 2 or 3. Ugh.
491 def handle_starttag(this, tag, attrs):
492 func = getattr(this, "start_" + tag, None)
494 print("ERROR - unrecognized tag %s" % tag)
498 def handle_endtag(this, tag):
499 func = getattr(this, "end_" + tag, None)
503 def start_img(this, attrs):
506 # for (key, value) in attrs:
507 # if (key == "src"): src = value
508 # elif (key == "align"): align = value
509 args = this.attrs_to_map(attrs)
510 src = args.get("src", "")
511 align = args.get("align", "")
512 this.handle_image(src, "", "", align, "", "")
514 _html.handle_starttag = handle_starttag
515 _html.handle_endtag = handle_endtag
516 _html.start_img = start_img
519 class HTML(gui.Document):
524 globals -- global variables (for scripting)
525 locals -- local variables (for scripting)
526 loader -- the resource loader
528 You may access html elements that have an id via widget[id]
531 def __init__(self,data,globals=None,locals=None,loader=None,**params):
532 gui.Document.__init__(self,**params)
533 # This ensures that the whole HTML document is left-aligned within
534 # the rendered surface.
535 self.style.align = -1
537 _globals,_locals = globals,locals
539 if _globals == None: _globals = {}
540 if _locals == None: _locals = {}
541 self._globals = _globals
542 self._locals = _locals
544 #font = gui.theme.get("label","","font")
546 # The constructor is slightly different
550 p.init(self,self.style.font,self.style.color,_globals,_locals,
557 def __getitem__(self,k):
558 return self._locals[k]
560 # Returns a box (pygame rectangle) surrounding all widgets in this document
561 def get_bounding_box(this):
562 minx = miny = sys.maxint
563 maxx = maxy = -sys.maxint
564 for e in this.layout.widgets:
565 minx = min(minx, e.rect.left)
566 miny = min(miny, e.rect.top)
567 maxx = max(maxx, e.rect.right+1)
568 maxy = max(maxy, e.rect.bottom+1)
569 return pygame.Rect(minx, miny, maxx-minx, maxy-miny)
572 def render_ext(font, rect, text, aa, color, bgcolor=(0,0,0,0), **params):
573 """Renders some html and returns the rendered surface, plus the
574 HTML instance that produced it.
577 htm = HTML(text, font=font, color=color, **params)
580 # Make the surface large enough to fit the rendered text
581 htm.resize(width=sys.maxint)
582 (width, height) = htm.get_bounding_box().size
583 # Now set the proper document width (computed from the bounding box)
584 htm.resize(width=width)
585 elif (type(rect) == int):
586 # Fix the width of the document, while the height is variable
588 height = htm.resize(width=width)[1]
590 # Otherwise the width and height of the document is fixed
591 (width, height) = rect.size
592 htm.resize(width=width)
594 # Now construct a surface and paint to it
595 surf = pygame.Surface((width, height)).convert_alpha()
600 def render(font, rect, text, aa, color, bgcolor=(0,0,0,0), **params):
601 """Renders some html"""
602 return render_ext(font, rect, text, aa, color, bgcolor, **params)[0]
604 def rendertrim(font,rect,text,aa,color,bgcolor=(0,0,0,0),**params):
605 """Render html, and make sure to trim the size."""
607 (surf, htm) = render_ext(font, rect, text, aa, color, bgcolor, **params)
608 return surf.subsurface(htm.get_bounding_box())
611 def write(s,font,rect,text,aa=0,color=(0,0,0), **params):
612 """Write html to a surface."""
613 htm = HTML(text, font=font, color=color, **params)
614 htm.resize(width=rect.w)
615 s = s.subsurface(rect)