|
"""XML/XHTML Templates for 'peak.web', similar to Twisted's Woven |
|
|
|
TODO |
|
|
|
- implement interaction wrapper for "/skin", "/request", etc. model paths |
|
|
|
- implement sub-template support (convert template->view in another template) |
|
|
|
- add hooks for views to validate the list of supplied patterns |
|
|
|
- 'list' view needs iteration variables, maybe paging |
|
|
|
- need translation views, among lots of other kinds of views |
|
|
|
- support DTD fragments, and the rest of the XML standard |
|
""" |
|
|
from __future__ import generators |
from __future__ import generators |
from peak.api import * |
from peak.api import * |
from interfaces import * |
from interfaces import * |
from xml.sax.saxutils import quoteattr |
from xml.sax.saxutils import quoteattr, escape |
from publish import LocationPath |
from publish import LocationPath |
|
|
__all__ = [ |
__all__ = [ |
# really only the parser, bindings, etc. should be public |
'TEMPLATE_NS', 'VIEWS_PROPERTY', 'TemplateParser', 'TemplateDocument' |
] |
] |
|
|
|
TEMPLATE_NS = 'http://peak.telecommunity.com/peak.web.templates/' |
|
VIEWS_PROPERTY = PropertyName('peak.web.views') |
|
|
unicodeJoin = u''.join |
unicodeJoin = u''.join |
|
|
return ob is NOT_FOUND or ob is NOT_ALLOWED |
return ob is NOT_FOUND or ob is NOT_ALLOWED |
|
|
|
|
|
class TemplateAsMethod(binding.Component): |
|
|
|
"""Render a template component""" |
|
|
|
protocols.advise( |
|
instancesProvide = [IWebMethod], |
|
asAdapterForProtocols = [ITemplateNode], |
|
factoryMethod = 'fromNode' |
|
) |
|
|
|
templateNode = binding.requireBinding("""Node to render""") |
|
|
|
def fromNode(klass, subject, protocol): |
|
return klass(templateNode = subject) |
|
|
|
fromNode = classmethod(fromNode) |
|
|
|
def render(self, interaction): |
|
myLocation = self.getParentComponent() |
|
myOwner = myLocation.getParentComponent() |
|
data = [] |
|
self.templateNode.renderTo( |
|
interaction, data.append, myOwner, interaction |
|
) |
|
return unicodeJoin(data) |
|
|
|
|
|
|
|
|
|
|
|
|
|
class TemplateParser(binding.Component): |
|
|
|
"""Parser that assembles a TemplateDocument""" |
|
|
|
def parser(self,d,a): |
|
|
class TemplateAsMethod(binding.Component): |
from xml.parsers.expat import ParserCreate |
|
p = ParserCreate() |
|
|
"""Render a template component""" |
p.ordered_attributes = True |
|
p.returns_unicode = True |
|
p.specified_attributes = True |
|
|
protocols.advise( |
p.StartDoctypeDeclHandler = self.startDoctype |
instancesProvide = [IWebMethod], |
p.StartElementHandler = self.startElement |
asAdapterForProtocols = [ITemplateNode], |
p.EndElementHandler = self.endElement |
factoryMethod = 'fromNode' |
p.CharacterDataHandler = self.characters |
|
p.StartNamespaceDeclHandler = self.startNS |
|
p.EndNamespaceDeclHandler = self.endNS |
|
p.CommentHandler = self.comment |
|
|
|
# We don't use: |
|
# .XmlDeclHandler(version, encoding, standalone) |
|
# .ElementDeclHandler(name, model) |
|
# .AttlistDeclHandler(elname, attname, type, default, required) |
|
# .EndDoctypeDeclHandler() |
|
# .ProcessingInstructionHandler(target, data) |
|
# .UnparsedEntityDeclHandler(entityN,base,systemId,publicId,notationN) |
|
# .EntityDeclHandler( |
|
# entityName, is_parameter_entity, value, base, |
|
# systemId, publicId, notationName) |
|
# .NotationDeclHandler(notationName, base, systemId, publicId) |
|
# .StartCdataSectionHandler() |
|
# .EndCdataSectionHandler() |
|
# .NotStandaloneHandler() |
|
return p |
|
|
|
parser = binding.Once(parser) |
|
|
|
|
|
|
|
views = binding.New(list) |
|
stack = binding.New(list) |
|
nsUri = binding.New(dict) |
|
myNs = binding.New(dict) |
|
|
|
myNs = binding.Once( |
|
lambda self,d,a: dict( |
|
[(p,1) for (p,u) in self.nsUri.items() if u and u[-1]==TEMPLATE_NS] |
|
) |
) |
) |
|
|
templateNode = binding.requireBinding("""Node to render""") |
|
|
|
def fromNode(klass, subject, protocol): |
def parseFile(self, stream, document=None): |
return klass(templateNode = subject) |
if document is None: |
|
document = TemplateDocument(self.getParentComponent()) |
|
self.stack.append(document) |
|
self.views.append(document) |
|
self.parser.ParseFile(stream) |
|
|
fromNode = classmethod(fromNode) |
|
|
|
def render(self, interaction): |
def comment(self,data): |
data = [] |
self.buildLiteral(u'<!--%s-->' % data) |
self.templateNode.renderTo( |
|
interaction, data.append, self.getParentComponent(), interaction |
|
|
def startNS(self, prefix, uri): |
|
self.nsUri.setdefault(prefix,[]).append(uri) |
|
if uri==TEMPLATE_NS: |
|
self._delBinding('myNs') |
|
|
|
|
|
def endNS(self, prefix): |
|
uri = self.nsUri[prefix].pop() |
|
if uri==TEMPLATE_NS: |
|
self._delBinding('myNs') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def startElement(self, name, attrs): |
|
|
|
a = [] |
|
append = a.append |
|
myNs = self.myNs |
|
|
|
top = self.stack[-1] |
|
factory = top.tagFactory |
|
model = '' |
|
view = pattern = None |
|
|
|
for i in range(0,len(attrs),2): |
|
k,v = attrs[i], attrs[i+1] |
|
if ':' in k: |
|
ns, n = k.split(':',1) |
|
else: |
|
ns, n = '', k |
|
|
|
if myNs: |
|
if ns not in myNs: |
|
append((k,v)) |
|
continue |
|
elif ns: |
|
append((k,v)) |
|
continue |
|
|
|
if k=='view': |
|
view = v |
|
factory = VIEWS_PROPERTY.of(top)[v] |
|
factory = adapt(factory, ITemplateElementFactory) |
|
continue |
|
elif k=='model': |
|
model = v |
|
continue |
|
elif k=='pattern': |
|
pattern = v |
|
continue |
|
|
|
append((k,v)) |
|
continue |
|
|
|
tag = factory(top, tagName=name, attribItems=a, |
|
# XXX nonEmpty=False, |
|
viewProperty=view, modelPath=model, patternName=pattern |
) |
) |
return unicodeJoin(data) |
|
|
if pattern: |
|
self.views[-1].addPattern(pattern,tag) |
|
|
|
if view: |
|
# New view, put it on the view stack |
|
self.views.append(tag) |
|
else: |
|
# Duplicate the old view |
|
self.views.append(self.views[-1]) |
|
|
|
self.stack.append(tag) |
|
|
|
|
|
def endElement(self, name): |
|
self.views.pop() |
|
last = self.stack.pop() |
|
self.stack[-1].addChild(last) |
|
|
|
|
|
def buildLiteral(self,xml): |
|
top = self.stack[-1] |
|
literal = top.literalFactory(top, xml=xml) |
|
top.addChild(literal) |
|
|
|
|
|
def characters(self, data): |
|
top = self.stack[-1] |
|
text = top.textFactory(top, xml=escape(data)) |
|
top.addChild(text) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def startDoctype(self, doctypeName, systemId, publicId, has_internal): |
|
|
|
if publicId: |
|
p = ' PUBLIC %s %s' % (quoteattr(publicId),quoteattr(systemId)) |
|
elif systemId: |
|
p = ' SYSTEM %s' % quoteattr(systemId) |
|
else: |
|
p = '' |
|
|
|
# we ignore internal DTD subsets; they're not useful for HTML |
|
xml = u'<!DOCTYPE %s%s>\n' % (doctypeName, p) |
|
|
|
self.buildLiteral(xml) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
) |
) |
|
|
children = binding.New(list) |
children = binding.New(list) |
paterns = binding.New(list) |
|
patternMap = binding.New(dict) |
patternMap = binding.New(dict) |
|
|
tagName = binding.requireBinding("Tag name of element") |
tagName = binding.requireBinding("Tag name of element") |
attribItems = binding.requireBinding("Attribute name,value pairs") |
attribItems = binding.requireBinding("Attribute name,value pairs") |
nonEmpty = False |
nonEmpty = False |
viewProperty = None |
viewProperty = None |
modelPath = binding.Constant(None, adaptTo=LocationPath) |
modelPath = binding.Constant('', adaptTo=LocationPath) |
patternName = None |
patternName = None |
|
|
# ITemplateNode |
# ITemplateNode |
|
|
texts = [child.staticText for child in self.optimizedChildren] |
texts = [child.staticText for child in self.optimizedChildren] |
|
|
if None in texts or self.isDynamic: |
if None in texts: |
return None |
return None |
|
|
if texts or self.nonEmpty: |
if texts or self.nonEmpty: |
|
|
|
|
|
|
|
|
def optimizedChildren(self, d, a): |
def optimizedChildren(self, d, a): |
|
|
"""Child nodes with as many separate text nodes combined as possible""" |
"""Child nodes with as many separate text nodes combined as possible""" |
def addPattern(self, name, element): |
def addPattern(self, name, element): |
"""Declare 'element' as part of pattern 'name'""" |
"""Declare 'element' as part of pattern 'name'""" |
|
|
self.patterns.append( (name,element) ) |
# self.patterns.append( (name,element) ) |
self.patternMap.setdefault(name,[]).append(element) |
self.patternMap.setdefault(name,[]).append(element) |
|
|
|
|
) |
) |
) |
) |
|
|
tagFactory = classAttr(binding.bindTo('TemplateElement')) |
tagFactory = None # real value is set below |
textFactory = TemplateLiteral |
textFactory = TemplateLiteral |
literalFactory = TemplateLiteral |
literalFactory = TemplateLiteral |
|
|
|
TemplateElement.tagFactory = TemplateElement |
|
|
|
|
|
class TemplateDocument(TemplateElement): |
|
|
|
"""Document-level template element""" |
|
|
|
_openTag = _closeTag = _emptyTag = '' |
|
|
|
parserClass = TemplateParser |
|
|
|
def parseFile(self, stream): |
|
parser = self.parserClass(self) |
|
parser.parseFile(stream,self) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|