1 # -*- test-case-name: twisted.web.test.test_template -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
7 HTML rendering for twisted.web.
9 @var VALID_HTML_TAG_NAMES: A list of recognized HTML tag names, used by the
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.
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
23 'TEMPLATE_NAMESPACE', 'VALID_HTML_TAG_NAMES', 'Element', 'TagLoader',
24 'XMLString', 'XMLFile', 'renderer', 'flatten', 'flattenString', 'tags',
25 'Comment', 'CDATA', 'Tag', 'slot', 'CharRef', 'renderElement'
29 from zope.interface import implements
31 from cStringIO import StringIO
32 from xml.sax import make_parser, handler
34 from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef
35 from twisted.python.filepath import FilePath
37 TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1'
39 from twisted.web.iweb import ITemplateLoader
40 from twisted.python import log
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.
49 # See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this.
52 class _NSContext(object):
54 A mapping from XML namespaces onto their prefixes in the document.
57 def __init__(self, parent=None):
59 Pull out the parent's namespaces, if there's no parent then default to
63 if parent is not None:
64 self.nss = dict(parent.nss)
66 self.nss = {'http://www.w3.org/XML/1998/namespace':'xml'}
69 def get(self, k, d=None):
71 Get a prefix for a namespace.
73 @param d: The default prefix value.
75 return self.nss.get(k, d)
78 def __setitem__(self, k, v):
80 Proxy through to setting the prefix for the namespace.
82 self.nss.__setitem__(k, v)
85 def __getitem__(self, k):
87 Proxy through to getting the prefix for the namespace.
89 return self.nss.__getitem__(k)
93 class _ToStan(handler.ContentHandler, handler.EntityResolver):
95 A SAX parser which converts an XML document to the Twisted STAN
96 Document Object Model.
99 def __init__(self, sourceFilename):
101 @param sourceFilename: the filename to load the XML out of.
103 self.sourceFilename = sourceFilename
104 self.prefixMap = _NSContext()
108 def setDocumentLocator(self, locator):
110 Set the document locator, which knows about line and character numbers.
112 self.locator = locator
115 def startDocument(self):
117 Initialise the document.
120 self.current = self.document
125 def endDocument(self):
131 def processingInstruction(self, target, data):
133 Processing instructions are ignored.
137 def startPrefixMapping(self, prefix, uri):
139 Set up the prefix mapping, which maps fully qualified namespace URIs
140 onto namespace prefixes.
142 This gets called before startElementNS whenever an C{xmlns} attribute
146 self.prefixMap = _NSContext(self.prefixMap)
147 self.prefixMap[uri] = prefix
149 # Ignore the template namespace; we'll replace those during parsing.
150 if uri == TEMPLATE_NAMESPACE:
153 # Add to a list that will be applied once we have the element.
155 self.xmlnsAttrs.append(('xmlns',uri))
157 self.xmlnsAttrs.append(('xmlns:%s'%prefix,uri))
160 def endPrefixMapping(self, prefix):
162 "Pops the stack" on the prefix mapping.
164 Gets called after endElementNS.
166 self.prefixMap = self.prefixMap.parent
169 def startElementNS(self, namespaceAndName, qname, attrs):
171 Gets called when we encounter a new xmlns attribute.
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.
180 filename = self.sourceFilename
181 lineNumber = self.locator.getLineNumber()
182 columnNumber = self.locator.getColumnNumber()
184 ns, name = namespaceAndName
185 if ns == TEMPLATE_NAMESPACE:
186 if name == 'transparent':
190 # Try to get the default value for the slot
191 default = attrs[(None, 'default')]
193 # If there wasn't one, then use None to indicate no
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
208 for k, v in attrs.items():
209 attrNS, justTheName = k
210 if attrNS != TEMPLATE_NAMESPACE:
212 if justTheName == 'render':
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.
223 nonTemplateAttrs = {}
224 for (attrNs, attrName), v in attrs.items():
225 nsPrefix = self.prefixMap.get(attrNs)
229 attrKey = '%s:%s' % (nsPrefix, attrName)
230 nonTemplateAttrs[attrKey] = v
232 if ns == TEMPLATE_NAMESPACE and name == 'attr':
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:
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
248 # Apply any xmlns attributes
250 nonTemplateAttrs.update(dict(self.xmlnsAttrs))
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)
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
268 def characters(self, ch):
270 Called when we receive some characters. CDATA characters get passed
276 self.stack[-1].append(ch)
278 self.current.append(ch)
281 def endElementNS(self, name, qname):
283 A namespace tag is closed. Pop the stack, if there's anything left in
284 it, otherwise return to the document's namespace.
288 self.current = self.stack[-1].children
290 self.current = self.document
293 def startDTD(self, name, publicId, systemId):
299 def endDTD(self, *args):
305 def startCDATA(self):
307 We're starting to be in a CDATA element, make a note of this.
310 self.stack.append([])
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.
319 comment = ''.join(self.stack.pop())
320 self.current.append(CDATA(comment))
323 def comment(self, content):
325 Add an XML comment which we've encountered.
327 self.current.append(Comment(content))
331 def _flatsaxParse(fl):
333 Perform a SAX parse of an XML document with the _ToStan class.
335 @param fl: The XML document to be parsed.
336 @type fl: A file object or filename.
338 @return: a C{list} of Stan objects.
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)
346 s = _ToStan(getattr(fl, "name", None))
347 parser.setContentHandler(s)
348 parser.setEntityResolver(s)
349 parser.setProperty(handler.property_lexical_handler, s)
356 class TagLoader(object):
358 An L{ITemplateLoader} that loads existing L{IRenderable} providers.
360 @ivar tag: The object which will be loaded.
361 @type tag: An L{IRenderable} provider.
363 implements(ITemplateLoader)
365 def __init__(self, tag):
367 @param tag: The object which will be loaded.
368 @type tag: An L{IRenderable} provider.
378 class XMLString(object):
380 An L{ITemplateLoader} that loads and parses XML from a string.
382 @ivar _loadedTemplate: The loaded document.
383 @type _loadedTemplate: a C{list} of Stan objects.
385 implements(ITemplateLoader)
387 def __init__(self, s):
389 Run the parser on a StringIO copy of the string.
391 @param s: The string from which to load the XML.
394 self._loadedTemplate = _flatsaxParse(StringIO(s))
401 @return: the loaded document.
402 @rtype: a C{list} of Stan objects.
404 return self._loadedTemplate
408 class XMLFile(object):
410 An L{ITemplateLoader} that loads and parses XML from a file.
412 @ivar _loadedTemplate: The loaded document, or C{None}, if not loaded.
413 @type _loadedTemplate: a C{list} of Stan objects, or C{None}.
415 @ivar _path: The L{FilePath}, file object, or filename that is being
418 implements(ITemplateLoader)
420 def __init__(self, path):
422 Run the parser on a file.
424 @param path: The file from which to load the XML.
425 @type path: L{FilePath}
427 if not isinstance(path, FilePath):
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
438 Read and parse the XML.
440 @return: the loaded document.
441 @rtype: a C{list} of Stan objects.
443 if not isinstance(self._path, FilePath):
444 return _flatsaxParse(self._path)
446 f = self._path.open('r')
448 return _flatsaxParse(f)
454 return '<XMLFile of %r>' % (self._path,)
459 Return the document, first loading it if necessary.
461 @return: the loaded document.
462 @rtype: a C{list} of Stan objects.
464 if self._loadedTemplate is None:
465 self._loadedTemplate = self._loadDoc()
466 return self._loadedTemplate
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.
474 VALID_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',
494 class _TagFactory(object):
496 A factory for L{Tag} objects; the implementation of the L{tags} object.
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
502 The class is not exposed publicly because you only ever need one of these,
503 and we already made it for you.
507 def __getattr__(self, tagName):
508 if tagName == 'transparent':
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,))
522 def renderElement(request, element,
523 doctype='<!DOCTYPE html>', _failElement=None):
525 Render an element or other C{IRenderable}.
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>'}.
534 @returns: NOT_DONE_YET
538 if doctype is not None:
539 request.write(doctype)
542 if _failElement is None:
543 _failElement = twisted.web.util.FailureElement
545 d = flatten(request, element, request.write)
548 log.err(failure, "An error occurred while rendering the response.")
549 if request.site.displayTracebacks:
550 return flatten(request, _failElement(failure), request.write)
553 ('<div style="font-size:800%;'
554 'background-color:#FFF;'
556 '">An error occurred while rendering the response.</div>'))
559 d.addBoth(lambda _: request.finish())
564 from twisted.web._element import Element, renderer
565 from twisted.web._flatten import flatten, flattenString
566 import twisted.web.util