1# -*- test-case-name: twisted.web.test.test_template -*- 2# Copyright (c) Twisted Matrix Laboratories. 3# See LICENSE for details. 4 5 6""" 7HTML rendering for twisted.web. 8 9@var VALID_HTML_TAG_NAMES: A list of recognized HTML tag names, used by the 10 L{tag} object. 11 12@var TEMPLATE_NAMESPACE: The XML namespace used to identify attributes and 13 elements used by the templating system, which should be removed from the 14 final output document. 15 16@var tags: A convenience object which can produce L{Tag} objects on demand via 17 attribute access. For example: C{tags.div} is equivalent to C{Tag("div")}. 18 Tags not specified in L{VALID_HTML_TAG_NAMES} will result in an 19 L{AttributeError}. 20""" 21 22__all__ = [ 23 'TEMPLATE_NAMESPACE', 'VALID_HTML_TAG_NAMES', 'Element', 'TagLoader', 24 'XMLString', 'XMLFile', 'renderer', 'flatten', 'flattenString', 'tags', 25 'Comment', 'CDATA', 'Tag', 'slot', 'CharRef', 'renderElement' 26 ] 27 28import warnings 29from zope.interface import implements 30 31from cStringIO import StringIO 32from xml.sax import make_parser, handler 33 34from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef 35from twisted.python.filepath import FilePath 36 37TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1' 38 39from twisted.web.iweb import ITemplateLoader 40from twisted.python import log 41 42# Go read the definition of NOT_DONE_YET. For lulz. This is totally 43# equivalent. And this turns out to be necessary, because trying to import 44# NOT_DONE_YET in this module causes a circular import which we cannot escape 45# from. From which we cannot escape. Etc. glyph is okay with this solution for 46# now, and so am I, as long as this comment stays to explain to future 47# maintainers what it means. ~ C. 48# 49# See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this. 50NOT_DONE_YET = 1 51 52class _NSContext(object): 53 """ 54 A mapping from XML namespaces onto their prefixes in the document. 55 """ 56 57 def __init__(self, parent=None): 58 """ 59 Pull out the parent's namespaces, if there's no parent then default to 60 XML. 61 """ 62 self.parent = parent 63 if parent is not None: 64 self.nss = dict(parent.nss) 65 else: 66 self.nss = {'http://www.w3.org/XML/1998/namespace':'xml'} 67 68 69 def get(self, k, d=None): 70 """ 71 Get a prefix for a namespace. 72 73 @param d: The default prefix value. 74 """ 75 return self.nss.get(k, d) 76 77 78 def __setitem__(self, k, v): 79 """ 80 Proxy through to setting the prefix for the namespace. 81 """ 82 self.nss.__setitem__(k, v) 83 84 85 def __getitem__(self, k): 86 """ 87 Proxy through to getting the prefix for the namespace. 88 """ 89 return self.nss.__getitem__(k) 90 91 92 93class _ToStan(handler.ContentHandler, handler.EntityResolver): 94 """ 95 A SAX parser which converts an XML document to the Twisted STAN 96 Document Object Model. 97 """ 98 99 def __init__(self, sourceFilename): 100 """ 101 @param sourceFilename: the filename to load the XML out of. 102 """ 103 self.sourceFilename = sourceFilename 104 self.prefixMap = _NSContext() 105 self.inCDATA = False 106 107 108 def setDocumentLocator(self, locator): 109 """ 110 Set the document locator, which knows about line and character numbers. 111 """ 112 self.locator = locator 113 114 115 def startDocument(self): 116 """ 117 Initialise the document. 118 """ 119 self.document = [] 120 self.current = self.document 121 self.stack = [] 122 self.xmlnsAttrs = [] 123 124 125 def endDocument(self): 126 """ 127 Document ended. 128 """ 129 130 131 def processingInstruction(self, target, data): 132 """ 133 Processing instructions are ignored. 134 """ 135 136 137 def startPrefixMapping(self, prefix, uri): 138 """ 139 Set up the prefix mapping, which maps fully qualified namespace URIs 140 onto namespace prefixes. 141 142 This gets called before startElementNS whenever an C{xmlns} attribute 143 is seen. 144 """ 145 146 self.prefixMap = _NSContext(self.prefixMap) 147 self.prefixMap[uri] = prefix 148 149 # Ignore the template namespace; we'll replace those during parsing. 150 if uri == TEMPLATE_NAMESPACE: 151 return 152 153 # Add to a list that will be applied once we have the element. 154 if prefix is None: 155 self.xmlnsAttrs.append(('xmlns',uri)) 156 else: 157 self.xmlnsAttrs.append(('xmlns:%s'%prefix,uri)) 158 159 160 def endPrefixMapping(self, prefix): 161 """ 162 "Pops the stack" on the prefix mapping. 163 164 Gets called after endElementNS. 165 """ 166 self.prefixMap = self.prefixMap.parent 167 168 169 def startElementNS(self, namespaceAndName, qname, attrs): 170 """ 171 Gets called when we encounter a new xmlns attribute. 172 173 @param namespaceAndName: a (namespace, name) tuple, where name 174 determines which type of action to take, if the namespace matches 175 L{TEMPLATE_NAMESPACE}. 176 @param qname: ignored. 177 @param attrs: attributes on the element being started. 178 """ 179 180 filename = self.sourceFilename 181 lineNumber = self.locator.getLineNumber() 182 columnNumber = self.locator.getColumnNumber() 183 184 ns, name = namespaceAndName 185 if ns == TEMPLATE_NAMESPACE: 186 if name == 'transparent': 187 name = '' 188 elif name == 'slot': 189 try: 190 # Try to get the default value for the slot 191 default = attrs[(None, 'default')] 192 except KeyError: 193 # If there wasn't one, then use None to indicate no 194 # default. 195 default = None 196 el = slot( 197 attrs[(None, 'name')], default=default, 198 filename=filename, lineNumber=lineNumber, 199 columnNumber=columnNumber) 200 self.stack.append(el) 201 self.current.append(el) 202 self.current = el.children 203 return 204 205 render = None 206 207 attrs = dict(attrs) 208 for k, v in attrs.items(): 209 attrNS, justTheName = k 210 if attrNS != TEMPLATE_NAMESPACE: 211 continue 212 if justTheName == 'render': 213 render = v 214 del attrs[k] 215 216 # nonTemplateAttrs is a dictionary mapping attributes that are *not* in 217 # TEMPLATE_NAMESPACE to their values. Those in TEMPLATE_NAMESPACE were 218 # just removed from 'attrs' in the loop immediately above. The key in 219 # nonTemplateAttrs is either simply the attribute name (if it was not 220 # specified as having a namespace in the template) or prefix:name, 221 # preserving the xml namespace prefix given in the document. 222 223 nonTemplateAttrs = {} 224 for (attrNs, attrName), v in attrs.items(): 225 nsPrefix = self.prefixMap.get(attrNs) 226 if nsPrefix is None: 227 attrKey = attrName 228 else: 229 attrKey = '%s:%s' % (nsPrefix, attrName) 230 nonTemplateAttrs[attrKey] = v 231 232 if ns == TEMPLATE_NAMESPACE and name == 'attr': 233 if not self.stack: 234 # TODO: define a better exception for this? 235 raise AssertionError( 236 '<{%s}attr> as top-level element' % (TEMPLATE_NAMESPACE,)) 237 if 'name' not in nonTemplateAttrs: 238 # TODO: same here 239 raise AssertionError( 240 '<{%s}attr> requires a name attribute' % (TEMPLATE_NAMESPACE,)) 241 el = Tag('', render=render, filename=filename, 242 lineNumber=lineNumber, columnNumber=columnNumber) 243 self.stack[-1].attributes[nonTemplateAttrs['name']] = el 244 self.stack.append(el) 245 self.current = el.children 246 return 247 248 # Apply any xmlns attributes 249 if self.xmlnsAttrs: 250 nonTemplateAttrs.update(dict(self.xmlnsAttrs)) 251 self.xmlnsAttrs = [] 252 253 # Add the prefix that was used in the parsed template for non-template 254 # namespaces (which will not be consumed anyway). 255 if ns != TEMPLATE_NAMESPACE and ns is not None: 256 prefix = self.prefixMap[ns] 257 if prefix is not None: 258 name = '%s:%s' % (self.prefixMap[ns],name) 259 el = Tag( 260 name, attributes=dict(nonTemplateAttrs), render=render, 261 filename=filename, lineNumber=lineNumber, 262 columnNumber=columnNumber) 263 self.stack.append(el) 264 self.current.append(el) 265 self.current = el.children 266 267 268 def characters(self, ch): 269 """ 270 Called when we receive some characters. CDATA characters get passed 271 through as is. 272 273 @type ch: C{string} 274 """ 275 if self.inCDATA: 276 self.stack[-1].append(ch) 277 return 278 self.current.append(ch) 279 280 281 def endElementNS(self, name, qname): 282 """ 283 A namespace tag is closed. Pop the stack, if there's anything left in 284 it, otherwise return to the document's namespace. 285 """ 286 self.stack.pop() 287 if self.stack: 288 self.current = self.stack[-1].children 289 else: 290 self.current = self.document 291 292 293 def startDTD(self, name, publicId, systemId): 294 """ 295 DTDs are ignored. 296 """ 297 298 299 def endDTD(self, *args): 300 """ 301 DTDs are ignored. 302 """ 303 304 305 def startCDATA(self): 306 """ 307 We're starting to be in a CDATA element, make a note of this. 308 """ 309 self.inCDATA = True 310 self.stack.append([]) 311 312 313 def endCDATA(self): 314 """ 315 We're no longer in a CDATA element. Collect up the characters we've 316 parsed and put them in a new CDATA object. 317 """ 318 self.inCDATA = False 319 comment = ''.join(self.stack.pop()) 320 self.current.append(CDATA(comment)) 321 322 323 def comment(self, content): 324 """ 325 Add an XML comment which we've encountered. 326 """ 327 self.current.append(Comment(content)) 328 329 330 331def _flatsaxParse(fl): 332 """ 333 Perform a SAX parse of an XML document with the _ToStan class. 334 335 @param fl: The XML document to be parsed. 336 @type fl: A file object or filename. 337 338 @return: a C{list} of Stan objects. 339 """ 340 parser = make_parser() 341 parser.setFeature(handler.feature_validation, 0) 342 parser.setFeature(handler.feature_namespaces, 1) 343 parser.setFeature(handler.feature_external_ges, 0) 344 parser.setFeature(handler.feature_external_pes, 0) 345 346 s = _ToStan(getattr(fl, "name", None)) 347 parser.setContentHandler(s) 348 parser.setEntityResolver(s) 349 parser.setProperty(handler.property_lexical_handler, s) 350 351 parser.parse(fl) 352 353 return s.document 354 355 356class TagLoader(object): 357 """ 358 An L{ITemplateLoader} that loads existing L{IRenderable} providers. 359 360 @ivar tag: The object which will be loaded. 361 @type tag: An L{IRenderable} provider. 362 """ 363 implements(ITemplateLoader) 364 365 def __init__(self, tag): 366 """ 367 @param tag: The object which will be loaded. 368 @type tag: An L{IRenderable} provider. 369 """ 370 self.tag = tag 371 372 373 def load(self): 374 return [self.tag] 375 376 377 378class XMLString(object): 379 """ 380 An L{ITemplateLoader} that loads and parses XML from a string. 381 382 @ivar _loadedTemplate: The loaded document. 383 @type _loadedTemplate: a C{list} of Stan objects. 384 """ 385 implements(ITemplateLoader) 386 387 def __init__(self, s): 388 """ 389 Run the parser on a StringIO copy of the string. 390 391 @param s: The string from which to load the XML. 392 @type s: C{str} 393 """ 394 self._loadedTemplate = _flatsaxParse(StringIO(s)) 395 396 397 def load(self): 398 """ 399 Return the document. 400 401 @return: the loaded document. 402 @rtype: a C{list} of Stan objects. 403 """ 404 return self._loadedTemplate 405 406 407 408class XMLFile(object): 409 """ 410 An L{ITemplateLoader} that loads and parses XML from a file. 411 412 @ivar _loadedTemplate: The loaded document, or C{None}, if not loaded. 413 @type _loadedTemplate: a C{list} of Stan objects, or C{None}. 414 415 @ivar _path: The L{FilePath}, file object, or filename that is being 416 loaded from. 417 """ 418 implements(ITemplateLoader) 419 420 def __init__(self, path): 421 """ 422 Run the parser on a file. 423 424 @param path: The file from which to load the XML. 425 @type path: L{FilePath} 426 """ 427 if not isinstance(path, FilePath): 428 warnings.warn( 429 "Passing filenames or file objects to XMLFile is deprecated " 430 "since Twisted 12.1. Pass a FilePath instead.", 431 category=DeprecationWarning, stacklevel=2) 432 self._loadedTemplate = None 433 self._path = path 434 435 436 def _loadDoc(self): 437 """ 438 Read and parse the XML. 439 440 @return: the loaded document. 441 @rtype: a C{list} of Stan objects. 442 """ 443 if not isinstance(self._path, FilePath): 444 return _flatsaxParse(self._path) 445 else: 446 f = self._path.open('r') 447 try: 448 return _flatsaxParse(f) 449 finally: 450 f.close() 451 452 453 def __repr__(self): 454 return '<XMLFile of %r>' % (self._path,) 455 456 457 def load(self): 458 """ 459 Return the document, first loading it if necessary. 460 461 @return: the loaded document. 462 @rtype: a C{list} of Stan objects. 463 """ 464 if self._loadedTemplate is None: 465 self._loadedTemplate = self._loadDoc() 466 return self._loadedTemplate 467 468 469 470# Last updated October 2011, using W3Schools as a reference. Link: 471# http://www.w3schools.com/html5/html5_reference.asp 472# Note that <xmp> is explicitly omitted; its semantics do not work with 473# t.w.template and it is officially deprecated. 474VALID_HTML_TAG_NAMES = set([ 475 'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside', 476 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote', 477 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 478 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn', 479 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 480 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 481 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 482 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'legend', 483 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes', 484 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 485 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 486 'section', 'select', 'small', 'source', 'span', 'strike', 'strong', 487 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea', 488 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul', 'var', 489 'video', 'wbr', 490]) 491 492 493 494class _TagFactory(object): 495 """ 496 A factory for L{Tag} objects; the implementation of the L{tags} object. 497 498 This allows for the syntactic convenience of C{from twisted.web.html import 499 tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML 500 tag. 501 502 The class is not exposed publicly because you only ever need one of these, 503 and we already made it for you. 504 505 @see: L{tags} 506 """ 507 def __getattr__(self, tagName): 508 if tagName == 'transparent': 509 return Tag('') 510 # allow for E.del as E.del_ 511 tagName = tagName.rstrip('_') 512 if tagName not in VALID_HTML_TAG_NAMES: 513 raise AttributeError('unknown tag %r' % (tagName,)) 514 return Tag(tagName) 515 516 517 518tags = _TagFactory() 519 520 521 522def renderElement(request, element, 523 doctype='<!DOCTYPE html>', _failElement=None): 524 """ 525 Render an element or other C{IRenderable}. 526 527 @param request: The C{Request} being rendered to. 528 @param element: An C{IRenderable} which will be rendered. 529 @param doctype: A C{str} which will be written as the first line of 530 the request, or C{None} to disable writing of a doctype. The C{string} 531 should not include a trailing newline and will default to the HTML5 532 doctype C{'<!DOCTYPE html>'}. 533 534 @returns: NOT_DONE_YET 535 536 @since: 12.1 537 """ 538 if doctype is not None: 539 request.write(doctype) 540 request.write('\n') 541 542 if _failElement is None: 543 _failElement = twisted.web.util.FailureElement 544 545 d = flatten(request, element, request.write) 546 547 def eb(failure): 548 log.err(failure, "An error occurred while rendering the response.") 549 if request.site.displayTracebacks: 550 return flatten(request, _failElement(failure), request.write) 551 else: 552 request.write( 553 ('<div style="font-size:800%;' 554 'background-color:#FFF;' 555 'color:#F00' 556 '">An error occurred while rendering the response.</div>')) 557 558 d.addErrback(eb) 559 d.addBoth(lambda _: request.finish()) 560 return NOT_DONE_YET 561 562 563 564from twisted.web._element import Element, renderer 565from twisted.web._flatten import flatten, flattenString 566import twisted.web.util 567