Initial import to Tizen
[profile/ivi/python-twisted.git] / twisted / web / template.py
1 # -*- test-case-name: twisted.web.test.test_template -*-
2 # Copyright (c) Twisted Matrix Laboratories.
3 # See LICENSE for details.
4
5
6 """
7 HTML 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
28 import warnings
29 from zope.interface import implements
30
31 from cStringIO import StringIO
32 from xml.sax import make_parser, handler
33
34 from twisted.web._stan import Tag, slot, Comment, CDATA, CharRef
35 from twisted.python.filepath import FilePath
36
37 TEMPLATE_NAMESPACE = 'http://twistedmatrix.com/ns/twisted.web.template/0.1'
38
39 from twisted.web.iweb import ITemplateLoader
40 from 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.
50 NOT_DONE_YET = 1
51
52 class _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
93 class _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
331 def _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
356 class 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
378 class 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
408 class 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.
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',
489     'video', 'wbr',
490 ])
491
492
493
494 class _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
518 tags = _TagFactory()
519
520
521
522 def 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
564 from twisted.web._element import Element, renderer
565 from twisted.web._flatten import flatten, flattenString
566 import twisted.web.util