|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575 |
- # -*- test-case-name: twisted.web.test.test_template -*-
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
-
- """
- HTML rendering for twisted.web.
-
- @var VALID_HTML_TAG_NAMES: A list of recognized HTML tag names, used by the
- L{tag} object.
-
- @var TEMPLATE_NAMESPACE: The XML namespace used to identify attributes and
- elements used by the templating system, which should be removed from the
- final output document.
-
- @var tags: A convenience object which can produce L{Tag} objects on demand via
- attribute access. For example: C{tags.div} is equivalent to C{Tag("div")}.
- Tags not specified in L{VALID_HTML_TAG_NAMES} will result in an
- L{AttributeError}.
- """
-
- from __future__ import division, absolute_import
-
- __all__ = [
- 'TEMPLATE_NAMESPACE', 'VALID_HTML_TAG_NAMES', 'Element', 'TagLoader',
- 'XMLString', 'XMLFile', 'renderer', 'flatten', 'flattenString', 'tags',
- 'Comment', 'CDATA', 'Tag', 'slot', 'CharRef', 'renderElement'
- ]
-
- import warnings
-
- from collections import OrderedDict
-
- from zope.interface import implementer
-
- from xml.sax import make_parser, handler
-
- from twisted.python.compat import NativeStringIO, items
- from twisted.python.filepath import FilePath
- from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef
- from twisted.web.iweb import ITemplateLoader
- from twisted.logger import Logger
-
- TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1'
-
- # Go read the definition of NOT_DONE_YET. For lulz. This is totally
- # equivalent. And this turns out to be necessary, because trying to import
- # NOT_DONE_YET in this module causes a circular import which we cannot escape
- # from. From which we cannot escape. Etc. glyph is okay with this solution for
- # now, and so am I, as long as this comment stays to explain to future
- # maintainers what it means. ~ C.
- #
- # See http://twistedmatrix.com/trac/ticket/5557 for progress on fixing this.
- NOT_DONE_YET = 1
- _moduleLog = Logger()
-
-
- class _NSContext(object):
- """
- A mapping from XML namespaces onto their prefixes in the document.
- """
-
- def __init__(self, parent=None):
- """
- Pull out the parent's namespaces, if there's no parent then default to
- XML.
- """
- self.parent = parent
- if parent is not None:
- self.nss = OrderedDict(parent.nss)
- else:
- self.nss = {'http://www.w3.org/XML/1998/namespace':'xml'}
-
-
- def get(self, k, d=None):
- """
- Get a prefix for a namespace.
-
- @param d: The default prefix value.
- """
- return self.nss.get(k, d)
-
-
- def __setitem__(self, k, v):
- """
- Proxy through to setting the prefix for the namespace.
- """
- self.nss.__setitem__(k, v)
-
-
- def __getitem__(self, k):
- """
- Proxy through to getting the prefix for the namespace.
- """
- return self.nss.__getitem__(k)
-
-
-
- class _ToStan(handler.ContentHandler, handler.EntityResolver):
- """
- A SAX parser which converts an XML document to the Twisted STAN
- Document Object Model.
- """
-
- def __init__(self, sourceFilename):
- """
- @param sourceFilename: the filename to load the XML out of.
- """
- self.sourceFilename = sourceFilename
- self.prefixMap = _NSContext()
- self.inCDATA = False
-
-
- def setDocumentLocator(self, locator):
- """
- Set the document locator, which knows about line and character numbers.
- """
- self.locator = locator
-
-
- def startDocument(self):
- """
- Initialise the document.
- """
- self.document = []
- self.current = self.document
- self.stack = []
- self.xmlnsAttrs = []
-
-
- def endDocument(self):
- """
- Document ended.
- """
-
-
- def processingInstruction(self, target, data):
- """
- Processing instructions are ignored.
- """
-
-
- def startPrefixMapping(self, prefix, uri):
- """
- Set up the prefix mapping, which maps fully qualified namespace URIs
- onto namespace prefixes.
-
- This gets called before startElementNS whenever an C{xmlns} attribute
- is seen.
- """
-
- self.prefixMap = _NSContext(self.prefixMap)
- self.prefixMap[uri] = prefix
-
- # Ignore the template namespace; we'll replace those during parsing.
- if uri == TEMPLATE_NAMESPACE:
- return
-
- # Add to a list that will be applied once we have the element.
- if prefix is None:
- self.xmlnsAttrs.append(('xmlns',uri))
- else:
- self.xmlnsAttrs.append(('xmlns:%s'%prefix,uri))
-
-
- def endPrefixMapping(self, prefix):
- """
- "Pops the stack" on the prefix mapping.
-
- Gets called after endElementNS.
- """
- self.prefixMap = self.prefixMap.parent
-
-
- def startElementNS(self, namespaceAndName, qname, attrs):
- """
- Gets called when we encounter a new xmlns attribute.
-
- @param namespaceAndName: a (namespace, name) tuple, where name
- determines which type of action to take, if the namespace matches
- L{TEMPLATE_NAMESPACE}.
- @param qname: ignored.
- @param attrs: attributes on the element being started.
- """
-
- filename = self.sourceFilename
- lineNumber = self.locator.getLineNumber()
- columnNumber = self.locator.getColumnNumber()
-
- ns, name = namespaceAndName
- if ns == TEMPLATE_NAMESPACE:
- if name == 'transparent':
- name = ''
- elif name == 'slot':
- try:
- # Try to get the default value for the slot
- default = attrs[(None, 'default')]
- except KeyError:
- # If there wasn't one, then use None to indicate no
- # default.
- default = None
- el = slot(
- attrs[(None, 'name')], default=default,
- filename=filename, lineNumber=lineNumber,
- columnNumber=columnNumber)
- self.stack.append(el)
- self.current.append(el)
- self.current = el.children
- return
-
- render = None
-
- attrs = OrderedDict(attrs)
- for k, v in items(attrs):
- attrNS, justTheName = k
- if attrNS != TEMPLATE_NAMESPACE:
- continue
- if justTheName == 'render':
- render = v
- del attrs[k]
-
- # nonTemplateAttrs is a dictionary mapping attributes that are *not* in
- # TEMPLATE_NAMESPACE to their values. Those in TEMPLATE_NAMESPACE were
- # just removed from 'attrs' in the loop immediately above. The key in
- # nonTemplateAttrs is either simply the attribute name (if it was not
- # specified as having a namespace in the template) or prefix:name,
- # preserving the xml namespace prefix given in the document.
-
- nonTemplateAttrs = OrderedDict()
- for (attrNs, attrName), v in items(attrs):
- nsPrefix = self.prefixMap.get(attrNs)
- if nsPrefix is None:
- attrKey = attrName
- else:
- attrKey = '%s:%s' % (nsPrefix, attrName)
- nonTemplateAttrs[attrKey] = v
-
- if ns == TEMPLATE_NAMESPACE and name == 'attr':
- if not self.stack:
- # TODO: define a better exception for this?
- raise AssertionError(
- '<{%s}attr> as top-level element' % (TEMPLATE_NAMESPACE,))
- if 'name' not in nonTemplateAttrs:
- # TODO: same here
- raise AssertionError(
- '<{%s}attr> requires a name attribute' % (TEMPLATE_NAMESPACE,))
- el = Tag('', render=render, filename=filename,
- lineNumber=lineNumber, columnNumber=columnNumber)
- self.stack[-1].attributes[nonTemplateAttrs['name']] = el
- self.stack.append(el)
- self.current = el.children
- return
-
- # Apply any xmlns attributes
- if self.xmlnsAttrs:
- nonTemplateAttrs.update(OrderedDict(self.xmlnsAttrs))
- self.xmlnsAttrs = []
-
- # Add the prefix that was used in the parsed template for non-template
- # namespaces (which will not be consumed anyway).
- if ns != TEMPLATE_NAMESPACE and ns is not None:
- prefix = self.prefixMap[ns]
- if prefix is not None:
- name = '%s:%s' % (self.prefixMap[ns],name)
- el = Tag(
- name, attributes=OrderedDict(nonTemplateAttrs), render=render,
- filename=filename, lineNumber=lineNumber,
- columnNumber=columnNumber)
- self.stack.append(el)
- self.current.append(el)
- self.current = el.children
-
-
- def characters(self, ch):
- """
- Called when we receive some characters. CDATA characters get passed
- through as is.
-
- @type ch: C{string}
- """
- if self.inCDATA:
- self.stack[-1].append(ch)
- return
- self.current.append(ch)
-
-
- def endElementNS(self, name, qname):
- """
- A namespace tag is closed. Pop the stack, if there's anything left in
- it, otherwise return to the document's namespace.
- """
- self.stack.pop()
- if self.stack:
- self.current = self.stack[-1].children
- else:
- self.current = self.document
-
-
- def startDTD(self, name, publicId, systemId):
- """
- DTDs are ignored.
- """
-
-
- def endDTD(self, *args):
- """
- DTDs are ignored.
- """
-
-
- def startCDATA(self):
- """
- We're starting to be in a CDATA element, make a note of this.
- """
- self.inCDATA = True
- self.stack.append([])
-
-
- def endCDATA(self):
- """
- We're no longer in a CDATA element. Collect up the characters we've
- parsed and put them in a new CDATA object.
- """
- self.inCDATA = False
- comment = ''.join(self.stack.pop())
- self.current.append(CDATA(comment))
-
-
- def comment(self, content):
- """
- Add an XML comment which we've encountered.
- """
- self.current.append(Comment(content))
-
-
-
- def _flatsaxParse(fl):
- """
- Perform a SAX parse of an XML document with the _ToStan class.
-
- @param fl: The XML document to be parsed.
- @type fl: A file object or filename.
-
- @return: a C{list} of Stan objects.
- """
- parser = make_parser()
- parser.setFeature(handler.feature_validation, 0)
- parser.setFeature(handler.feature_namespaces, 1)
- parser.setFeature(handler.feature_external_ges, 0)
- parser.setFeature(handler.feature_external_pes, 0)
-
- s = _ToStan(getattr(fl, "name", None))
- parser.setContentHandler(s)
- parser.setEntityResolver(s)
- parser.setProperty(handler.property_lexical_handler, s)
-
- parser.parse(fl)
-
- return s.document
-
-
- @implementer(ITemplateLoader)
- class TagLoader(object):
- """
- An L{ITemplateLoader} that loads existing L{IRenderable} providers.
-
- @ivar tag: The object which will be loaded.
- @type tag: An L{IRenderable} provider.
- """
-
- def __init__(self, tag):
- """
- @param tag: The object which will be loaded.
- @type tag: An L{IRenderable} provider.
- """
- self.tag = tag
-
-
- def load(self):
- return [self.tag]
-
-
-
- @implementer(ITemplateLoader)
- class XMLString(object):
- """
- An L{ITemplateLoader} that loads and parses XML from a string.
-
- @ivar _loadedTemplate: The loaded document.
- @type _loadedTemplate: a C{list} of Stan objects.
- """
-
- def __init__(self, s):
- """
- Run the parser on a L{NativeStringIO} copy of the string.
-
- @param s: The string from which to load the XML.
- @type s: C{str}, or a UTF-8 encoded L{bytes}.
- """
- if not isinstance(s, str):
- s = s.decode('utf8')
-
- self._loadedTemplate = _flatsaxParse(NativeStringIO(s))
-
-
- def load(self):
- """
- Return the document.
-
- @return: the loaded document.
- @rtype: a C{list} of Stan objects.
- """
- return self._loadedTemplate
-
-
-
- @implementer(ITemplateLoader)
- class XMLFile(object):
- """
- An L{ITemplateLoader} that loads and parses XML from a file.
-
- @ivar _loadedTemplate: The loaded document, or L{None}, if not loaded.
- @type _loadedTemplate: a C{list} of Stan objects, or L{None}.
-
- @ivar _path: The L{FilePath}, file object, or filename that is being
- loaded from.
- """
-
- def __init__(self, path):
- """
- Run the parser on a file.
-
- @param path: The file from which to load the XML.
- @type path: L{FilePath}
- """
- if not isinstance(path, FilePath):
- warnings.warn(
- "Passing filenames or file objects to XMLFile is deprecated "
- "since Twisted 12.1. Pass a FilePath instead.",
- category=DeprecationWarning, stacklevel=2)
- self._loadedTemplate = None
- self._path = path
-
-
- def _loadDoc(self):
- """
- Read and parse the XML.
-
- @return: the loaded document.
- @rtype: a C{list} of Stan objects.
- """
- if not isinstance(self._path, FilePath):
- return _flatsaxParse(self._path)
- else:
- with self._path.open('r') as f:
- return _flatsaxParse(f)
-
-
- def __repr__(self):
- return '<XMLFile of %r>' % (self._path,)
-
-
- def load(self):
- """
- Return the document, first loading it if necessary.
-
- @return: the loaded document.
- @rtype: a C{list} of Stan objects.
- """
- if self._loadedTemplate is None:
- self._loadedTemplate = self._loadDoc()
- return self._loadedTemplate
-
-
-
- # Last updated October 2011, using W3Schools as a reference. Link:
- # http://www.w3schools.com/html5/html5_reference.asp
- # Note that <xmp> is explicitly omitted; its semantics do not work with
- # t.w.template and it is officially deprecated.
- VALID_HTML_TAG_NAMES = set([
- 'a', 'abbr', 'acronym', 'address', 'applet', 'area', 'article', 'aside',
- 'audio', 'b', 'base', 'basefont', 'bdi', 'bdo', 'big', 'blockquote',
- 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code',
- 'col', 'colgroup', 'command', 'datalist', 'dd', 'del', 'details', 'dfn',
- 'dir', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption',
- 'figure', 'font', 'footer', 'form', 'frame', 'frameset', 'h1', 'h2', 'h3',
- 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe',
- 'img', 'input', 'ins', 'isindex', 'keygen', 'kbd', 'label', 'legend',
- 'li', 'link', 'map', 'mark', 'menu', 'meta', 'meter', 'nav', 'noframes',
- 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param',
- 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script',
- 'section', 'select', 'small', 'source', 'span', 'strike', 'strong',
- 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'textarea',
- 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'tt', 'u', 'ul', 'var',
- 'video', 'wbr',
- ])
-
-
-
- class _TagFactory(object):
- """
- A factory for L{Tag} objects; the implementation of the L{tags} object.
-
- This allows for the syntactic convenience of C{from twisted.web.html import
- tags; tags.a(href="linked-page.html")}, where 'a' can be basically any HTML
- tag.
-
- The class is not exposed publicly because you only ever need one of these,
- and we already made it for you.
-
- @see: L{tags}
- """
- def __getattr__(self, tagName):
- if tagName == 'transparent':
- return Tag('')
- # allow for E.del as E.del_
- tagName = tagName.rstrip('_')
- if tagName not in VALID_HTML_TAG_NAMES:
- raise AttributeError('unknown tag %r' % (tagName,))
- return Tag(tagName)
-
-
-
- tags = _TagFactory()
-
-
-
- def renderElement(request, element,
- doctype=b'<!DOCTYPE html>', _failElement=None):
- """
- Render an element or other C{IRenderable}.
-
- @param request: The C{Request} being rendered to.
- @param element: An C{IRenderable} which will be rendered.
- @param doctype: A C{bytes} which will be written as the first line of
- the request, or L{None} to disable writing of a doctype. The C{string}
- should not include a trailing newline and will default to the HTML5
- doctype C{'<!DOCTYPE html>'}.
-
- @returns: NOT_DONE_YET
-
- @since: 12.1
- """
- if doctype is not None:
- request.write(doctype)
- request.write(b'\n')
-
- if _failElement is None:
- _failElement = twisted.web.util.FailureElement
-
- d = flatten(request, element, request.write)
-
- def eb(failure):
- _moduleLog.failure(
- "An error occurred while rendering the response.",
- failure=failure
- )
- if request.site.displayTracebacks:
- return flatten(request, _failElement(failure),
- request.write).encode('utf8')
- else:
- request.write(
- (b'<div style="font-size:800%;'
- b'background-color:#FFF;'
- b'color:#F00'
- b'">An error occurred while rendering the response.</div>'))
-
- d.addErrback(eb)
- d.addBoth(lambda _: request.finish())
- return NOT_DONE_YET
-
-
-
- from twisted.web._element import Element, renderer
- from twisted.web._flatten import flatten, flattenString
- import twisted.web.util
|