[PEAK] PWT: layout, macros, parameters, etc.
Phillip J. Eby
pje at telecommunity.com
Tue Oct 19 15:15:29 EDT 2004
I've now finished most everything I planned for peak.web this month, except
for the sitemap parser (the raw sitemap functionality is implemented, just
not the parser to activate the functionality from XML) and the advanced
facilities of the next-generation PWT language.
So, this post is to help me flesh out those "next generation" features,
which up to this point have only been sketched.
The main thing I want to touch on is the "layout" or "macro" facility, that
should allow us to "call" another template or DOMlet with blocks and data
designated as named parameters.
After giving it some thought, I don't think we need a separate ':layout'
attribute to invoke macros or layouts. Instead, the standard ':replace'
can be used. Normally, 'replace' expects to convert the target data to
unicode, but we can first adapt to IDOMletNode. If the target to be
rendered is a DOMlet node, we simply invoke it in-place (after setting up
any parameters).
This would mean that defining a view on an object to map to a template or
DOMlet, would automatically cause it to be rendered inside a referencing
document, and changing a view from a Python string expression to a template
or back would not affect templates using the view.
So how do the parameters get passed? I'm envisioning the creation of a
context object, pointing to a mapping that contains the parameters. This
context would be passed to the invoked DOMlet as the current context, so
any path usage in the invoked DOMlet would see the parameters.
That concept works nicely for a DOMlet being used as a macro, but not so
well for a view on an object. A view on an object really wants to have
only one parameter: the object to be viewed. And, it should bind tightly
to the object being viewed; it shouldn't be possible to refer to the view
and then somehow disconnect it from its target.
So, when we register "resource" and "object" views, we need to include some
glue code that asks the view object if it would like to "bind" to the
object being viewed, by returning a "version of itself" that's linked to
the target. In the case of a DOMlet-based view, this would allow it to
ignore any extraneous parameters. (We don't need this code for "attribute"
or "function" views, since presumably these can do their own binding.)
Of course, I suppose it's possible to have a situation where you need to
pass parameters into a view on an object, but I don't see a clean way to
support that, without forcing some sort of prefix convention like
'here/foo' on every view template (the way I think ZPT does). I suppose it
might not be so bad, if the prefix were '..', such that '../foo' referred
to 'foo' on the view's target object, and 'bar' referred to the 'bar'
parameter.
But that seems awfully backwards. Maybe we could add a 'params' attribute
to ITraversalContext instead. Then, the parameters could stay with the
traversal no matter what, and they can be accessed with
'/params/whatever'. I don't think this generalizes well, though. For
example, I wouldn't want to use '/params' as the way to access sequence
variables (like DTML's "sequence-index") inside a "list".
But it would work a lot better if the data were actually part of the
IDOMletState, because parameters are part of the execution state, not the
traversal context. After all, you can traverse to '..', which would take
you to a context where the parameters don't apply any more. That's just
too wonky.
So, here's what we'll do instead. The IDOMletState will hold a mapping for
parameters, variables, and whathaveyou, that will get cloned whenever you
create a new DOMletState. For example, if we're passing parameters to an
IDOMletNode view, we'll clone the current state and put the parameters in
under a 'params' key. Then, whenever we do path traversals that begin
with a '/' -- which currently traverse to the traversal context object --
we'll instead traverse to a wrapper that layers the DOMletState's mapping
on top of the traversal context. Thus, both context-specific and
state-specific items will be available to templates.
So /params/foo will get you to the foo parameter, /user gets you the
current user, and so on. If the 'list' domlet grows sequence variables
like DTML's sequence-index, you'll be able to use paths like
/sequence/index to get at them.
Excellent! There's only one other open issue for parameter passing, and
that's multiple parameters with the same name. Currently, DOMlets allow
you to specify more than one parameter with the same name, which is useful
for e.g. the 'list' DOMlet to rotate through alternative formats of list
items. Anyway, I don't think this approach can continue, because templates
using these things as parameters won't have any way to tell if a given
parameter is a single value, or a list. So, I think we need to go
"strongly typed" here, and either banish multi-valued parameters, or have a
different way to specify a single value versus a list value.
So, let's say we used 'this:is' (and 'content:is') to mean a single-valued
parameter, and 'this:is-a' to mean a multi-valued parameter? Then, even if
you only supply one 'is-a', the resulting parameter is multi-valued (i.e.,
a list).
I'm not sure this is the best thing either, though. So far, there's only
one DOMlet we use that takes even one multi-valued parameter. And, more
often than not, you're only giving it one value: the format for all list
items. So, it's going to be weird trying to remember when to use ':is'
versus ':is-a'.
Maybe the solution there is to use a different parameter name for rotating
list items, e.g. 'this:is-a="rotatingFormat"' when you need rotating
formats, and use 'this:is="anItem"' normally. Hm. This also shows that
parameter names can suggest by their spelling, what mode is used to invoke
them. So we could say 'this:is="theHeader"' and 'this:is="theFooter"' to
indicate list header and footer, although that seems a wee bit over the top.
Really, I'm having a hard time coming up with use cases for multi-valued
parameters besides ones that are passed to Python-based DOMlets, as there
are lots of other ways to do things in templates. Especially since
multi-valued parameters specified in a template are always going to be
fixed, functionlike values.
Maybe it would be better to just drop the whole idea, and make the
'list' DOMlet work harder to get rotating formats. E.g. make it check for
parameters named 'format-1', 'format-2', and so on. That would be pretty
damn ugly, but at least it could be processed at "compile time", so to
speak. Maybe I could just allow DOMlets to specify multi-valued argument
names at "compile time", and disallow all others. This would mean that
DOMlets written in Python (like 'list') could use multi-valued parameters,
but dynamic template-based DOMlets chosen at runtime would always receive
single-valued parameters. That seems like a reasonable compromise.
Okay, I think we've got the invocation and parameter model sorted
out. What's left? Consulting my to-do list, I see:
* dynamic attribute values
* path alternatives
* /nothing, /default, and /modules
* :if/:unless
* :try/:except
* :expects
So let's go through them. Dynamic attribute values. It would be nice to
be able to specify image width and height attributes. I had been thinking
about stealing ZPT syntax, which is something like 'tal:attributes="width
widthexpr; height heightexpr"'. But today I saw that Nevow has something like:
<img src="something"><nevow:attr name="width"><nevow:slot name="widthval"
/></nevow:attr><nevow:attr name="height"><nevow:slot name="heightval"
/></nevow:attr></img>
Which is actually pretty sucky, compared to the ZPT syntax. But, it leads
to this idea:
<img src="dummy"><span this:attr="width" this:replace="widthexpr"/><span
this:attr="height" this:replace="heightexpr"/></img>
...which is somewhat more interesting. As long as you don't include any
whitespace between the tags, the '<img>' tag as rendered would be an empty
tag. The interesting question, of course, is whether other tools would
barf on the non-empty '<img>' tag in the source.
Another possibility is just to have an attribute namespace, like this:
<img src="dummy" attr:width="widthexpr" attr:height="heightexpr">
This would work for maybe 90% of the use cases; in the rare scenario where
you need to also specify an XML namespace, you could maybe do:
<something ns_for:foo="bar" attr:foo="foo_expr">
Which at runtime would set a 'bar:foo' attribute to the value of "foo_expr".
Yeah, I think I like this. In order to make it work, we're going to have
to have an interface to let you tell DOMlets about dynamic
attributes. Which we could refactor the existing 'url' DOMlet to
use. Interestingly, once we had that interface, it could also be used for
the more Nevow-like approach of using nested DOMlets to tell an outer
DOMlet about attributes. But I don't see any immediate use for that.
Excellent. Let's move on to path alternatives, /nothing and /default. I
think we're often going to run into the case that a value we're looking for
doesn't exist, or even more often, the user isn't allowed to get to. ZPT
handles this by allowing alternative paths, e.g "foo|bar" to mean, "look
for foo, and if you can't get it, try bar next". Often "bar" is actually
"nothing" or "default". "nothing" means, "don't put anything here", and
"default" means, "put whatever is in the template here". These are both
common use cases.
We could allow path alternatives, and add 'nothing' and 'default' objects
to the context namespace, such that "foo|/nothing" and "bar|/default" would
do what we want. But this looks ugly, and if these are common cases, it
would be nice to have a more compact, yet more readable syntax.
There are three states we want to distinguish in the common cases:
* If this thing isn't accessible, blow up with an error
* If this thing isn't accessible, don't display anything
* If this thing isn't accessible, display something else instead
There are some interesting distinctions between these cases. For example,
I think that the "don't display anything" case will usually apply to a
bigger block than the one where the value is being looked for. Consider an
output form, where you are displaying various fields, perhaps using a
table. You'll want to omit the entire table row where an inaccessible
field occurs, not just the cell where you're outputting the value, e.g.:
<tr>
<td>Field X:</td>
<td content:replace="field_x">Value here</td>
</tr>
What you really want is to do something like:
<tr this:if="field_x">
<td>Field X:</td>
<td content:replace=".">Value here</td>
</tr>
That leaves the question of alternative/default output. For attributes, I
think we can use the ZPT style of alternative paths,
e.g. 'attr:foo="something|/nothing"', or 'attr:bar="foo|/default". For
attributes, this notation is reasonably compact, and I don't expect
attribute mangling to be common anyway. 'nothing' would mean, "don't
include the attribute at all", and 'default' would mean, "use the
attribute's existing value in the template".
For default output at the element level, I'm a bit more torn. For example,
consider the 'list' DOMlet. Usually, you're going to have some kind of
structure, let's say:
<ul content:list="foo">
<li this:is="listItem" content:replace="title">Item here</li>
</ul>
But, if "foo" is empty or unavailable, you probably don't want the '<ul>'
to even appear, e.g.:
<ul this:if="foo" content:list=".">
<li this:is="listItem" content:replace="title">Item here</li>
</ul>
And maybe you want a <p> to appear instead, saying that there are no items
available:
<p this:unless="foo">No foo items available.</p>
This is becoming altogether too much like programming. Also, the ':if'
doesn't look as though it should traverse to the thing it points at. It
almost seems that ':with' would make more sense:
<ul this:with="foo" content:list=".">
<li this:is="listItem" content:replace="title">Item here</li>
</ul>
<p this:unless="foo">No foo items available.</p>
So, the idea could be that 'with' traverses to the item, but if the item
does't exist, the whole block is skipped. In other words, "execute this
block 'with' this object, if it's available." I'm not sure whether
'unless' also needs renaming. It clearly can't change what the current
object is, since its content only executes if the target doesn't exist.
However, this brings up a different issue. Should 'with' and 'unless' pay
attention to the boolean value of their subjects, or just their
accessibility? What about the '|' operator? Should it pay attention? So
far, the above use case is the only one that suggests there's value in
paying attention to it, and maybe it could be handled differently:
<ul this:try="" content:list="foo">
<li this:is="listItem" content:replace="title">Item here</li>
<p this:is="emptyMessage" this:catch="NotFound,NotAllowed">No foo
items available.</p>
</ul>
The idea here is that the outer block is a 'try' block; it tries to execute
its contents, and if there's an error, it checks its parameters for a
matching 'catch' block, and then replaces itself with that
block. Meanwhile, in the example above, that block is also being used as
an 'emptyMessage' parameter to the 'list' DOMlet, so the same message is
used if the list is empty.
Hm. It works, but it's now horribly ugly when compared to its
predecessor. Of course, we *could* spell it like so:
<ul this:with="foo" content:list=".">
<li this:is="listItem" content:replace="title">Item here</li>
<p this:is="emptyMessage">No foo items available.</p>
</ul>
<p this:unless="foo">No foo items available.</p>
at the cost of some duplication. But I'm guessing that in the common case,
it's more likely that you'll be doing:
<ul this:with="foo" content:list=".">
<li this:is="listItem" content:replace="title">Item here</li>
<p this:is="emptyMessage">No foo items available.</p>
</ul>
and just not saying anything if the user isn't allowed to access
'foo'. No sense making a big deal out of it. Anyway, I think we'll want
to have 'with' and 'unless' ignore the boolean value, and deal only with
accessibility. There are plenty of other ways and places to deal with the
boolean nature, such as through views, and Python DOMlets.
Finally, I think we'll have 'default' map internally to NOT_GIVEN, and
'nothing' map to NOT_FOUND. That will probably make the most sense.
Okay, so what's left now?
* /modules
* :try/:catch
* :expects
The '/modules' thing is an idea stolen from ZPT, which has a 'modules'
mapping to access arbitrary Python modules. We'll put it in the
DOMletState variables mapping, so it'll only be accessible from within
hardcoded template paths. Technically, it won't give access to the
modules, but rather to their dictionaries. So,
'/modules/foo.bar/something' will look up 'something' in the 'foo.bar'
module's dictionary. This is because our security machinery considers
dictionaries' contents to be "fair game" for anybody to look at, unless the
object pulled out of the dictionary forbids access to itself. But, it
doesn't allow access to attributes unless they are explicitly
granted. Anyway, the purpose of this is to allow you to pass Python
objects as parameters to DOMlets or templates.
Of course, if the object is a function or class or something like that, you
won't be able to actually *do* anything with it from a template, unless it
can be adapted to an IDOMletNode, or it's something like a string, or if
you have some kind of view declared on objects of that type.
Okay, what about try/catch? Well, the main use case I had in mind for them
was to deal with inaccessibility of locations, and that's going to mostly
be handled by with/unless. And I worry that having them will increase the
temptation to use PWT as a "scripting" language, instead of merely as a
templating system. So, I think I'll call YAGNI on them for the moment.
':expects', on the other hand, is just an assertion as to the type or
protocol of the thing currently being handled. E.g.,
'this:expects="mypkg.IFoo"', to indicate that the current object should be
adapted to 'IFoo' before proceeding. More commonly, I imagine you'll do
'this:with="foo" content:expects="mypkg.IFoo"'. Since this is an
essentially static declaration, the string should be an import string
that's executed at compile time. It's hard to think of a meaningful use
case for this with a dynamic target protocol, so I'll call YAGNI on
anything but statically defined protocols. It's probably creeping
scriptism, that should be moved into a Python DOMlet specialized to the task.
Hm. Rechecking my offline to-do list, I've still got a few items left to
sort out:
* with:paramName="path"
* page:layout="layout_path"
* page:content-type="something/something"
The first one is a syntax for passing path parameters to a template or
DOMlet. There are two open issues with it.
First, should the path be interpreted eagerly (before calling) or lazily
(by the called template/DOMlet)? There are arguments both ways. Lazy
means that bugs could be harder to track, if the value is maybe passed down
multiple calling levels. On the other hand, eager means that you can't
conditionally not bother using one of the parameters. I think I'm going to
lean towards being lazy here, since it enables more things. Maybe there's
a way to have the error diagnostic report the location at which the path
was specified, not just the location at which the problem was detected.
So, on to the second issue: what DOMlet do the parameters get sent
to? This could be tricky if there are two DOMlets sharing an
element. (That is, if there is both a 'this:' DOMlet and a 'content:'
DOMlet on the same element.) And, if there's neither, do the parameters
"bubble up" to the next outer element?
I guess my inclination is to say that the parameters go to the innermost
element that wants parameters. For example, in:
<ul this:with="foo" content:list="."
with:listItem="++resources++/foo.bar/someTemplate">
<li>This is just sample data that will be ignored.</li>
</ul>
We want the 'listItem' parameter to be a specified template resource,
passed to 'content:list'. But here:
<span this:replace="@@someview" with:mode="simple">
blah blah
</span>
we want the 'mode' parameter to go to the '@@someview' being invoked.
I don't actually have a use case for these parameters "bubbling up" to a
containing element at the moment, except in cases where the parameter needs
to be multi-valued (and the target element allows it at compile
time). However, the current parameter machinery that already exists, does
this kind of bubbling up, so in the absence of any reason *not* to do it,
we'll just use that existing machinery. Currently, parameters are required
to implement IDOMletNode, so we may have to do some things to support these
new lazy path expression parameters.
Okay, so that's all settled now. On to the 'page:' attributes. In this post:
http://www.eby-sarna.com/pipermail/peak/2004-August/001715.html
I suggested using 'this:page_layout' to specify a DOMlet that would be used
to wrap the contents if the template were accessed as a web page, as
opposed to being used as a fragment in another template. If the value were
empty or "None", it would mean the page was standalone, and the default, if
no layout was specified, would be that the template could *only* be used as
a fragment.
But, if we have 'with:' and ':is' available, I don't think we need these
any more. Instead, we can define *parameters*, that bubble up to the
TemplateDocument object that is the template's top-level element! For
example, sticking 'with:layout="/default"' on the 'html' element of the
document could mean, "this is a standalone template", versus
'with:layout="some/path_expr"' to select a specific layout. Or, maybe
'this:is="fragment"', to mark the portion of the document that should be
used as a fragment, and 'this:is="document"' to mark a portion to be used
as a web page.
Hm. Let's consider the use cases. We want to be able to:
* Take a complete document and slice out a portion of it to be used as a
fragment, when invoked from another template
* Take a fragment, and embed it in a dynamically chosen layout.
* Deal intelligently with data that's outside of any XML/XHTML element,
such as the '<?xml' preamble, '<!DOCTYPE', and DTD stuff. ("Intelligently"
would mean, being able to consider it part of the document, if it is.)
Hm. So how about these special parameters:
* with:page-layout="some_layout" (or 'this:is="page-layout"')
* with:fragment-layout="something_else" (or 'this:is="fragment-layout"')
* with:content-type="text/whatever"
then, the document will either invoke page-layout or fragment-layout,
according to invocation mode. In either case, it will pass in any other
document-level parameters. So, if you have a document that defines
parameter blocks (this:is or content:is) to refer to a page header,
sidebar, footer, and so forth for the layout to include, then these will be
passed to either the fragment layout or the page layout. And, the fragment
layout could ignore everything but the body content, while the page layout
would include all that stuff.
One thing you could *not* do, however, is use this/content parameters to
specify a fragment nested within a document. In other words, you can't
take the document as a whole and then say, hey, use this bit inside here as
the fragment, and this outer part as the document. At least, you can't if
there are any parameter-accepting DOMlets between the two, because that
will stop the "bubbling-up" process. In principle, you can do it; it's the
practice that could be tricky. But I guess if you aren't manually invoking
a layout component on the document or any subset thereof, then this would
work just fine.
So, now the semantics: if you don't specify a page layout, or use /nothing,
the template shouldn't be usable as a page. If you use /default, then the
entire document, preambles and all, is the page layout. If you don't
specify a fragment layout, or you use /default, then the whole template is
the fragment layout. If you use /nothing, then the template isn't usable
as a fragment. So, the semantics for explicit values are the same, but
omitting the parameters has different meaning, defaulting to the safer
"don't allow access from the web" mode.
I'm almost tempted to say the parameter names should just be 'page' (or
'document') and 'fragment'. These make more sense when used with 'this:is'
and 'content:is'. But, when used with 'with:', the '-layout' names make
more sense. Maybe it should just be that way. That is, you use '-layout'
names to designate something that will be called with the other parameters,
and the non-layout names to designate a block to be used. So,
'with:page-layout="something"', versus 'this:is="page"', and similarly for
fragment-layout and fragment.
Whew. It's been a long haul, but worth it. I think we now have a syntax
and execution model that offers a great deal of flexibility, while still
mostly avoiding the pitfalls of turning into a scripting language. Its
main syntactual drawback is the need to define a lot of XML namespaces:
* this
* content
* with
* attr
* ns_attr
But probably 80% of documents can get by with just the first three, and
maybe 99% or more can get by with the first four. And, you don't have to
define any of them if your template isn't being run through tools that care
about valid XML namespaces. I'm also thinking it probably wouldn't be hard
to write a tool that would add the namespace definitions for you, pulling
the data from PEAK's default namespace settings.
Finally, I think I'm going to treat the /modules thing as a YAGNI for
now. The only thing left that might have used it is ':expects', but that's
going to use an import string directly. If you need constants and the
like, you can easily define them as views on relevant objects from your
sitemap, and sitemaps allow easy importing. I think we'll need to wait for
more "field experience" with these matters to see if the shortcut is really
useful.
One final question: /params or /args? Which name makes more sense as the
place where you find the things your template has been called
with? /params feels a bit more "right" to me, but /args is much shorter to
type while still conveying the same basic idea.
Thoughts, anyone?
More information about the PEAK
mailing list