Package web2py :: Package gluon :: Module html
[hide private]
[frames] | no frames]

Source Code for Module web2py.gluon.html

   1  #!/usr/bin/env python 
   2  # -*- coding: utf-8 -*- 
   3   
   4  """ 
   5  This file is part of the web2py Web Framework 
   6  Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu> 
   7  License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) 
   8  """ 
   9   
  10  import cgi 
  11  import os 
  12  import re 
  13  import copy 
  14  import types 
  15  import urllib 
  16  import base64 
  17  import sanitizer 
  18  import itertools 
  19  import decoder 
  20  import copy_reg 
  21  import cPickle 
  22  import marshal 
  23   
  24  from HTMLParser import HTMLParser 
  25  from htmlentitydefs import name2codepoint 
  26   
  27  from storage import Storage 
  28  from utils import web2py_uuid, hmac_hash 
  29  from highlight import highlight 
  30   
  31  regex_crlf = re.compile('\r|\n') 
  32   
  33  join = ''.join 
  34   
  35  __all__ = [ 
  36      'A', 
  37      'B', 
  38      'BEAUTIFY', 
  39      'BODY', 
  40      'BR', 
  41      'BUTTON', 
  42      'CENTER', 
  43      'CAT', 
  44      'CODE', 
  45      'COL', 
  46      'COLGROUP', 
  47      'DIV', 
  48      'EM', 
  49      'EMBED', 
  50      'FIELDSET', 
  51      'FORM', 
  52      'H1', 
  53      'H2', 
  54      'H3', 
  55      'H4', 
  56      'H5', 
  57      'H6', 
  58      'HEAD', 
  59      'HR', 
  60      'HTML', 
  61      'I', 
  62      'IFRAME', 
  63      'IMG', 
  64      'INPUT', 
  65      'LABEL', 
  66      'LEGEND', 
  67      'LI', 
  68      'LINK', 
  69      'OL', 
  70      'UL', 
  71      'MARKMIN', 
  72      'MENU', 
  73      'META', 
  74      'OBJECT', 
  75      'ON', 
  76      'OPTION', 
  77      'P', 
  78      'PRE', 
  79      'SCRIPT', 
  80      'OPTGROUP', 
  81      'SELECT', 
  82      'SPAN', 
  83      'STYLE', 
  84      'TABLE', 
  85      'TAG', 
  86      'TD', 
  87      'TEXTAREA', 
  88      'TH', 
  89      'THEAD', 
  90      'TBODY', 
  91      'TFOOT', 
  92      'TITLE', 
  93      'TR', 
  94      'TT', 
  95      'URL', 
  96      'XHTML', 
  97      'XML', 
  98      'xmlescape', 
  99      'embed64', 
 100      ] 
 101   
 102   
