[Grok-dev] some ideas concerning skins and theming

Martijn Faassen faassen at startifact.com
Wed Mar 21 18:18:30 EDT 2007


Hi there,

Just now Lennart Regebro and I had a discussion on what Grok's approach 
to theming might look like. Here I jot down some of the ideas we had. 
The aims is to allow theming while being template language independent. 
We do want theming to be in-process, as we don't want to require 
something like deliverance to be used

A skin is like a Zope 3 skin, identified by an interface that can 
inherit other skins. A skin in Grok can however have a post-processing 
step associated with it. This means that any views that declares it is 
in this skin will be post-processed by that skin. Let's look at what 
that looks like:

class MyPage(grok.View):
    grok.skin('some_skin')

using the 'grok.skin' directive we say that this page is in our skin. 
How we define skins and such we haven't really fleshed out in detail, 
but one approach is sketched out in a Grok design document:

http://svn.zope.org/grok/trunk/doc/design/skin-minimal.py

(this is certainly not set in stone; I think we need to deal with skin 
inheritance and skins-as-interfaces, for instance. Possibly we just make 
a special ISkin interface and then just grok anything that subclasses it 
as a skin?)

The post-process step can then mess about with the HTML before it 
finally goes back to the user. This will allow us to do theming.

What will this look like? We could have a postprocess grokker to 
identify the postprocessing step (or the skin?) so we can write this:

class MySiteTheme(grok.Postprocess):
     # possibly also allow pointing to individual views for this?
     # do we have a parallel with grok.context going on here, but
     # then for skins or views?
     grok.skin('some_skin')

     def postprocess(self, html):
         return html.replace('h1', 'h2')

Now the replace will be done for any HTML generated by any view that's 
in the skin. This means that any text 'h1' will change into 'h2'.

Obviously this is a very coarse implementation. Better would be if the 
template generated a stream of events (along the line of sax) and for 
the postprocessor to transform the streams instead.

Genshi happens to have an implementation of this with interesting 
features, called Markup Streams. This is not the templating language 
itself, but a library underlying it:

  http://genshi.edgewall.org/wiki/Documentation/streams.html

The interesting feature is that they can do xpath on these streams. If 
we were to generate such a Markup Stream for our templates, we could 
post-process stuff. We may be able to reuse this bit of Genshi for our 
purposes, as Genshi is a pure-python library shipped as an egg. Note 
that I don't mean we use Genshi the template language here (that's 
another topic), just bits of Genshi's implementation.

Still, postprocessing a markup stream by hand sucks. We want to make 
this easier. What if we could use xpath to match bits of the output, and 
then replace these with something else? That would allow us to do 
something similar to what you do with macros and slots in Zope, but 
without actually having to define any slots. You basicaly just have 
fill-slot. :) What would that look like in grok terms?

What if we had views that didn't apply to a model, but instead applied 
to bits of HTML? The context is *not* a model, but instead a HTML tree.

Let's imagine that that could look like:

class TitleChanger(grok.MatchView):
    grok.skin('some_skin')
    grok.match('head/title') # xpath expression

    def render(self):
       return 'ME GROK ' + self.select('text()').upper()

Now for all views in the skin 'some_skin', we'd change the title of the 
page (<head><title>hello</title></head>, for instance) from 'hello' (or 
whatever it may be) to 'ME GROK HELLO'.

Of course instead of render, we could use Grok's existing view 
mechanisms to also use a template to render the content instead. Note 
the existence of a special 'select' method on the grok.MatchView to 
select some bits (in the current context).

I imagine a matcher could also be used to insert viewlets and so on. 
Using a template would be something like this:

class BoxInserter(grok.MatchView):
    grok.skin('some_skin')
    grok.match('some_section')

and then in boxinserter.pt

<div name="mybox">
This is a box with arbitrary stuff, like the weather, or a stock ticker, 
or whatever.
</div>
<tal:block replace="python:view.select('*|text()')" />

(or should we allow context.select() here?)

This would insert the box and then put in the original content (this is 
what select('*|text()') should do. This is modeled after Genshi match 
templates, but associated with skins instead of included in a document. 
This is a document describing Genshi match templates:

http://genshi.edgewall.org/wiki/Documentation/xml-templates.html#id5

When you write templates while building your application, you make can 
life easier for those trying to theme your application by using id= or 
name= on tags in your HTML. Those are easier for a theme writer to match.

The nice thing about this design however is that you don't really have 
to: we actually put no requirements on the template writer at all, and 
it could still be themed (with a bit more of a hassle). This means that 
we put very little burden on the application developer - a positive 
compared to the requirement to define the same slots everywhere.

The standard skin postprocessing logic would apply all the MatchViews 
associated to that skin. We need to think a bit about application order 
here - what if multiple expressions match? What if we have a skin that 
inherits from another one? Is there a way to disable this for a skin and 
use a different set of matchers altogether?

If this approach works, there are also some performance aspects we need 
to think about (how many parsing/serialization steps are necessary in 
order to make this work? is self.select() efficient enough on a view, or 
can we

Note that this mechanism is inspired by Genshi's match templates:

http://genshi.edgewall.org/wiki/Documentation/xml-templates.html#id5

I think with a bit of luck we can reuse much of Genshi's implementation 
to come up with a prototype for all this.

Regards,

Martijn








More information about the Grok-dev mailing list