[Subversion] / PEAK / src / peak / web / templates.py  

View of /PEAK/src/peak/web/templates.py

Parent Directory | Revision Log
Revision: 1275 - (download) (as text)
Mon Jul 21 00:55:16 2003 UTC (20 years, 9 months ago) by pje
File size: 13500 byte(s)
The templates have landed!  We can parse and render templates, and use them
as web methods on locations.  There's even a (yes, just one) unit test.
Big TODO lists both in the package and its test suite, but at least it's
working.  Made minor adjustments to publishing algorithms so that locations
know their parent locations, and methods know the location that they were
found in. Bumped package version to 0.5a3, since we've been in the alpha 3
cycle for a while now.
"""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 peak.api import *
from interfaces import *
from xml.sax.saxutils import quoteattr, escape
from publish import LocationPath

__all__ = [
    'TEMPLATE_NS', 'VIEWS_PROPERTY', 'TemplateParser', 'TemplateDocument'
]

TEMPLATE_NS = 'http://peak.telecommunity.com/peak.web.templates/'
VIEWS_PROPERTY = PropertyName('peak.web.views')

unicodeJoin = u''.join

def infiniter(sequence):
    while 1:
        for item in sequence:
            yield item

def isNull(ob):
    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):

        from xml.parsers.expat import ParserCreate
        p = ParserCreate()

        p.ordered_attributes = True
        p.returns_unicode = True
        p.specified_attributes = True

        p.StartDoctypeDeclHandler = self.startDoctype
        p.StartElementHandler = self.startElement
        p.EndElementHandler = self.endElement
        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]
        )
    )


    def parseFile(self, stream, document=None):
        if document is None:
            document = TemplateDocument(self.getParentComponent())
        self.stack.append(document)
        self.views.append(document)
        self.parser.ParseFile(stream)


    def comment(self,data):
        self.buildLiteral(u'<!--%s-->' % data)


    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
        )

        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)




























class TemplateLiteral(binding.Component):

    """Simple static text node"""

    protocols.advise(
        classProvides = [ITemplateNodeFactory],
        instancesProvide = [ITemplateNode],
    )

    xml = u''

    staticText = binding.bindTo('xml')

    def renderTo(self, interaction, writeFunc, currentModel, executionContext):
        writeFunc(self.xml)


























class TemplateElement(binding.Component):

    protocols.advise(
        classProvides = [ITemplateElementFactory],
        instancesProvide = [ITemplateElement],
    )

    children   = binding.New(list)
    patternMap = binding.New(dict)

    tagName      = binding.requireBinding("Tag name of element")
    attribItems  = binding.requireBinding("Attribute name,value pairs")
    nonEmpty     = False
    viewProperty = None
    modelPath    = binding.Constant('', adaptTo=LocationPath)
    patternName  = None

    # ITemplateNode

    def staticText(self, d, a):

        """Note: replace w/staticText = None in dynamic element subclasses"""

        texts = [child.staticText for child in self.optimizedChildren]

        if None in texts:
            return None

        if texts or self.nonEmpty:
            texts.insert(0, self._openTag)
            texts.append(self._closeTag)
            return unicodeJoin(texts)
        else:
            return self._emptyTag

    staticText = binding.Once(staticText, suggestParent=False)





    def optimizedChildren(self, d, a):

        """Child nodes with as many separate text nodes combined as possible"""

        all = []
        texts = []

        def flush():
            if texts:
                all.append(
                    self.literalFactory(self, xml=unicodeJoin(texts))
                )
                texts[:]=[]

        for child in self.children:
            t = child.staticText
            if t is None:
                flush()
                all.append(child)
            else:
                texts.append(t)

        flush()
        return all

    optimizedChildren = binding.Once(optimizedChildren)


    def _getSubModel(self, interaction, currentModel):

        if isNull(currentModel):
            return currentModel

        return self.modelPath.traverse(
            currentModel, interaction, lambda o,i: self._wrapInteraction(i)
        )





    def renderTo(self, interaction, writeFunc, currentModel, executionContext):

        text = self.staticText
        if text is not None:
            writeFunc(text)
            return

        if self.modelPath:
            currentModel = self._getSubModel(interaction, currentModel)

        if not self.optimizedChildren and not self.nonEmpty:
            self._renderEmpty(
                interaction, writeFunc, currentModel, executionContext
            )
            return

        self._open(interaction, writeFunc, currentModel, executionContext)
        for child in self.optimizedChildren:
            child.renderTo(
                interaction, writeFunc, currentModel, executionContext
            )
        writeFunc(self._closeTag)


    def addChild(self, node):
        """Add 'node' (an 'ITemplateNode') to element's direct children"""

        if self._hasBinding('optimizedChildren'):
            raise TypeError(
                "Attempt to add child after rendering", self, node
            )
        self.children.append(node)


    def addPattern(self, name, element):
        """Declare 'element' as part of pattern 'name'"""

        # self.patterns.append( (name,element) )
        self.patternMap.setdefault(name,[]).append(element)


    # Override in subclasses

    def _open(self, interaction, writeFunc, currentModel, executionContext):
        writeFunc(self._openTag)

    def _renderEmpty(self,
        interaction, writeFunc, currentModel, executionContext
    ):
        writeFunc(self._emptyTag)


    def _wrapInteraction(self,interaction):
        # XXX This should wrap the interaction in an IWebLocation simulator,
        # XXX which should include access to this element's patterns as well
        # XXX as interaction variables.
        raise NotImplementedError


    _emptyTag = binding.Once(
        lambda self,d,a: self._openTag[:-1]+u' />'
    )

    _closeTag = binding.Once(
        lambda self,d,a: u'</%s>' % self.tagName
    )

    _openTag = binding.Once(
        lambda self,d,a: u'<%s%s>' % ( self.tagName,
            unicodeJoin([
                u' %s=%s' % (k,quoteattr(v)) for (k,v) in self.attribItems
            ])
        )
    )

    tagFactory     = None # real value is set below
    textFactory    = 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)






