103 -def xmlescape(data, quote = True):
104 """ 105 returns an escaped string of the provided data 106 107 :param data: the data to be escaped 108 :param quote: optional (default False) 109 """ 110 111 # first try the xml function 112 if hasattr(data,'xml') and callable(data.xml): 113 return data.xml() 114 115 # otherwise, make it a string 116 if not isinstance(data, (str, unicode)): 117 data = str(data) 118 elif isinstance(data, unicode): 119 data = data.encode('utf8', 'xmlcharrefreplace') 120 121 # ... and do the escaping 122 data = cgi.escape(data, quote).replace("'","&#x27;") 123 return data
124 125
126 -def truncate_string(text, length, dots='...'):
127 text = text.decode('utf-8') 128 if len(text)>length: 129 text = text[:length-len(dots)].encode('utf-8')+dots 130 return text
131
132 -def URL( 133 a=None, 134 c=None, 135 f=None, 136 r=None, 137 args=None, 138 vars=None, 139 anchor='', 140 extension=None, 141 env=None, 142 hmac_key=None, 143 hash_vars=True, 144 salt=None, 145 user_signature=None, 146 scheme=None, 147 host=None, 148 port=None, 149 encode_embedded_slash=False, 150 url_encode=True 151 ):
152 """ 153 generate a URL 154 155 example:: 156 157 >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], 158 ... vars={'p':1, 'q':2}, anchor='1')) 159 '/a/c/f/x/y/z?p=1&q=2#1' 160 161 >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], 162 ... vars={'p':(1,3), 'q':2}, anchor='1')) 163 '/a/c/f/x/y/z?p=1&p=3&q=2#1' 164 165 >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], 166 ... vars={'p':(3,1), 'q':2}, anchor='1')) 167 '/a/c/f/x/y/z?p=3&p=1&q=2#1' 168 169 >>> str(URL(a='a', c='c', f='f', anchor='1+2')) 170 '/a/c/f#1%2B2' 171 172 >>> str(URL(a='a', c='c', f='f', args=['x', 'y', 'z'], 173 ... vars={'p':(1,3), 'q':2}, anchor='1', hmac_key='key')) 174 '/a/c/f/x/y/z?p=1&p=3&q=2&_signature=a32530f0d0caa80964bb92aad2bedf8a4486a31f#1' 175 176 >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'])) 177 '/a/c/f/w/x/y/z' 178 179 >>> str(URL(a='a', c='c', f='f', args=['w/x', 'y/z'], encode_embedded_slash=True)) 180 '/a/c/f/w%2Fx/y%2Fz' 181 182 >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=False)) 183 '/a/c/f/%(id)d' 184 185 >>> str(URL(a='a', c='c', f='f', args=['%(id)d'], url_encode=True)) 186 '/a/c/f/%25%28id%29d' 187 188 >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=False)) 189 '/a/c/f?id=%(id)d' 190 191 >>> str(URL(a='a', c='c', f='f', vars={'id' : '%(id)d' }, url_encode=True)) 192 '/a/c/f?id=%25%28id%29d' 193 194 >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=False)) 195 '/a/c/f#%(id)d' 196 197 >>> str(URL(a='a', c='c', f='f', anchor='%(id)d', url_encode=True)) 198 '/a/c/f#%25%28id%29d' 199 200 generates a url '/a/c/f' corresponding to application a, controller c 201 and function f. If r=request is passed, a, c, f are set, respectively, 202 to r.application, r.controller, r.function. 203 204 The more typical usage is: 205 206 URL(r=request, f='index') that generates a url for the index function 207 within the present application and controller. 208 209 :param a: application (default to current if r is given) 210 :param c: controller (default to current if r is given) 211 :param f: function (default to current if r is given) 212 :param r: request (optional) 213 :param args: any arguments (optional) 214 :param vars: any variables (optional) 215 :param anchor: anchorname, without # (optional) 216 :param hmac_key: key to use when generating hmac signature (optional) 217 :param hash_vars: which of the vars to include in our hmac signature 218 True (default) - hash all vars, False - hash none of the vars, 219 iterable - hash only the included vars ['key1','key2'] 220 :param scheme: URI scheme (True, 'http' or 'https', etc); forces absolute URL (optional) 221 :param host: string to force absolute URL with host (True means http_host) 222 :param port: optional port number (forces absolute URL) 223 224 :raises SyntaxError: when no application, controller or function is 225 available 226 :raises SyntaxError: when a CRLF is found in the generated url 227 """ 228 229 from rewrite import url_out # done here in case used not-in web2py 230 231 if args in (None,[]): args = [] 232 vars = vars or {} 233 application = None 234 controller = None 235 function = None 236 237 if not isinstance(args, (list, tuple)): 238 args = [args] 239 240 if not r: 241 if a and not c and not f: (f,a,c)=(a,c,f) 242 elif a and c and not f: (c,f,a)=(a,c,f) 243 from globals import current 244 if hasattr(current,'request'): 245 r = current.request 246 if r: 247 application = r.application 248 controller = r.controller 249 function = r.function 250 env = r.env 251 if extension is None and r.extension != 'html': 252 extension = r.extension 253 if a: 254 application = a 255 if c: 256 controller = c 257 if f: 258 if not isinstance(f, str): 259 if hasattr(f,'__name__'): 260 function = f.__name__ 261 else: 262 raise SyntaxError, 'when calling URL, function or function name required' 263 elif '/' in f: 264 if f.startswith("/"): 265 f = f[1:] 266 items = f.split('/') 267 function = f = items[0] 268 args = items[1:] + args 269 else: 270 function = f 271 272 # if the url gets a static resource, don't force extention 273 if controller == 'static': 274 extension = None 275 276 if '.' in function: 277 function, extension = function.split('.', 1) 278 279 function2 = '%s.%s' % (function,extension or 'html') 280 281 if not (application and controller and function): 282 raise SyntaxError, 'not enough information to build the url (%s %s %s)' % (application, controller, function) 283 284 if args: 285 if url_encode: 286 if encode_embedded_slash: 287 other = '/' + '/'.join([urllib.quote(str(x), '') for x in args]) 288 else: 289 other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) 290 else: 291 other = args and ('/' + '/'.join([str(x) for x in args])) 292 else: 293 other = '' 294 295 if other.endswith('/'): 296 other += '/' # add trailing slash to make last trailing empty arg explicit 297 298 if vars.has_key('_signature'): vars.pop('_signature') 299 list_vars = [] 300 for (key, vals) in sorted(vars.items()): 301 if not isinstance(vals, (list, tuple)): 302 vals = [vals] 303 for val in vals: 304 list_vars.append((key, val)) 305 306 if user_signature: 307 from globals import current 308 if current.session.auth: 309 hmac_key = current.session.auth.hmac_key 310 311 if hmac_key: 312 # generate an hmac signature of the vars & args so can later 313 # verify the user hasn't messed with anything 314 315 h_args = '/%s/%s/%s%s' % (application, controller, function2, other) 316 317 # how many of the vars should we include in our hash? 318 if hash_vars is True: # include them all 319 h_vars = list_vars 320 elif hash_vars is False: # include none of them 321 h_vars = '' 322 else: # include just those specified 323 if hash_vars and not isinstance(hash_vars, (list, tuple)): 324 hash_vars = [hash_vars] 325 h_vars = [(k, v) for (k, v) in list_vars if k in hash_vars] 326 327 # re-assembling the same way during hash authentication 328 message = h_args + '?' + urllib.urlencode(sorted(h_vars)) 329 330 sig = hmac_hash(message, hmac_key, digest_alg='sha1', salt=salt) 331 # add the signature into vars 332 list_vars.append(('_signature', sig)) 333 334 if list_vars: 335 if url_encode: 336 other += '?%s' % urllib.urlencode(list_vars) 337 else: 338 other += '?%s' % '&'.join([var[0]+'='+var[1] for var in list_vars]) 339 if anchor: 340 if url_encode: 341 other += '#' + urllib.quote(str(anchor)) 342 else: 343 other += '#' + (str(anchor)) 344 if extension: 345 function += '.' + extension 346 347 if regex_crlf.search(join([application, controller, function, other])): 348 raise SyntaxError, 'CRLF Injection Detected' 349 url = url_out(r, env, application, controller, function, 350 args, other, scheme, host, port) 351 return url
352 353
354 -def verifyURL(request, hmac_key=None, hash_vars=True, salt=None, user_signature=None):
355 """ 356 Verifies that a request's args & vars have not been tampered with by the user 357 358 :param request: web2py's request object 359 :param hmac_key: the key to authenticate with, must be the same one previously 360 used when calling URL() 361 :param hash_vars: which vars to include in our hashing. (Optional) 362 Only uses the 1st value currently 363 True (or undefined) means all, False none, 364 an iterable just the specified keys 365 366 do not call directly. Use instead: 367 368 URL.verify(hmac_key='...') 369 370 the key has to match the one used to generate the URL. 371 372 >>> r = Storage() 373 >>> gv = Storage(p=(1,3),q=2,_signature='a32530f0d0caa80964bb92aad2bedf8a4486a31f') 374 >>> r.update(dict(application='a', controller='c', function='f', extension='html')) 375 >>> r['args'] = ['x', 'y', 'z'] 376 >>> r['get_vars'] = gv 377 >>> verifyURL(r, 'key') 378 True 379 >>> verifyURL(r, 'kay') 380 False 381 >>> r.get_vars.p = (3, 1) 382 >>> verifyURL(r, 'key') 383 True 384 >>> r.get_vars.p = (3, 2) 385 >>> verifyURL(r, 'key') 386 False 387 388 """ 389 390 if not request.get_vars.has_key('_signature'): 391 return False # no signature in the request URL 392 393 # check if user_signature requires 394 if user_signature: 395 from globals import current 396 if not current.session or not current.session.auth: 397 return False 398 hmac_key = current.session.auth.hmac_key 399 if not hmac_key: 400 return False 401 402 # get our sig from request.get_vars for later comparison 403 original_sig = request.get_vars._signature 404 405 # now generate a new hmac for the remaining args & vars 406 vars, args = request.get_vars, request.args 407 408 # remove the signature var since it was not part of our signed message 409 request.get_vars.pop('_signature') 410 411 # join all the args & vars into one long string 412 413 # always include all of the args 414 other = args and urllib.quote('/' + '/'.join([str(x) for x in args])) or '' 415 h_args = '/%s/%s/%s.%s%s' % (request.application, 416 request.controller, 417 request.function, 418 request.extension, 419 other) 420 421 # but only include those vars specified (allows more flexibility for use with 422 # forms or ajax) 423 424 list_vars = [] 425 for (key, vals) in sorted(vars.items()): 426 if not isinstance(vals, (list, tuple)): 427 vals = [vals] 428 for val in vals: 429 list_vars.append((key, val)) 430 431 # which of the vars are to be included? 432 if hash_vars is True: # include them all 433 h_vars = list_vars 434 elif hash_vars is False: # include none of them 435 h_vars = '' 436 else: # include just those specified 437 # wrap in a try - if the desired vars have been removed it'll fail 438 try: 439 if hash_vars and not isinstance(hash_vars, (list, tuple)): 440 hash_vars = [hash_vars] 441 h_vars = [(k, v) for (k, v) in list_vars if k in hash_vars] 442 except: 443 # user has removed one of our vars! Immediate fail 444 return False 445 # build the full message string with both args & vars 446 message = h_args + '?' + urllib.urlencode(sorted(h_vars)) 447 448 # hash with the hmac_key provided 449 sig = hmac_hash(message, str(hmac_key), digest_alg='sha1', salt=salt) 450 451 # put _signature back in get_vars just in case a second call to URL.verify is performed 452 # (otherwise it'll immediately return false) 453 request.get_vars['_signature'] = original_sig 454 455 # return whether or not the signature in the request matched the one we just generated 456 # (I.E. was the message the same as the one we originally signed) 457 return original_sig == sig
458 459 URL.verify = verifyURL 460 461 ON = True 462 463
464 -class XmlComponent(object):
465 """ 466 Abstract root for all Html components 467 """ 468 469 # TODO: move some DIV methods to here 470
471 - def xml(self):
472 raise NotImplementedError
473 474
475 -class XML(XmlComponent):
476 """ 477 use it to wrap a string that contains XML/HTML so that it will not be 478 escaped by the template 479 480 example: 481 482 >>> XML('<h1>Hello</h1>').xml() 483 '<h1>Hello</h1>' 484 """ 485
486 - def __init__( 487 self, 488 text, 489 sanitize = False, 490 permitted_tags = [ 491 'a', 492 'b', 493 'blockquote', 494 'br/', 495 'i', 496 'li', 497 'ol', 498 'ul', 499 'p', 500 'cite', 501 'code', 502 'pre', 503 'img/', 504 'h1','h2','h3','h4','h5','h6', 505 'table','tr','td','div', 506 ], 507 allowed_attributes = { 508 'a': ['href', 'title'], 509 'img': ['src', 'alt'], 510 'blockquote': ['type'], 511 'td': ['colspan'], 512 }, 513 ):
514 """ 515 :param text: the XML text 516 :param sanitize: sanitize text using the permitted tags and allowed 517 attributes (default False) 518 :param permitted_tags: list of permitted tags (default: simple list of 519 tags) 520 :param allowed_attributes: dictionary of allowed attributed (default 521 for A, IMG and BlockQuote). 522 The key is the tag; the value is a list of allowed attributes. 523 """ 524 525 if sanitize: 526 text = sanitizer.sanitize(text, permitted_tags, 527 allowed_attributes) 528 if isinstance(text, unicode): 529 text = text.encode('utf8', 'xmlcharrefreplace') 530 elif not isinstance(text, str): 531 text = str(text) 532 self.text = text
533
534 - def xml(self):
535 return self.text
536
537 - def __str__(self):
538 return self.xml()
539
540 - def __add__(self,other):
541 return '%s%s' % (self,other)
542
543 - def __radd__(self,other):
544 return '%s%s' % (other,self)
545
546 - def __cmp__(self,other):
547 return cmp(str(self),str(other))
548
549 - def __hash__(self):
550 return hash(str(self))
551
552 - def __getattr__(self,name):
553 return getattr(str(self),name)
554
555 - def __getitem__(self,i):
556 return str(self)[i]
557
558 - def __getslice__(self,i,j):
559 return str(self)[i:j]
560
561 - def __iter__(self):
562 for c in str(self): yield c
563
564 - def __len__(self):
565 return len(str(self))
566
567 - def flatten(self,render=None):
568 """ 569 return the text stored by the XML object rendered by the render function 570 """ 571 if render: 572 return render(self.text,None,{}) 573 return self.text
574
575 - def elements(self, *args, **kargs):
576 """ 577 to be considered experimental since the behavior of this method is questionable 578 another options could be TAG(self.text).elements(*args,**kargs) 579 """ 580 return []
581 582 ### important to allow safe session.flash=T(....)
583 -def XML_unpickle(data):
584 return marshal.loads(data)
585 -def XML_pickle(data):
586 return XML_unpickle, (marshal.dumps(str(data)),)
587 copy_reg.pickle(XML, XML_pickle, XML_unpickle) 588 589 590
591 -class DIV(XmlComponent):
592 """ 593 HTML helper, for easy generating and manipulating a DOM structure. 594 Little or no validation is done. 595 596 Behaves like a dictionary regarding updating of attributes. 597 Behaves like a list regarding inserting/appending components. 598 599 example:: 600 601 >>> DIV('hello', 'world', _style='color:red;').xml() 602 '<div style=\"color:red;\">helloworld</div>' 603 604 all other HTML helpers are derived from DIV. 605 606 _something=\"value\" attributes are transparently translated into 607 something=\"value\" HTML attributes 608 """ 609 610 # name of the tag, subclasses should update this 611 # tags ending with a '/' denote classes that cannot 612 # contain components 613 tag = 'div' 614
615 - def __init__(self, *components, **attributes):
616 """ 617 :param *components: any components that should be nested in this element 618 :param **attributes: any attributes you want to give to this element 619 620 :raises SyntaxError: when a stand alone tag receives components 621 """ 622 623 if self.tag[-1:] == '/' and components: 624 raise SyntaxError, '<%s> tags cannot have components'\ 625 % self.tag 626 if len(components) == 1 and isinstance(components[0], (list,tuple)): 627 self.components = list(components[0]) 628 else: 629 self.components = list(components) 630 self.attributes = attributes 631 self._fixup() 632 # converts special attributes in components attributes 633 self._postprocessing() 634 self.parent = None 635 for c in self.components: 636 self._setnode(c)
637
638 - def update(self, **kargs):
639 """ 640 dictionary like updating of the tag attributes 641 """ 642 643 for (key, value) in kargs.items(): 644 self[key] = value 645 return self
646
647 - def append(self, value):
648 """ 649 list style appending of components 650 651 >>> a=DIV() 652 >>> a.append(SPAN('x')) 653 >>> print a 654 <div><span>x</span></div> 655 """ 656 self._setnode(value) 657 ret = self.components.append(value) 658 self._fixup() 659 return ret
660
661 - def insert(self, i, value):
662 """ 663 list style inserting of components 664 665 >>> a=DIV() 666 >>> a.insert(0,SPAN('x')) 667 >>> print a 668 <div><span>x</span></div> 669 """ 670 self._setnode(value) 671 ret = self.components.insert(i, value) 672 self._fixup() 673 return ret
674
675 - def __getitem__(self, i):
676 """ 677 gets attribute with name 'i' or component #i. 678 If attribute 'i' is not found returns None 679 680 :param i: index 681 if i is a string: the name of the attribute 682 otherwise references to number of the component 683 """ 684 685 if isinstance(i, str): 686 try: 687 return self.attributes[i] 688 except KeyError: 689 return None 690 else: 691 return self.components[i]
692
693 - def __setitem__(self, i, value):
694 """ 695 sets attribute with name 'i' or component #i. 696 697 :param i: index 698 if i is a string: the name of the attribute 699 otherwise references to number of the component 700 :param value: the new value 701 """ 702 self._setnode(value) 703 if isinstance(i, (str, unicode)): 704 self.attributes[i] = value 705 else: 706 self.components[i] = value
707
708 - def __delitem__(self, i):
709 """ 710 deletes attribute with name 'i' or component #i. 711 712 :param i: index 713 if i is a string: the name of the attribute 714 otherwise references to number of the component 715 """ 716 717 if isinstance(i, str): 718 del self.attributes[i] 719 else: 720 del self.components[i]
721
722 - def __len__(self):
723 """ 724 returns the number of included components 725 """ 726 return len(self.components)
727
728 - def __nonzero__(self):
729 """ 730 always return True 731 """ 732 return True
733
734 - def _fixup(self):
735 """ 736 Handling of provided components. 737 738 Nothing to fixup yet. May be overridden by subclasses, 739 eg for wrapping some components in another component or blocking them. 740 """ 741 return
742
743 - def _wrap_components(self, allowed_parents, 744 wrap_parent = None, 745 wrap_lambda = None):
746 """ 747 helper for _fixup. Checks if a component is in allowed_parents, 748 otherwise wraps it in wrap_parent 749 750 :param allowed_parents: (tuple) classes that the component should be an 751 instance of 752 :param wrap_parent: the class to wrap the component in, if needed 753 :param wrap_lambda: lambda to use for wrapping, if needed 754 755 """ 756 components = [] 757 for c in self.components: 758 if isinstance(c, allowed_parents): 759 pass 760 elif wrap_lambda: 761 c = wrap_lambda(c) 762 else: 763 c = wrap_parent(c) 764 if isinstance(c,DIV): 765 c.parent = self 766 components.append(c) 767 self.components = components
768
769 - def _postprocessing(self):
770 """ 771 Handling of attributes (normally the ones not prefixed with '_'). 772 773 Nothing to postprocess yet. May be overridden by subclasses 774 """ 775 return
776
777 - def _traverse(self, status, hideerror=False):
778 # TODO: docstring 779 newstatus = status 780 for c in self.components: 781 if hasattr(c, '_traverse') and callable(c._traverse): 782 c.vars = self.vars 783 c.request_vars = self.request_vars 784 c.errors = self.errors 785 c.latest = self.latest 786 c.session = self.session 787 c.formname = self.formname 788 if hideerror: c['hideerror'] = hideerror 789 newstatus = c._traverse(status,hideerror) and newstatus 790 791 # for input, textarea, select, option 792 # deal with 'value' and 'validation' 793 794 name = self['_name'] 795 if newstatus: 796 newstatus = self._validate() 797 self._postprocessing() 798 elif 'old_value' in self.attributes: 799 self['value'] = self['old_value'] 800 self._postprocessing() 801 elif name and name in self.vars: 802 self['value'] = self.vars[name] 803 self._postprocessing() 804 if name: 805 self.latest[name] = self['value'] 806 return newstatus
807
808 - def _validate(self):
809 """ 810 nothing to validate yet. May be overridden by subclasses 811 """ 812 return True
813
814 - def _setnode(self,value):
815 if isinstance(value,DIV): 816 value.parent = self
817
818 - def _xml(self):
819 """ 820 helper for xml generation. Returns separately: 821 - the component attributes 822 - the generated xml of the inner components 823 824 Component attributes start with an underscore ('_') and 825 do not have a False or None value. The underscore is removed. 826 A value of True is replaced with the attribute name. 827 828 :returns: tuple: (attributes, components) 829 """ 830 831 # get the attributes for this component 832 # (they start with '_', others may have special meanings) 833 fa = '' 834 for key in sorted(self.attributes): 835 value = self[key] 836 if key[:1] != '_': 837 continue 838 name = key[1:] 839 if value is True: 840 value = name 841 elif value is False or value is None: 842 continue 843 fa += ' %s="%s"' % (name, xmlescape(value, True)) 844 845 # get the xml for the inner components 846 co = join([xmlescape(component) for component in 847 self.components]) 848 849 return (fa, co)
850
851 - def xml(self):
852 """ 853 generates the xml for this component. 854 """ 855 856 (fa, co) = self._xml() 857 858 if not self.tag: 859 return co 860 861 if self.tag[-1:] == '/': 862 # <tag [attributes] /> 863 return '<%s%s />' % (self.tag[:-1], fa) 864 865 # else: <tag [attributes]> inner components xml </tag> 866 return '<%s%s>%s</%s>' % (self.tag, fa, co, self.tag)
867
868 - def __str__(self):
869 """ 870 str(COMPONENT) returns equals COMPONENT.xml() 871 """ 872 873 return self.xml()
874
875 - def flatten(self, render=None):
876 """ 877 return the text stored by the DIV object rendered by the render function 878 the render function must take text, tagname, and attributes 879 render=None is equivalent to render=lambda text, tag, attr: text 880 881 >>> markdown = lambda text,tag=None,attributes={}: \ 882 {None: re.sub('\s+',' ',text), \ 883 'h1':'#'+text+'\\n\\n', \ 884 'p':text+'\\n'}.get(tag,text) 885 >>> a=TAG('<h1>Header</h1><p>this is a test</p>') 886 >>> a.flatten(markdown) 887 '#Header\\n\\nthis is a test\\n' 888 """ 889 890 text = '' 891 for c in self.components: 892 if isinstance(c,XmlComponent): 893 s=c.flatten(render) 894 elif render: 895 s=render(str(c)) 896 else: 897 s=str(c) 898 text+=s 899 if render: 900 text = render(text,self.tag,self.attributes) 901 return text
902 903 regex_tag=re.compile('^[\w\-\:]+') 904 regex_id=re.compile('#([\w\-]+)') 905 regex_class=re.compile('\.([\w\-]+)') 906 regex_attr=re.compile('\[([\w\-\:]+)=(.*?)\]') 907 908
909 - def elements(self, *args, **kargs):
910 """ 911 find all component that match the supplied attribute dictionary, 912 or None if nothing could be found 913 914 All components of the components are searched. 915 916 >>> a = DIV(DIV(SPAN('x'),3,DIV(SPAN('y')))) 917 >>> for c in a.elements('span',first_only=True): c[0]='z' 918 >>> print a 919 <div><div><span>z</span>3<div><span>y</span></div></div></div> 920 >>> for c in a.elements('span'): c[0]='z' 921 >>> print a 922 <div><div><span>z</span>3<div><span>z</span></div></div></div> 923 924 It also supports a syntax compatible with jQuery 925 926 >>> a=TAG('<div><span><a id="1-1" u:v=$>hello</a></span><p class="this is a test">world</p></div>') 927 >>> for e in a.elements('div a#1-1, p.is'): print e.flatten() 928 hello 929 world 930 >>> for e in a.elements('#1-1'): print e.flatten() 931 hello 932 >>> a.elements('a[u:v=$]')[0].xml() 933 '<a id="1-1" u:v="$">hello</a>' 934 935 >>> a=FORM( INPUT(_type='text'), SELECT(range(1)), TEXTAREA() ) 936 >>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled' 937 >>> a.xml() 938 '<form action="" enctype="multipart/form-data" method="post"><input disabled="disabled" type="text" /><select disabled="disabled"><option value="0">0</option></select><textarea cols="40" disabled="disabled" rows="10"></textarea></form>' 939 """ 940 if len(args)==1: 941 args = [a.strip() for a in args[0].split(',')] 942 if len(args)>1: 943 subset = [self.elements(a,**kargs) for a in args] 944 return reduce(lambda a,b:a+b,subset,[]) 945 elif len(args)==1: 946 items = args[0].split() 947 if len(items)>1: 948 subset=[a.elements(' '.join(items[1:]),**kargs) for a in self.elements(items[0])] 949 return reduce(lambda a,b:a+b,subset,[]) 950 else: 951 item=items[0] 952 if '#' in item or '.' in item or '[' in item: 953 match_tag = self.regex_tag.search(item) 954 match_id = self.regex_id.search(item) 955 match_class = self.regex_class.search(item) 956 match_attr = self.regex_attr.finditer(item) 957 args = [] 958 if match_tag: args = [match_tag.group()] 959 if match_id: kargs['_id'] = match_id.group(1) 960 if match_class: kargs['_class'] = re.compile('(?<!\w)%s(?!\w)' % \ 961 match_class.group(1).replace('-','\\-').replace(':','\\:')) 962 for item in match_attr: 963 kargs['_'+item.group(1)]=item.group(2) 964 return self.elements(*args,**kargs) 965 # make a copy of the components 966 matches = [] 967 first_only = False 968 if kargs.has_key("first_only"): 969 first_only = kargs["first_only"] 970 del kargs["first_only"] 971 # check if the component has an attribute with the same 972 # value as provided 973 check = True 974 tag = getattr(self,'tag').replace("/","") 975 if args and tag not in args: 976 check = False 977 for (key, value) in kargs.items(): 978 if isinstance(value,(str,int)): 979 if self[key] != str(value): 980 check = False 981 elif key in self.attributes: 982 if not value.search(str(self[key])): 983 check = False 984 else: 985 check = False 986 if 'find' in kargs: 987 find = kargs['find'] 988 for c in self.components: 989 if isinstance(find,(str,int)): 990 if isinstance(c,str) and str(find) in c: 991 check = True 992 else: 993 if isinstance(c,str) and find.search(c): 994 check = True 995 # if found, return the component 996 if check: 997 matches.append(self) 998 if first_only: 999 return matches 1000 # loop the copy 1001 for c in self.components: 1002 if isinstance(c, XmlComponent): 1003 kargs['first_only'] = first_only 1004 child_matches = c.elements( *args, **kargs ) 1005 if first_only and len(child_matches) != 0: 1006 return child_matches 1007 matches.extend( child_matches ) 1008 return matches
1009 1010
1011 - def element(self, *args, **kargs):
1012 """ 1013 find the first component that matches the supplied attribute dictionary, 1014 or None if nothing could be found 1015 1016 Also the components of the components are searched. 1017 """ 1018 kargs['first_only'] = True 1019 elements = self.elements(*args, **kargs) 1020 if not elements: 1021 # we found nothing 1022 return None 1023 return elements[0]
1024
1025 - def siblings(self,*args,**kargs):
1026 """ 1027 find all sibling components that match the supplied argument list 1028 and attribute dictionary, or None if nothing could be found 1029 """ 1030 sibs = [s for s in self.parent.components if not s == self] 1031 matches = [] 1032 first_only = False 1033 if kargs.has_key("first_only"): 1034 first_only = kargs["first_only"] 1035 del kargs["first_only"] 1036 for c in sibs: 1037 try: 1038 check = True 1039 tag = getattr(c,'tag').replace("/","") 1040 if args and tag not in args: 1041 check = False 1042 for (key, value) in kargs.items(): 1043 if c[key] != value: 1044 check = False 1045 if check: 1046 matches.append(c) 1047 if first_only: break 1048 except: 1049 pass 1050 return matches
1051
1052 - def sibling(self,*args,**kargs):
1053 """ 1054 find the first sibling component that match the supplied argument list 1055 and attribute dictionary, or None if nothing could be found 1056 """ 1057 kargs['first_only'] = True 1058 sibs = self.siblings(*args, **kargs) 1059 if not sibs: 1060 return None 1061 return sibs[0]
1062
1063 -class CAT(DIV):
1064 1065 tag = ''
1066
1067 -def TAG_unpickler(data):
1068 return cPickle.loads(data)
1069
1070 -def TAG_pickler(data):
1071 d = DIV() 1072 d.__dict__ = data.__dict__ 1073 marshal_dump = cPickle.dumps(d) 1074 return (TAG_unpickler, (marshal_dump,))
1075
1076 -class __TAG__(XmlComponent):
1077 1078 """ 1079 TAG factory example:: 1080 1081 >>> print TAG.first(TAG.second('test'), _key = 3) 1082 <first key=\"3\"><second>test</second></first> 1083 1084 """ 1085
1086 - def __getitem__(self, name):
1087 return self.__getattr__(name)
1088
1089 - def __getattr__(self, name):
1090 if name[-1:] == '_': 1091 name = name[:-1] + '/' 1092 if isinstance(name,unicode): 1093 name = name.encode('utf-8') 1094 class __tag__(DIV): 1095 tag = name
1096 copy_reg.pickle(__tag__, TAG_pickler, TAG_unpickler) 1097 return lambda *a, **b: __tag__(*a, **b)
1098
1099 - def __call__(self,html):
1100 return web2pyHTMLParser(decoder.decoder(html)).tree
1101 1102 TAG = __TAG__() 1103 1104
1105 -class HTML(DIV):
1106 """ 1107 There are four predefined document type definitions. 1108 They can be specified in the 'doctype' parameter: 1109 1110 -'strict' enables strict doctype 1111 -'transitional' enables transitional doctype (default) 1112 -'frameset' enables frameset doctype 1113 -'html5' enables HTML 5 doctype 1114 -any other string will be treated as user's own doctype 1115 1116 'lang' parameter specifies the language of the document. 1117 Defaults to 'en'. 1118 1119 See also :class:`DIV` 1120 """ 1121 1122 tag = 'html' 1123 1124 strict = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n' 1125 transitional = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">\n' 1126 frameset = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Frameset//EN" "http://www.w3.org/TR/html4/frameset.dtd">\n' 1127 html5 = '<!DOCTYPE HTML>\n' 1128
1129 - def xml(self):
1130 lang = self['lang'] 1131 if not lang: 1132 lang = 'en' 1133 self.attributes['_lang'] = lang 1134 doctype = self['doctype'] 1135 if doctype: 1136 if doctype == 'strict': 1137 doctype = self.strict 1138 elif doctype == 'transitional': 1139 doctype = self.transitional 1140 elif doctype == 'frameset': 1141 doctype = self.frameset 1142 elif doctype == 'html5': 1143 doctype = self.html5 1144 else: 1145 doctype = '%s\n' % doctype 1146 else: 1147 doctype = self.transitional 1148 (fa, co) = self._xml() 1149 return '%s<%s%s>%s</%s>' % (doctype, self.tag, fa, co, self.tag)
1150
1151 -class XHTML(DIV):
1152 """ 1153 This is XHTML version of the HTML helper. 1154 1155 There are three predefined document type definitions. 1156 They can be specified in the 'doctype' parameter: 1157 1158 -'strict' enables strict doctype 1159 -'transitional' enables transitional doctype (default) 1160 -'frameset' enables frameset doctype 1161 -any other string will be treated as user's own doctype 1162 1163 'lang' parameter specifies the language of the document and the xml document. 1164 Defaults to 'en'. 1165 1166 'xmlns' parameter specifies the xml namespace. 1167 Defaults to 'http://www.w3.org/1999/xhtml'. 1168 1169 See also :class:`DIV` 1170 """ 1171 1172 tag = 'html' 1173 1174 strict = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n' 1175 transitional = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n' 1176 frameset = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n' 1177 xmlns = 'http://www.w3.org/1999/xhtml' 1178
1179 - def xml(self):
1180 xmlns = self['xmlns'] 1181 if xmlns: 1182 self.attributes['_xmlns'] = xmlns 1183 else: 1184 self.attributes['_xmlns'] = self.xmlns 1185 lang = self['lang'] 1186 if not lang: 1187 lang = 'en' 1188 self.attributes['_lang'] = lang 1189 self.attributes['_xml:lang'] = lang 1190 doctype = self['doctype'] 1191 if doctype: 1192 if doctype == 'strict': 1193 doctype = self.strict 1194 elif doctype == 'transitional': 1195 doctype = self.transitional 1196 elif doctype == 'frameset': 1197 doctype = self.frameset 1198 else: 1199 doctype = '%s\n' % doctype 1200 else: 1201 doctype = self.transitional 1202 (fa, co) = self._xml() 1203 return '%s<%s%s>%s</%s>' % (doctype, self.tag, fa, co, self.tag)
1204 1205
1206 -class HEAD(DIV):
1207 1208 tag = 'head'
1209
1210 -class TITLE(DIV):
1211 1212 tag = 'title'
1213 1214
1215 -class META(DIV):
1216 1217 tag = 'meta/'
1218 1219 1223 1224
1225 -class SCRIPT(DIV):
1226 1227 tag = 'script' 1228
1229 - def xml(self):
1230 (fa, co) = self._xml() 1231 # no escaping of subcomponents 1232 co = '\n'.join([str(component) for component in 1233 self.components]) 1234 if co: 1235 # <script [attributes]><!--//--><![CDATA[//><!-- 1236 # script body 1237 # //--><!]]></script> 1238 # return '<%s%s><!--//--><![CDATA[//><!--\n%s\n//--><!]]></%s>' % (self.tag, fa, co, self.tag) 1239 return '<%s%s><!--\n%s\n//--></%s>' % (self.tag, fa, co, self.tag) 1240 else: 1241 return DIV.xml(self)
1242 1243
1244 -class STYLE(DIV):
1245 1246 tag = 'style' 1247
1248 - def xml(self):
1249 (fa, co) = self._xml() 1250 # no escaping of subcomponents 1251 co = '\n'.join([str(component) for component in 1252 self.components]) 1253 if co: 1254 # <style [attributes]><!--/*--><![CDATA[/*><!--*/ 1255 # style body 1256 # /*]]>*/--></style> 1257 return '<%s%s><!--/*--><![CDATA[/*><!--*/\n%s\n/*]]>*/--></%s>' % (self.tag, fa, co, self.tag) 1258 else: 1259 return DIV.xml(self)
1260 1261
1262 -class IMG(DIV):
1263 1264 tag = 'img/'
1265 1266
1267 -class SPAN(DIV):
1268 1269 tag = 'span'
1270 1271
1272 -class BODY(DIV):
1273 1274 tag = 'body'
1275 1276
1277 -class H1(DIV):
1278 1279 tag = 'h1'
1280 1281
1282 -class H2(DIV):
1283 1284 tag = 'h2'
1285 1286
1287 -class H3(DIV):
1288 1289 tag = 'h3'
1290 1291
1292 -class H4(DIV):
1293 1294 tag = 'h4'
1295 1296
1297 -class H5(DIV):
1298 1299 tag = 'h5'
1300 1301
1302 -class H6(DIV):
1303 1304 tag = 'h6'
1305 1306
1307 -class P(DIV):
1308 """ 1309 Will replace ``\\n`` by ``<br />`` if the `cr2br` attribute is provided. 1310 1311 see also :class:`DIV` 1312 """ 1313 1314 tag = 'p' 1315
1316 - def xml(self):
1317 text = DIV.xml(self) 1318 if self['cr2br']: 1319 text = text.replace('\n', '<br />') 1320 return text
1321 1322
1323 -class B(DIV):
1324 1325 tag = 'b'
1326 1327
1328 -class BR(DIV):
1329 1330 tag = 'br/'
1331 1332
1333 -class HR(DIV):
1334 1335 tag = 'hr/'
1336 1337
1338 -class A(DIV):
1339 1340 tag = 'a' 1341
1342 - def xml(self):
1343 if self['delete']: 1344 d = "jQuery(this).closest('%s').remove();" % self['delete'] 1345 else: 1346 d = '' 1347 if self['component']: 1348 self['_onclick']="web2py_component('%s','%s');%sreturn false;" % \ 1349 (self['component'],self['target'] or '',d) 1350 self['_href'] = self['_href'] or '#null' 1351 elif self['callback']: 1352 if d: 1353 self['_onclick']="if(confirm(w2p_ajax_confirm_message||'Are you sure you want o delete this object?')){ajax('%s',[],'%s');%s};return false;" % (self['callback'],self['target'] or '',d) 1354 else: 1355 self['_onclick']="ajax('%s',[],'%s');%sreturn false;" % \ 1356 (self['callback'],self['target'] or '',d) 1357 self['_href'] = self['_href'] or '#null' 1358 elif self['cid']: 1359 self['_onclick']='web2py_component("%s","%s");%sreturn false;' % \ 1360 (self['_href'],self['cid'],d) 1361 return DIV.xml(self)
1362 1363
1364 -class BUTTON(DIV):
1365 1366 tag = 'button'
1367 1368
1369 -class EM(DIV):
1370 1371 tag = 'em'
1372 1373
1374 -class EMBED(DIV):
1375 1376 tag = 'embed/'
1377 1378
1379 -class TT(DIV):
1380 1381 tag = 'tt'
1382 1383
1384 -class PRE(DIV):
1385 1386 tag = 'pre'
1387 1388
1389 -class CENTER(DIV):
1390 1391 tag = 'center'
1392 1393
1394 -class CODE(DIV):
1395 1396 """ 1397 displays code in HTML with syntax highlighting. 1398 1399 :param attributes: optional attributes: 1400 1401 - language: indicates the language, otherwise PYTHON is assumed 1402 - link: can provide a link 1403 - styles: for styles 1404 1405 Example:: 1406 1407 {{=CODE(\"print 'hello world'\", language='python', link=None, 1408 counter=1, styles={}, highlight_line=None)}} 1409 1410 1411 supported languages are \"python\", \"html_plain\", \"c\", \"cpp\", 1412 \"web2py\", \"html\". 1413 The \"html\" language interprets {{ and }} tags as \"web2py\" code, 1414 \"html_plain\" doesn't. 1415 1416 if a link='/examples/global/vars/' is provided web2py keywords are linked to 1417 the online docs. 1418 1419 the counter is used for line numbering, counter can be None or a prompt 1420 string. 1421 """ 1422
1423 - def xml(self):
1424 language = self['language'] or 'PYTHON' 1425 link = self['link'] 1426 counter = self.attributes.get('counter', 1) 1427 highlight_line = self.attributes.get('highlight_line', None) 1428 context_lines = self.attributes.get('context_lines', None) 1429 styles = self['styles'] or {} 1430 return highlight( 1431 join(self.components), 1432 language=language, 1433 link=link, 1434 counter=counter, 1435 styles=styles, 1436 attributes=self.attributes, 1437 highlight_line=highlight_line, 1438 context_lines=context_lines, 1439 )
1440 1441
1442 -class LABEL(DIV):
1443 1444 tag = 'label'
1445 1446
1447 -class LI(DIV):
1448 1449 tag = 'li'
1450 1451
1452 -class UL(DIV):
1453 """ 1454 UL Component. 1455 1456 If subcomponents are not LI-components they will be wrapped in a LI 1457 1458 see also :class:`DIV` 1459 """ 1460 1461 tag = 'ul' 1462
1463 - def _fixup(self):
1464 self._wrap_components(LI, LI)
1465 1466
1467 -class OL(UL):
1468 1469 tag = 'ol'
1470 1471
1472 -class TD(DIV):
1473 1474 tag = 'td'
1475 1476
1477 -class TH(DIV):
1478 1479 tag = 'th'
1480 1481
1482 -class TR(DIV):
1483 """ 1484 TR Component. 1485 1486 If subcomponents are not TD/TH-components they will be wrapped in a TD 1487 1488 see also :class:`DIV` 1489 """ 1490 1491 tag = 'tr' 1492
1493 - def _fixup(self):
1494 self._wrap_components((TD, TH), TD)
1495
1496 -class THEAD(DIV):
1497 1498 tag = 'thead' 1499
1500 - def _fixup(self):
1501 self._wrap_components(TR, TR)
1502 1503
1504 -class TBODY(DIV):
1505 1506 tag = 'tbody' 1507
1508 - def _fixup(self):
1509 self._wrap_components(TR, TR)
1510 1511
1512 -class TFOOT(DIV):
1513 1514 tag = 'tfoot' 1515
1516 - def _fixup(self):
1517 self._wrap_components(TR, TR)
1518 1519
1520 -class COL(DIV):
1521 1522 tag = 'col'
1523 1524
1525 -class COLGROUP(DIV):
1526 1527 tag = 'colgroup'
1528 1529
1530 -class TABLE(DIV):
1531 """ 1532 TABLE Component. 1533 1534 If subcomponents are not TR/TBODY/THEAD/TFOOT-components 1535 they will be wrapped in a TR 1536 1537 see also :class:`DIV` 1538 """ 1539 1540 tag = 'table' 1541
1542 - def _fixup(self):
1544
1545 -class I(DIV):
1546 1547 tag = 'i'
1548
1549 -class IFRAME(DIV):
1550 1551 tag = 'iframe'
1552 1553
1554 -class INPUT(DIV):
1555 1556 """ 1557 INPUT Component 1558 1559 examples:: 1560 1561 >>> INPUT(_type='text', _name='name', value='Max').xml() 1562 '<input name=\"name\" type=\"text\" value=\"Max\" />' 1563 1564 >>> INPUT(_type='checkbox', _name='checkbox', value='on').xml() 1565 '<input checked=\"checked\" name=\"checkbox\" type=\"checkbox\" value=\"on\" />' 1566 1567 >>> INPUT(_type='radio', _name='radio', _value='yes', value='yes').xml() 1568 '<input checked=\"checked\" name=\"radio\" type=\"radio\" value=\"yes\" />' 1569 1570 >>> INPUT(_type='radio', _name='radio', _value='no', value='yes').xml() 1571 '<input name=\"radio\" type=\"radio\" value=\"no\" />' 1572 1573 the input helper takes two special attributes value= and requires=. 1574 1575 :param value: used to pass the initial value for the input field. 1576 value differs from _value because it works for checkboxes, radio, 1577 textarea and select/option too. 1578 1579 - for a checkbox value should be '' or 'on'. 1580 - for a radio or select/option value should be the _value 1581 of the checked/selected item. 1582 1583 :param requires: should be None, or a validator or a list of validators 1584 for the value of the field. 1585 """ 1586 1587 tag = 'input/' 1588
1589 - def _validate(self):
1590 1591 # # this only changes value, not _value 1592 1593 name = self['_name'] 1594 if name is None or name == '': 1595 return True 1596 name = str(name) 1597 1598 if self['_type'] != 'checkbox': 1599 self['old_value'] = self['value'] or self['_value'] or '' 1600 value = self.request_vars.get(name, '') 1601 self['value'] = value 1602 else: 1603 self['old_value'] = self['value'] or False 1604 value = self.request_vars.get(name) 1605 if isinstance(value, (tuple, list)): 1606 self['value'] = self['_value'] in value 1607 else: 1608 self['value'] = self['_value'] == value 1609 requires = self['requires'] 1610 if requires: 1611 if not isinstance(requires, (list, tuple)): 1612 requires = [requires] 1613 for validator in requires: 1614 (value, errors) = validator(value) 1615 if not errors is None: 1616 self.vars[name] = value 1617 self.errors[name] = errors 1618 break 1619 if not name in self.errors: 1620 self.vars[name] = value 1621 return True 1622 return False
1623
1624 - def _postprocessing(self):
1625 t = self['_type'] 1626 if not t: 1627 t = self['_type'] = 'text' 1628 t = t.lower() 1629 value = self['value'] 1630 if self['_value'] is None: 1631 _value = None 1632 else: 1633 _value = str(self['_value']) 1634 if '_checked' in self.attributes and not 'value' in self.attributes: 1635 pass 1636 elif t == 'checkbox': 1637 if not _value: 1638 _value = self['_value'] = 'on' 1639 if not value: 1640 value = [] 1641 elif value is True: 1642 value = [_value] 1643 elif not isinstance(value,(list,tuple)): 1644 value = str(value).split('|') 1645 self['_checked'] = _value in value and 'checked' or None 1646 elif t == 'radio': 1647 if str(value) == str(_value): 1648 self['_checked'] = 'checked' 1649 else: 1650 self['_checked'] = None 1651 elif t == 'text' or t == 'hidden': 1652 if value is None: 1653 self['value'] = _value 1654 else: 1655 self['_value'] = value
1656
1657 - def xml(self):
1658 name = self.attributes.get('_name', None) 1659 if name and hasattr(self, 'errors') \ 1660 and self.errors.get(name, None) \ 1661 and self['hideerror'] != True: 1662 self['_class'] = (self['_class'] and self['_class']+' ' or '')+'invalidinput' 1663 return DIV.xml(self) + DIV(self.errors[name], _class='error', 1664 errors=None, _id='%s__error' % name).xml() 1665 else: 1666 if self['_class'] and self['_class'].endswith('invalidinput'): 1667 self['_class'] = self['_class'][:-12] 1668 if self['_class'] == '': 1669 self['_class'] = None 1670 return DIV.xml(self)
1671 1672
1673 -class TEXTAREA(INPUT):
1674 1675 """ 1676 example:: 1677 1678 TEXTAREA(_name='sometext', value='blah '*100, requires=IS_NOT_EMPTY()) 1679 1680 'blah blah blah ...' will be the content of the textarea field. 1681 """ 1682 1683 tag = 'textarea' 1684
1685 - def _postprocessing(self):
1686 if not '_rows' in self.attributes: 1687 self['_rows'] = 10 1688 if not '_cols' in self.attributes: 1689 self['_cols'] = 40 1690 if not self['value'] is None: 1691 self.components = [self['value']] 1692 elif self.components: 1693 self['value'] = self.components[0]
1694 1695
1696 -class OPTION(DIV):
1697 1698 tag = 'option' 1699
1700 - def _fixup(self):
1701 if not '_value' in self.attributes: 1702 self.attributes['_value'] = str(self.components[0])
1703 1704
1705 -class OBJECT(DIV):
1706 1707 tag = 'object'
1708
1709 -class OPTGROUP(DIV):
1710 1711 tag = 'optgroup' 1712
1713 - def _fixup(self):
1714 components = [] 1715 for c in self.components: 1716 if isinstance(c, OPTION): 1717 components.append(c) 1718 else: 1719 components.append(OPTION(c, _value=str(c))) 1720 self.components = components
1721 1722
1723 -class SELECT(INPUT):
1724 1725 """ 1726 example:: 1727 1728 >>> from validators import IS_IN_SET 1729 >>> SELECT('yes', 'no', _name='selector', value='yes', 1730 ... requires=IS_IN_SET(['yes', 'no'])).xml() 1731 '<select name=\"selector\"><option selected=\"selected\" value=\"yes\">yes</option><option value=\"no\">no</option></select>' 1732 1733 """ 1734 1735 tag = 'select' 1736
1737 - def _fixup(self):
1738 components = [] 1739 for c in self.components: 1740 if isinstance(c, (OPTION, OPTGROUP)): 1741 components.append(c) 1742 else: 1743 components.append(OPTION(c, _value=str(c))) 1744 self.components = components
1745
1746 - def _postprocessing(self):
1747 component_list = [] 1748 for c in self.components: 1749 if isinstance(c, OPTGROUP): 1750 component_list.append(c.components) 1751 else: 1752 component_list.append([c]) 1753 options = itertools.chain(*component_list) 1754 1755 value = self['value'] 1756 if not value is None: 1757 if not self['_multiple']: 1758 for c in options: # my patch 1759 if value and str(c['_value'])==str(value): 1760 c['_selected'] = 'selected' 1761 else: 1762 c['_selected'] = None 1763 else: 1764 if isinstance(value,(list,tuple)): 1765 values = [str(item) for item in value] 1766 else: 1767 values = [str(value)] 1768 for c in options: # my patch 1769 if value and str(c['_value']) in values: 1770 c['_selected'] = 'selected' 1771 else: 1772 c['_selected'] = None
1773 1774
1775 -class FIELDSET(DIV):
1776 1777 tag = 'fieldset'
1778 1779
1780 -class LEGEND(DIV):
1781 1782 tag = 'legend'
1783 1784
1785 -class FORM(DIV):
1786 1787 """ 1788 example:: 1789 1790 >>> from validators import IS_NOT_EMPTY 1791 >>> form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY())) 1792 >>> form.xml() 1793 '<form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"test\" type=\"text\" /></form>' 1794 1795 a FORM is container for INPUT, TEXTAREA, SELECT and other helpers 1796 1797 form has one important method:: 1798 1799 form.accepts(request.vars, session) 1800 1801 if form is accepted (and all validators pass) form.vars contains the 1802 accepted vars, otherwise form.errors contains the errors. 1803 in case of errors the form is modified to present the errors to the user. 1804 """ 1805 1806 tag = 'form' 1807
1808 - def __init__(self, *components, **attributes):
1809 DIV.__init__(self, *components, **attributes) 1810 self.vars = Storage() 1811 self.errors = Storage() 1812 self.latest = Storage() 1813 self.accepted = None # none for not submitted
1814
1815 - def assert_status(self, status, request_vars):
1816 return status
1817
1818 - def accepts( 1819 self, 1820 request_vars, 1821 session=None, 1822 formname='default', 1823 keepvalues=False, 1824 onvalidation=None, 1825 hideerror=False, 1826 **kwargs 1827 ):
1828 """ 1829 kwargs is not used but allows to specify the same interface for FROM and SQLFORM 1830 """ 1831 if request_vars.__class__.__name__ == 'Request': 1832 request_vars=request_vars.post_vars 1833 self.errors.clear() 1834 self.request_vars = Storage() 1835 self.request_vars.update(request_vars) 1836 self.session = session 1837 self.formname = formname 1838 self.keepvalues = keepvalues 1839 1840 # if this tag is a form and we are in accepting mode (status=True) 1841 # check formname and formkey 1842 1843 status = True 1844 if self.session: 1845 formkey = self.session.get('_formkey[%s]' % self.formname, None) 1846 # check if user tampering with form and void CSRF 1847 if formkey != self.request_vars._formkey: 1848 status = False 1849 if self.formname != self.request_vars._formname: 1850 status = False 1851 if status and self.session: 1852 # check if editing a record that has been modified by the server 1853 if hasattr(self,'record_hash') and self.record_hash != formkey: 1854 status = False 1855 self.record_changed = True 1856 status = self._traverse(status,hideerror) 1857 status = self.assert_status(status, request_vars) 1858 if onvalidation: 1859 if isinstance(onvalidation, dict): 1860 onsuccess = onvalidation.get('onsuccess', None) 1861 onfailure = onvalidation.get('onfailure', None) 1862 if onsuccess and status: 1863 onsuccess(self) 1864 if onfailure and request_vars and not status: 1865 onfailure(self) 1866 status = len(self.errors) == 0 1867 elif status: 1868 if isinstance(onvalidation, (list, tuple)): 1869 [f(self) for f in onvalidation] 1870 else: 1871 onvalidation(self) 1872 if self.errors: 1873 status = False 1874 if not session is None: 1875 if hasattr(self,'record_hash'): 1876 formkey = self.record_hash 1877 else: 1878 formkey = web2py_uuid() 1879 self.formkey = session['_formkey[%s]' % formname] = formkey 1880 if status and not keepvalues: 1881 self._traverse(False,hideerror) 1882 self.accepted = status 1883 return status
1884
1885 - def _postprocessing(self):
1886 if not '_action' in self.attributes: 1887 self['_action'] = '' 1888 if not '_method' in self.attributes: 1889 self['_method'] = 'post' 1890 if not '_enctype' in self.attributes: 1891 self['_enctype'] = 'multipart/form-data'
1892
1893 - def hidden_fields(self):
1894 c = [] 1895 if 'hidden' in self.attributes: 1896 for (key, value) in self.attributes.get('hidden',{}).items(): 1897 c.append(INPUT(_type='hidden', _name=key, _value=value)) 1898 1899 if hasattr(self, 'formkey') and self.formkey: 1900 c.append(INPUT(_type='hidden', _name='_formkey', 1901 _value=self.formkey)) 1902 if hasattr(self, 'formname') and self.formname: 1903 c.append(INPUT(_type='hidden', _name='_formname', 1904 _value=self.formname)) 1905 return DIV(c, _class="hidden")
1906
1907 - def xml(self):
1908 newform = FORM(*self.components, **self.attributes) 1909 hidden_fields = self.hidden_fields() 1910 if hidden_fields.components: 1911 newform.append(hidden_fields) 1912 return DIV.xml(newform)
1913
1914 - def validate(self,**kwargs):
1915 """ 1916 This function validates the form, 1917 you can use it instead of directly form.accepts. 1918 1919 Usage: 1920 In controller 1921 1922 def action(): 1923 form=FORM(INPUT(_name=\"test\", requires=IS_NOT_EMPTY())) 1924 form.validate() #you can pass some args here - see below 1925 return dict(form=form) 1926 1927 This can receive a bunch of arguments 1928 1929 onsuccess = 'flash' - will show message_onsuccess in response.flash 1930 None - will do nothing 1931 can be a function (lambda form: pass) 1932 onfailure = 'flash' - will show message_onfailure in response.flash 1933 None - will do nothing 1934 can be a function (lambda form: pass) 1935 message_onsuccess 1936 message_onfailure 1937 next = where to redirect in case of success 1938 any other kwargs will be passed for form.accepts(...) 1939 """ 1940 from gluon import current, redirect 1941 kwargs['request_vars'] = kwargs.get('request_vars',current.request.post_vars) 1942 kwargs['session'] = kwargs.get('session',current.session) 1943 kwargs['dbio'] = kwargs.get('dbio',False) # necessary for SQLHTML forms 1944 1945 onsuccess = kwargs.get('onsuccess','flash') 1946 onfailure = kwargs.get('onfailure','flash') 1947 message_onsuccess = kwargs.get('message_onsuccess', 1948 current.T("Success!")) 1949 message_onfailure = kwargs.get('message_onfailure', 1950 current.T("Errors in form, please check it out.")) 1951 next = kwargs.get('next',None) 1952 for key in ('message_onsuccess','message_onfailure','onsuccess', 1953 'onfailure','next'): 1954 if key in kwargs: 1955 del kwargs[key] 1956 1957 if self.accepts(**kwargs): 1958 if onsuccess == 'flash': 1959 if next: 1960 current.session.flash = message_onsuccess 1961 else: 1962 current.response.flash = message_onsuccess 1963 elif callable(onsuccess): 1964 onsuccess(self) 1965 if next: 1966 if self.vars: 1967 for key,value in self.vars.items(): 1968 next = next.replace('[%s]' % key, 1969 urllib.quote(str(value))) 1970 if not next.startswith('/'): 1971 next = URL(next) 1972 redirect(next) 1973 return True 1974 elif self.errors: 1975 if onfailure == 'flash': 1976 current.response.flash = message_onfailure 1977 elif callable(onfailure): 1978 onfailure(self) 1979 return False
1980
1981 - def process(self, **kwargs):
1982 """ 1983 Perform the .validate() method but returns the form 1984 1985 Usage in controllers: 1986 # directly on return 1987 def action(): 1988 #some code here 1989 return dict(form=FORM(...).process(...)) 1990 1991 You can use it with FORM, SQLFORM or FORM based plugins 1992 1993 Examples: 1994 #response.flash messages 1995 def action(): 1996 form = SQLFORM(db.table).process(message_onsuccess='Sucess!') 1997 retutn dict(form=form) 1998 1999 # callback function 2000 # callback receives True or False as first arg, and a list of args. 2001 def my_callback(status, msg): 2002 response.flash = "Success! "+msg if status else "Errors occured" 2003 2004 # after argument can be 'flash' to response.flash messages 2005 # or a function name to use as callback or None to do nothing. 2006 def action(): 2007 return dict(form=SQLFORM(db.table).process(onsuccess=my_callback) 2008 """ 2009 kwargs['dbio'] = kwargs.get('dbio',True) # necessary for SQLHTML forms 2010 self.validate(**kwargs) 2011 return self
2012
2013 - def add_button(self,value,url,_class=None):
2014 self[0][-1][1].append(INPUT(_type="button",_value=value,_onclick="window.location='%s';return false" % url,_class=_class))
2015
2016 -class BEAUTIFY(DIV):
2017 2018 """ 2019 example:: 2020 2021 >>> BEAUTIFY(['a', 'b', {'hello': 'world'}]).xml() 2022 '<div><table><tr><td><div>a</div></td></tr><tr><td><div>b</div></td></tr><tr><td><div><table><tr><td style="font-weight:bold;vertical-align:top">hello</td><td valign="top">:</td><td><div>world</div></td></tr></table></div></td></tr></table></div>' 2023 2024 turns any list, dictionary, etc into decent looking html. 2025 Two special attributes are 2026 :sorted: a function that takes the dict and returned sorted keys 2027 :keyfilter: a funciton that takes a key and returns its representation 2028 or None if the key is to be skipped. By default key[:1]=='_' is skipped. 2029 """ 2030 2031 tag = 'div' 2032 2033 @staticmethod
2034 - def no_underscore(key):
2035 if key[:1]=='_': 2036 return None 2037 return key
2038
2039 - def __init__(self, component, **attributes):
2040 self.components = [component] 2041 self.attributes = attributes 2042 sorter = attributes.get('sorted',sorted) 2043 keyfilter = attributes.get('keyfilter',BEAUTIFY.no_underscore) 2044 components = [] 2045 attributes = copy.copy(self.attributes) 2046 level = attributes['level'] = attributes.get('level',6) - 1 2047 if '_class' in attributes: 2048 attributes['_class'] += 'i' 2049 if level == 0: 2050 return 2051 for c in self.components: 2052 if hasattr(c,'xml') and callable(c.xml): 2053 components.append(c) 2054 continue 2055 elif hasattr(c,'keys') and callable(c.keys): 2056 rows = [] 2057 try: 2058 keys = (sorter and sorter(c)) or c 2059 for key in keys: 2060 if isinstance(key,(str,unicode)) and keyfilter: 2061 filtered_key = keyfilter(key) 2062 else: 2063 filtered_key = str(key) 2064 if filtered_key is None: 2065 continue 2066 value = c[key] 2067 if type(value) == types.LambdaType: 2068 continue 2069 rows.append(TR(TD(filtered_key, _style='font-weight:bold;vertical-align:top'), 2070 TD(':',_valign='top'), 2071 TD(BEAUTIFY(value, **attributes)))) 2072 components.append(TABLE(*rows, **attributes)) 2073 continue 2074 except: 2075 pass 2076 if isinstance(c, str): 2077 components.append(str(c)) 2078 elif isinstance(c, unicode): 2079 components.append(c.encode('utf8')) 2080 elif isinstance(c, (list, tuple)): 2081 items = [TR(TD(BEAUTIFY(item, **attributes))) 2082 for item in c] 2083 components.append(TABLE(*items, **attributes)) 2084 elif isinstance(c, cgi.FieldStorage): 2085 components.append('FieldStorage object') 2086 else: 2087 components.append(repr(c)) 2088 self.components = components
2089 2090 2165 2166
2167 -def embed64( 2168 filename = None, 2169 file = None, 2170 data = None, 2171 extension = 'image/gif', 2172 ):
2173 """ 2174 helper to encode the provided (binary) data into base64. 2175 2176 :param filename: if provided, opens and reads this file in 'rb' mode 2177 :param file: if provided, reads this file 2178 :param data: if provided, uses the provided data 2179 """ 2180 2181 if filename and os.path.exists(file): 2182 fp = open(filename, 'rb') 2183 data = fp.read() 2184 fp.close() 2185 data = base64.b64encode(data) 2186 return 'data:%s;base64,%s' % (extension, data)
2187 2188
2189 -def test():
2190 """ 2191 Example: 2192 2193 >>> from validators import * 2194 >>> print DIV(A('click me', _href=URL(a='a', c='b', f='c')), BR(), HR(), DIV(SPAN(\"World\"), _class='unknown')).xml() 2195 <div><a href=\"/a/b/c\">click me</a><br /><hr /><div class=\"unknown\"><span>World</span></div></div> 2196 >>> print DIV(UL(\"doc\",\"cat\",\"mouse\")).xml() 2197 <div><ul><li>doc</li><li>cat</li><li>mouse</li></ul></div> 2198 >>> print DIV(UL(\"doc\", LI(\"cat\", _class='feline'), 18)).xml() 2199 <div><ul><li>doc</li><li class=\"feline\">cat</li><li>18</li></ul></div> 2200 >>> print TABLE(['a', 'b', 'c'], TR('d', 'e', 'f'), TR(TD(1), TD(2), TD(3))).xml() 2201 <table><tr><td>a</td><td>b</td><td>c</td></tr><tr><td>d</td><td>e</td><td>f</td></tr><tr><td>1</td><td>2</td><td>3</td></tr></table> 2202 >>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_EXPR('int(value)<10'))) 2203 >>> print form.xml() 2204 <form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" /></form> 2205 >>> print form.accepts({'myvar':'34'}, formname=None) 2206 False 2207 >>> print form.xml() 2208 <form action="" enctype="multipart/form-data" method="post"><input class="invalidinput" name="myvar" type="text" value="34" /><div class="error" id="myvar__error">invalid expression</div></form> 2209 >>> print form.accepts({'myvar':'4'}, formname=None, keepvalues=True) 2210 True 2211 >>> print form.xml() 2212 <form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input name=\"myvar\" type=\"text\" value=\"4\" /></form> 2213 >>> form=FORM(SELECT('cat', 'dog', _name='myvar')) 2214 >>> print form.accepts({'myvar':'dog'}, formname=None, keepvalues=True) 2215 True 2216 >>> print form.xml() 2217 <form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><select name=\"myvar\"><option value=\"cat\">cat</option><option selected=\"selected\" value=\"dog\">dog</option></select></form> 2218 >>> form=FORM(INPUT(_type='text', _name='myvar', requires=IS_MATCH('^\w+$', 'only alphanumeric!'))) 2219 >>> print form.accepts({'myvar':'as df'}, formname=None) 2220 False 2221 >>> print form.xml() 2222 <form action=\"\" enctype=\"multipart/form-data\" method=\"post\"><input class=\"invalidinput\" name=\"myvar\" type=\"text\" value=\"as df\" /><div class=\"error\" id=\"myvar__error\">only alphanumeric!</div></form> 2223 >>> session={} 2224 >>> form=FORM(INPUT(value=\"Hello World\", _name=\"var\", requires=IS_MATCH('^\w+$'))) 2225 >>> if form.accepts({}, session,formname=None): print 'passed' 2226 >>> if form.accepts({'var':'test ', '_formkey': session['_formkey[None]']}, session, formname=None): print 'passed' 2227 """ 2228 pass
2229 2230
2231 -class web2pyHTMLParser(HTMLParser):
2232 """ 2233 obj = web2pyHTMLParser(text) parses and html/xml text into web2py helpers. 2234 obj.tree contains the root of the tree, and tree can be manipulated 2235 2236 >>> str(web2pyHTMLParser('hello<div a="b" c=3>wor&lt;ld<span>xxx</span>y<script/>yy</div>zzz').tree) 2237 'hello<div a="b" c="3">wor&lt;ld<span>xxx</span>y<script></script>yy</div>zzz' 2238 >>> str(web2pyHTMLParser('<div>a<span>b</div>c').tree) 2239 '<div>a<span>b</span></div>c' 2240 >>> tree = web2pyHTMLParser('hello<div a="b">world</div>').tree 2241 >>> tree.element(_a='b')['_c']=5 2242 >>> str(tree) 2243 'hello<div a="b" c="5">world</div>' 2244 """
2245 - def __init__(self,text,closed=('input','link')):
2246 HTMLParser.__init__(self) 2247 self.tree = self.parent = TAG['']() 2248 self.closed = closed 2249 self.tags = [x for x in __all__ if isinstance(eval(x),DIV)] 2250 self.last = None 2251 self.feed(text)
2252 - def handle_starttag(self, tagname, attrs):
2253 if tagname.upper() in self.tags: 2254 tag=eval(tagname.upper()) 2255 else: 2256 if tagname in self.closed: tagname+='/' 2257 tag = TAG[tagname]() 2258 for key,value in attrs: tag['_'+key]=value 2259 tag.parent = self.parent 2260 self.parent.append(tag) 2261 if not tag.tag.endswith('/'): 2262 self.parent=tag 2263 else: 2264 self.last = tag.tag[:-1]
2265 - def handle_data(self,data):
2266 if not isinstance(data,unicode): 2267 try: 2268 data = data.decode('utf8') 2269 except: 2270 data = data.decode('latin1') 2271 self.parent.append(data.encode('utf8','xmlcharref'))
2272 - def handle_charref(self,name):
2273 if name[1].lower()=='x': 2274 self.parent.append(unichr(int(name[2:], 16)).encode('utf8')) 2275 else: 2276 self.parent.append(unichr(int(name[1:], 10)).encode('utf8'))
2277 - def handle_entityref(self,name):
2278 self.parent.append(unichr(name2codepoint[name]).encode('utf8'))
2279 - def handle_endtag(self, tagname):
2280 # this deals with unbalanced tags 2281 if tagname==self.last: 2282 return 2283 while True: 2284 try: 2285 parent_tagname=self.parent.tag 2286 self.parent = self.parent.parent 2287 except: 2288 raise RuntimeError, "unable to balance tag %s" % tagname 2289 if parent_tagname[:len(tagname)]==tagname: break
2290
2291 -def markdown_serializer(text,tag=None,attr=None):
2292 attr = attr or {} 2293 if tag is None: return re.sub('\s+',' ',text) 2294 if tag=='br': return '\n\n' 2295 if tag=='h1': return '#'+text+'\n\n' 2296 if tag=='h2': return '#'*2+text+'\n\n' 2297 if tag=='h3': return '#'*3+text+'\n\n' 2298 if tag=='h4': return '#'*4+text+'\n\n' 2299 if tag=='p': return text+'\n\n' 2300 if tag=='b' or tag=='strong': return '**%s**' % text 2301 if tag=='em' or tag=='i': return '*%s*' % text 2302 if tag=='tt' or tag=='code': return '`%s`' % text 2303 if tag=='a': return '[%s](%s)' % (text,attr.get('_href','')) 2304 if tag=='img': return '![%s](%s)' % (attr.get('_alt',''),attr.get('_src','')) 2305 return text
2306
2307 -def markmin_serializer(text,tag=None,attr=None):
2308 attr = attr or {} 2309 # if tag is None: return re.sub('\s+',' ',text) 2310 if tag=='br': return '\n\n' 2311 if tag=='h1': return '# '+text+'\n\n' 2312 if tag=='h2': return '#'*2+' '+text+'\n\n' 2313 if tag=='h3': return '#'*3+' '+text+'\n\n' 2314 if tag=='h4': return '#'*4+' '+text+'\n\n' 2315 if tag=='p': return text+'\n\n' 2316 if tag=='li': return '\n- '+text.replace('\n',' ') 2317 if tag=='tr': return text[3:].replace('\n',' ')+'\n' 2318 if tag in ['table','blockquote']: return '\n-----\n'+text+'\n------\n' 2319 if tag in ['td','th']: return ' | '+text 2320 if tag in ['b','strong','label']: return '**%s**' % text 2321 if tag in ['em','i']: return "''%s''" % text 2322 if tag in ['tt']: return '``%s``' % text.strip() 2323 if tag in ['code']: return '``\n%s``' % text 2324 if tag=='a': return '[[%s %s]]' % (text,attr.get('_href','')) 2325 if tag=='img': return '[[%s %s left]]' % (attr.get('_alt','no title'),attr.get('_src','')) 2326 return text
2327 2328
2329 -class MARKMIN(XmlComponent):
2330 """ 2331 For documentation: http://web2py.com/examples/static/markmin.html 2332 """
2333 - def __init__(self, text, extra=None, allowed=None, sep='p', 2334 url=None, environment=None, latex='google'):
2335 self.text = text 2336 self.extra = extra or {} 2337 self.allowed = allowed or {} 2338 self.sep = sep 2339 self.url = URL if url==True else url 2340 self.environment = environment 2341 self.latex = latex
2342
2343 - def xml(self):
2344 """ 2345 calls the gluon.contrib.markmin render function to convert the wiki syntax 2346 """ 2347 from contrib.markmin.markmin2html import render 2348 return render(self.text,extra=self.extra, 2349 allowed=self.allowed,sep=self.sep,latex=self.latex, 2350 URL=self.url, environment=self.environment)
2351
2352 - def __str__(self):
2353 return self.xml()
2354
2355 - def flatten(self,render=None):
2356 """ 2357 return the text stored by the MARKMIN object rendered by the render function 2358 """ 2359 return self.text
2360
2361 - def elements(self, *args, **kargs):
2362 """ 2363 to be considered experimental since the behavior of this method is questionable 2364 another options could be TAG(self.text).elements(*args,**kargs) 2365 """ 2366 return [self.text]
2367 2368 2369 if __name__ == '__main__': 2370 import doctest 2371 doctest.testmod() 2372