class TemplateReplacement(TemplateElement):

    """Abstract base for elements that replace their contents"""

    staticText = None
    children   = optimizedChildren = binding.bindTo('contents')
    contents   = binding.requireBinding("nodes to render in element body")

    def addChild(self, node):
        pass    # ignore children, only patterns count with us


class TemplateText(TemplateReplacement):

    """Replace element contents w/model"""

    def renderTo(self, interaction, writeFunc, currentModel, executionContext):

        if self.modelPath:
            currentModel = self._getSubModel(interaction, currentModel)

        writeFunc(self._openTag)

        if not isNull(currentModel):
            writeFunc(unicode(currentModel.getObject()))

        writeFunc(self._closeTag)














class TemplateList(TemplateReplacement):

    def renderTo(self, interaction, writeFunc, currentModel, executionContext):

        if self.modelPath:
            currentModel = self._getSubModel(interaction, currentModel)

        writeFunc(self._openTag)

        i = infiniter(self.patternMap['listItem'])
        locationProtocol = interaction.locationProtocol
        ct = 0

        if not isNull(currentModel):

            for item in currentModel.getObject():
                if not interaction.allows(item):
                    continue

                loc = adapt(item, locationProtocol, None)
                if loc is None:
                    continue

                # XXX this should probably use an iteration location, or maybe
                # XXX put some properties in execution context for loop vars?

                i.next().renderTo(
                    interaction, writeFunc, loc, executionContext
                )
                ct += 1

        if not ct:
            # Handle list being empty
            for child in self.patternMap.get('emptyList',()):
                child.renderTo(
                    interaction, writeFunc, currentModel, executionContext
                )

        writeFunc(self._closeTag)


cvs-admin@eby-sarna.com

Powered by ViewCVS 1.0-dev

ViewCVS and CVS Help