fill_template()fill_template()
This document covers the concept of customizing the publish process by making changes to the element library. It assumes that you've already got an understanding of templates and element libraries in Krang.
It's a good idea to have read the following documents before going any further:
Out of the box, Krang populates element templates according to a fixed set of rules. The standard publish process should be sufficient for publication of most sites. That being said, choices in how data is returned and how data is organized in the templates have been made - if these choices don't work with what you're attempting to accomplish, your next step is to change the behavior of the elements themselves.
The simplest thing to change in an element is the form the element's data takes when returned. With a few exceptions (see below) elements return data in the same form as it was when stored. The advantage to this technique is that the results will be seen, regardless of whether or not a template is used.
template_data()template_data() will return the fully-qualified URL of the object.
Suppose you wanted all header elements to return their data in all-caps when published, regardless of how they were entered into the system. At the same time, you don't want to actually make that change to the content itself, in case you change your mind later. Overriding template_data() in your element library's header.pm as follows will do the trick:
sub template_data {
my $self = shift;
my %args = @_;
my $element = $args{element};
return uc($element->data());
}
Elements that handle links to Stories and Media need to be handled a little bit differently - they need to return the URL of the object being pointed to, rather than the data itself, and they need to return a URL that's consistent with the current output mode - publish or preview. Keep this in mind if you consider changing the behavior for either of these two.
Here is how template_data() currently works for elements using Krang::ElementClass::StoryLink -
sub template_data {
my $self = shift;
my %args = @_;
my $element = $args{element};
if ($args{publisher}->is_publish()) {
return 'http://' . $element->data()->url();
} elsif ($args{publisher}->is_preview()) {
return 'http://' . $element->data()->preview_url();
} else {
croak (__PACKAGE__ . ': Not in publish or preview mode. Cannot return proper URL.');
}
}
In short, it queries the publisher ($args{publisher}) to determine if the mode is publish or preview (returning an error if it's neither). $element-data()> returns a Krang::Story object (if this was Krang::ElementClass::MediaLink, it would be a Krang::Media object). Depending on the mode. the appropriate URL is returned.
The next option is more ambitious - changing how an element goes about populating the variables in a template. At this point, you have two options - you can piggyback your changes on top of the work that Krang does, or you can choose to do it all yourself.
fill_template()fill_template() is responsible for filling the template object with data built from the element tree. Generally, it traverses the element tree, creating scalars and loops on an as-needed basis, populating the template objects with the results. The rules by which fill_template() operates can be found in the section How Krang Builds Template Data in Writing HTML::Template Templates in Krang.
If you are familiar with Bricolage, fill_template() functions using the same rules as the autofill() functionality found in Bricolage.
With the object hierarchy Krang provides, you can make small additions to fill_template() and then let Krang pick things up from there by calling the parent method's fill_template().
For these examples, we will be using the following Story element:
Story
- Deck (subclass of Krang::ElementClass::Text)
+ Page (subclass of Krang::ElementClass)
- Header (subclass of Krang::ElementClass::Text)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Pull Quote (subclass of Krang::ElementClass::Text)
- Paragraph (subclass of Krang::ElementClass::TextArea)
The story element will use the following templates:
<tmpl_loop page_loop>
<html>
<head>
<title><tmpl_var title></title>
</head>
<body>
<tmpl_if __first__>
<h1><tmpl_var title></h1>
<b><tmpl_var deck></b>
</tmpl_if>
<tmpl_var page>
</body> </html>
<tmpl_unless __last__>
<tmpl_var page_break>
</tmpl_unless>
</tmpl_loop>
<h2><tmpl_var header></h2>
<tmpl_loop element_loop>
<tmpl_if is_paragraph>
<p><tmpl_var paragraph></p>
</tmpl_if>
<tmpl_if is_pull_quote>
<p><blockquote><i>
<tmpl_var pull_quote>
</i></blockquote></p>
</tmpl_if>
</tmpl_loop>
IMPORTANT NOTE: Starting in Krang v3.02, developers have the option of using one complex template for each story type rather than multiple simple ones. The procedure for using a single template is beyond the scope of this document but described in Writing HTML::Template Templates in Krang. (The examples in this document all make use of the multiple-template approach.)
As a simple example, we want to add a variable greeting to the page template. This can be done by overriding fill_template in the article element, and adding a variable publish_time to the template. Other than this variable, the template should be populated as usual.
This is the new fill_template() that would be used in the Page element:
sub fill_template {
my $self = shift;
my %args = @_;
my $template = $args{tmpl};
$template->param(greeting => 'Hello World!');
return $self->SUPER::fill_template(@_);
}
In short, add the variable greeting to the template, and then call the parent fill_template() method that was overridden by this method (passing the original set of parameters along). The rest of the publish process is unaffected, and nothing will be noticed on output until the article template uses publish_time.
This new Page template will display the greeting:
<h1><tmpl_var greeting></h1>
<h2><tmpl_var header></h2>
<tmpl_loop element_loop>
<tmpl_if is_paragraph>
<p><tmpl_var paragraph></p>
</tmpl_if>
<tmpl_if is_pull_quote>
<p><blockquote><i>
<tmpl_var pull_quote>
</i></blockquote></p>
</tmpl_if>
</tmpl_loop>
The output for Page.tmpl (not the entire story, mind you) will look something like this:
<h1>Hello World!</h1>
<h2>Header Header</h2>
<p>paragraph1 paragraph1 paragraph1</p>
<p><blockquote><i>
Quote Quote Quote
</i></blockquote</p>
<p>paragraph2 paragraph2 paragraph2</p>
If you choose to populate the template manually, all the variables that Krang builds no longer apply. Additionally, it will be up to you to build variables based on the child elements of the current element.
fill_template() takes a set of named parameters -
fill_template() is expected to return the HTML that results from populating the template.
Read the Krang::ElementClass API documentation for further documentation on the actual interface.
This example re-uses the example from Option 1 - adding a single variable greeting to the template.
fill_template() for the Page element - take 1
sub fill_template {
my $self = shift;
my %args = @_;
my $template = $args{tmpl};
$template->param(greeting => 'Hello World!');
return $template->output();
}
Where the previous example in option 1 made a call to $self-SUPER::fill_template()>, this example simply calls $template-output()>. The result is that in this example, with no parent method to do additional work populating the template, the only variable available to the template is greeting.
Page.tmpl
The template from the previous example will still work:
<h1><tmpl_var greeting></h1>
<h2><tmpl_var header</h2>
<!-- Contributors -->
<tmpl_if contrib_loop>
By:
<tmpl_loop contrib_loop>
<!-- Determine whether we need a comma or an "and" to separate -->
<tmpl_unless __first__>
<tmpl_if __last__>
and
<tmpl_else>
,
</tmpl_if>
</tmpl_unless>
<!-- First Middle Last (Job) -->
<tmpl_var first> <tmpl_var middle> <tmpl_var last>
<tmpl_if contrib_type_loop>
(
<tmpl_loop contrib_type_loop>
<tmpl_var contrib_type_name>
<tmpl_if __last__>)</tmpl_if>
</tmpl_loop>
</tmpl_if>
</tmpl_loop>
</tmpl_if>
<tmpl_loop element_loop>
<tmpl_if is_paragraph>
<p><tmpl_var paragraph></p>
</tmpl_if>
<tmpl_if is_pull_quote>
<p><blockquote><i>
<tmpl_var pull_quote>
</i></blockquote></p>
</tmpl_if>
</tmpl_loop>
However, with Krang not providing any additional variables, the template output will look like this:
<h1>Hello World!</h1>
<h2></h2>
Clearly, the output for the above template isn't what we're looking for - the header is missing, along with the entire element loop. The next step is to add these:
fill_template() for the Page element - take 2
sub fill_template {
my $self = shift;
my %args = @_;
my @element_loop; my %params;
my $template = $args{tmpl};
my $element = $args{element};
my $publisher = $args{publisher};
$params{greeting} = 'Hello World!';
# retrieve the list of child elements my @children = $element->children();
foreach my $child (@children) {
my $name = $child->name();
my $html = $child->publish(publisher => $publisher);
unless (exists($params{$name})) {
$params{$name} = $html;
}
push @{$params{element_loop}}, { "is_$name" => 1, $name => $html };
}
$template->param(%params);
return $template->output();
}
Make sense? Rather than make a lot of calls to $template-param()>, parameters are stored in %params until all work is finished. The loop at the bottom iterates over the list of children (@children), building HTML for each child, and then placing the results in %params - you can see the element_loop being built there as well.
The resulting output looks like what we want:
<h1>Hello World!</h1>
<h2>Header Header</h2>
<p>paragraph1 paragraph1 paragraph1</p>
<p><blockquote><i>
Quote Quote Quote
</i></blockquote</p>
<p>paragraph2 paragraph2 paragraph2</p>
Adding contributors here is a straightforward process - a single method call makes it possible:
$contrib_loop = $self->_build_contrib_loop(@_);
This can be added to fill_template() as follows:
fill_template() for the Page element - take 3
sub fill_template {
my $self = shift;
my %args = @_;
my @element_loop; my %params;
my $template = $args{tmpl};
my $element = $args{element};
my $publisher = $args{publisher};
$params{greeting} = 'Hello World!';
$params{contrib_loop} = $self->_build_contrib_loop(@_);
# retrieve the list of child elements my @children = $element->children();
foreach my $child (@children) {
my $name = $child->name();
my $html = $child->publish(publisher => $publisher);
unless (exists($params{$name})) {
$params{$name} = $html;
}
push @{$params{element_loop}}, { "is_$name" => 1, $name => $html };
}
$template->param(%params);
return $template->output();
}
With the contrib_loop now added to the template:
Page.tmpl
<h1><tmpl_var greeting></h1>
<h2><tmpl_var header</h2>
<!-- Contributors -->
<tmpl_if contrib_loop>
By:
<tmpl_loop contrib_loop>
<!-- Determine whether we need a comma or an "and" to separate -->
<tmpl_unless __first__>
<tmpl_if __last__>
and
<tmpl_else>
,
</tmpl_if>
</tmpl_unless>
<!-- First Middle Last (Job) -->
<tmpl_var first> <tmpl_var middle> <tmpl_var last>
<tmpl_if contrib_type_loop>
(
<tmpl_loop contrib_type_loop>
<tmpl_var contrib_type_name>
<tmpl_if __last__>)</tmpl_if>
</tmpl_loop>
</tmpl_if>
</tmpl_loop>
</tmpl_if>
<!-- /Contributors -->
<tmpl_loop element_loop>
<tmpl_if is_paragraph>
<p><tmpl_var paragraph></p>
</tmpl_if>
<tmpl_if is_pull_quote>
<p><blockquote><i>
<tmpl_var pull_quote>
</i></blockquote></p>
</tmpl_if>
</tmpl_loop>
The resulting output will look something like this:
<h1>Hello World!</h1>
<h2>Header Header</h2>
<!-- Contributors -->
By: JR Bob Dobb (Writer, Photographer) and Venus Dee Milo (Illustrator)
<!-- /Contributors -->
<p>paragraph1 paragraph1 paragraph1</p>
<p><blockquote><i>
Quote Quote Quote
</i></blockquote</p>
<p>paragraph2 paragraph2 paragraph2</p>
Go back to the Contributors section of Writing HTML::Template Templates in Krang for further documentation on using Contributors.
It may come about that you want to pass information along to a child element for use when it goes through the publish process. This can be done by adding arguments to the named parameters passed to $child-publish()>.
If the child element you are calling is still using the fill_template() method provided by Krang, you can use the parameter fill_template_args in the fashion below:
foreach my $child ($element->children()) {
my %new_args = (greeting => 'Hello World!');
my $html = $child->publish(publisher => $publisher, fill_template_args => \%new_args);
$params{$child->name} = $html;
}
When the child element goes through the publish process, its fill_template() method will add greeting to template, provided the template is looking for a variable greeting. Keep in mind, you don't need to override fill_template() in the child element - this functionality is supported out-of-the-box.
Using the same Page.tmpl we started with at the beginning of these examples:
Page.tmpl
<h1><tmpl_var greeting></h1>
<h2><tmpl_var header></h2>
<tmpl_loop element_loop>
<tmpl_if is_paragraph>
<p><tmpl_var paragraph></p>
</tmpl_if>
<tmpl_if is_pull_quote>
<p><blockquote><i>
<tmpl_var pull_quote>
</i></blockquote></p>
</tmpl_if>
</tmpl_loop>
Rather than override the Page element's fill_template() method, we're going to use the one provided by Krang. Instead, we're going to override the fill_template() method for the Story element, and have it pass along the greeting to the page element.
fill_template() for the Story Element
sub fill_template {
my $self = shift;
my %args = @_;
my @element_loop; my %params;
my $template = $args{tmpl};
my $element = $args{element};
my $publisher = $args{publisher};
my $story = $publisher->story();
$params{title} = $story->title();
# retrieve the list of child elements my @children = $element->children();
foreach my $child (@children) {
my $name = $child->name();
my $html = $child->publish(publisher => $publisher,
fill_template_args => { greeting => 'Hello World!' });
unless (exists($params{$name})) {
$params{$name} = $html;
}
if ($name eq 'page') {
push @{$params{"$name_loop"}}, { $name => $html };
}
}
$template->param(%params);
return $template->output();
}
With the Story element passing the greeting parameter along, you don't need to override the fill_template() method in the Page element - the standard method used by Krang will suffice.
fill_template()Continuing from the previous example, if we were to override the Page element fill_template() method, we'd need to handle the fill_template_args parameter as well:
Page Element
sub fill_template {
my $self = shift;
my %args = @_;
my %params;
my $template = $args{tmpl};
my $element = $args{element};
my $publisher = $args{publisher};
# Additional template params passed in by the parent element.
if (exists($args{fill_template_args})) {
foreach my $arg (keys %{$args{fill_template_args}}) {
$params{$arg} = $args{fill_template_args}{$arg};
}
}
# retrieve the list of child elements my @children = $element->children();
foreach my $child (@children) {
my $name = $child->name();
my $html = $child->publish(publisher => $publisher);
unless (exists($params{$name})) {
$params{$name} = $html;
}
push @{$params{element_loop}}, { "is_$name" => 1, $name => $html };
}
$template->param(%params);
return $template->output();
}
The small block that consists of:
if (exists($args{fill_template_args})) {
foreach my $arg (keys %{$args{fill_template_args}}) {
$params{$arg} = $args{fill_template_args}{$arg};
}
}
Is the extent of what's needed to handle fill_template_args.
While we use fill_template_args in the previous examples, you can call
$child->publish(publisher => $publisher, any_param_name_you_want => $foo);
And the additional parameter(s) will get passed along to
fill_template() method of the child object. It is up to you, of
course, to make use of it on that end - the standard Krang
implementation only makes use of fill_template_args.
While publishing a given story, you may want to publish additional files containing data related to the story. For example, an RDF file for syndication, or an XML file containing keywords for search-engines, or an article preview page for subscription purposes.
While Krang doesn't provide direct support for such things within the UI, it provides a framework for you to use within your element library, allowing you to build the content in any way you see fit.
During the publish process, you can add additional content at any
point that you have the Krang::Publisher object available. For example:
# Write out 'extra.html' in conjunction with this story. my $additional_content = create_sidebar_story();
$publisher->additional_content_block(content => $additional_content,
filename => 'extra.html',
use_category => 1);
At the end of the publish process, Krang will handle the entry in
additional_content separately from the main story, and write it to
disk as 'extra.html' (or whatever you set filename to be).
What is Supported:
use_category to 1
if you want to wrap $additional_content in the category templates,
or set it to 0 if you want it to be written to disk as-is.
Restrictions:
For the following examples, we're going to use the following Story element tree:
Story
- Deck (subclass of Krang::ElementClass::Text)
+ Page (subclass of Krang::ElementClass)
- Header (subclass of Krang::ElementClass::Text)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Pull Quote (subclass of Krang::ElementClass::Text)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Leadin (subclass of Krang::ElementClass::StoryLink)
- Leadin (subclass of Krang::ElementClass::StoryLink)
- Leadin (subclass of Krang::ElementClass::StoryLink)
+ Page (subclass of Krang::ElementClass)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Pull Quote (subclass of Krang::ElementClass::Text)
- Paragraph (subclass of Krang::ElementClass::TextArea)
+ Page (subclass of Krang::ElementClass)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Paragraph (subclass of Krang::ElementClass::TextArea)
- Paragraph (subclass of Krang::ElementClass::TextArea)
If you aren't familiar with RSS (RDF Site Summary), take a look here: RDF Site Summary 1.0
This example uses XML::RSS, which is not part of Krang - it would have to be installed separately.
To generate the RSS file, we're going to override the fill_template() method for the Story element to generate the RSS file, and then continue the publish process, adding the RSS file to the final output.
sub fill_template {
my $self = shift;
my %args = @_;
my $rss = new XML::RSS;
my $publisher = $args{publisher};
my $story = $publisher->story();
$rss->channel(title => $story->title(),
link => 'http://' . $story->url(),
description => $story->slug()
);
foreach my $leadin ($story->linked_stories()) {
$rss->add_item(title => $leadin->title(),
link => 'http://' . $leadin->url());
}
my $rss_output = $rss->as_string();
return $self->SUPER::fill_template(@_) .
$publisher->additional_content_block(content => $rss_output,
filename => 'rss.xml',
use_category => 0);
}
As you can see, the regular publish work is still done by Krang, in the call at the bottom, $self->SUPER::fill_template(@_). That output is concatenated with the output generated by the XML::RSS module (after being tagged properly by $publisher->additional_content_block()), and returned to the Publisher, which will then write the two files out.
The use_category option to Krang::Publisher->additional_content_block() tells the Publisher to not combine the output from XML::RSS with any template output from the categories (e.g. headers/footers). While this is a desireable feature sometimes, we don't want to mix HTML templates with XML output in this case.
We want have two goals here - first, we want to build a three-page story out of this tree. Second, we want to create a wall page using the content of the first page, and a wall template.
Again, the best angle of attack here will be to work from the Story element - we want to manipulate content depending on which page element we're on, and page elements don't know about eachother. Additionally, we need access to elements that won't be available to Page elements.
fill_template() - Story ElementIn overriding the fill_template() method in the story element, we're going to maintain two separate hashes of parameters - one will be used for the regular story template, the other for the wall page template.
sub fill_template {
my $self = shift;
my %args = @_;
my %params;
my %wall_params;
my $wall_template = $self->_load_template(@_,
filename => 'wall.tmpl',
search_path => ['/foo/bar']);
my $template = $args{tmpl};
my $element = $args{element};
my $publisher = $args{publisher};
my $story = $publisher->story();
$params{title} = $story->title();
$wall_params{title} = $story->title();
# retrieve the list of child elements
my @children = $element->children();
foreach my $child (@children) {
my $name = $child->name();
my $html = $child->publish(publisher => $publisher,
fill_template_args => { greeting => 'Hello World!' });
unless (exists($params{$name})) {
$params{$name} = $html;
$wall_params{$name} = $html;
}
if ($name eq 'page') {
push @{$params{"$name_loop"}}, { $name => $html };
unless (exists($wall_params{"$name_loop"})) {
push @{$wall_params{"$name_loop"}}, { $name => $html };
}
}
}
$template->param(%params);
$wall_template->param(%wall_params);
my $html = $template->output();
my $wall_html = $wall_template->output();
$html .= $publisher->additional_content_block(filename => 'wall.html',
content => $wall_html,
use_category => 1
);
return $html;
}
The end result is the original three-page story, along with a wall.html file.
Note - using the approach of the first example, calling $self->SUPER::fill_template() to generate the content for the story itself could have been done here as well, but it would have incurred additional overhead, as some elements would have been published multiple times (the first page and all its child elements), creating a performance penalty. While the penalty is negligible in this case, be careful.
The process by which Krang chooses a publish template for a given element is as follows:
element-name.tmpl .
/SITENAME/foo/bar, start by looking for the template in /SITENAME/foo/bar/element-name.tmpl.
Krang::ElementClass::TemplateParseError).
/SITENAME/foo/bar/element-name.tmpl, try /SITENAME/foo/element-name.tmpl, /SITENAME/element-name.tmpl, finally /element-name.tmpl. If the template is found at any point, load the template as seen in step 3.
Krang::ElementClass::TemplateNotFound).
You can make changes to this process by overriding find_template() to change what template Krang will look for, where Krang will look for it, or even what kind of template will be loaded.
Loading an HTML::Template::Expr template requires two things - the template filename, and a list of directories to search for the template.
sub find_template {
my $self = shift;
my %args = @_;
my $tmpl;
my $publisher = $args{publisher};
my $element = $args{element};
my @search_path = $publisher->template_search_path();
my $filename = $element->name() . '.tmpl';
my $template = $self->_load_template(publisher => $publisher,
element => $element,
filename => $filename,
search_path => \@search_path);
return $template;
}
By making changes to either $filename or @search_path (ordered from first to last in terms of directories to search), you can affect what template gets loaded. $self->_load_template() handles the actual process of finding, loading, and throwing any required errors for HTML::Template::Expr templates.
If you want to change the type of template being loaded (e.g. you don't want to use HTML::Template::Expr templates), you need to roll your own code to find, load and parse the templates, throwing appropriate errors as needed. Be aware that find_template() and publish() are expecting an HTML::Template::Expr template, so you will need to override fill_template() and publish() functionality as well.
find_template() and fill_template() methods together. Returns publish output for the current element (and therefore, any children beneath it).
See Krang::ElementClass for more info on publish().
publish() acts as the coordinator of the publish process for a given element, making sure that the entire process runs smoothly. It works as follows:
find_template().
By default, if an element has no children, publish() will simply return $element-template_data()>. If the element has children, however, it will propegate the Krang::ElementClass::TemplateNotFound error thrown by find_template().
fill_template().
fill_template() is finished, return $template->output().
While Krang, by default, does not call publish() on any element that is not explicitly included in a template, this may not offer enough protection for elements you don't want published. Overriding publish to return will make sure that an element (and all of its children) will never be published.
sub publish {
return;
}
On the other hand, if you know an element will never have a template (or want to make sure that noone goofs things up by creating a template for that element), you can simplify the publish process greatly (again, no children would get published):
sub publish {
my $self = shift;
my %args = @_;
my $element = $args{element};
return $element->template_data();
}
Starting in Krang v3.04, Media objects can include element data, and this data can in turn be published when the Media objects are published. (See Krang::ElementClass::Media for an example.) This is an advanced feature which is still being developed.
This covers the major aspects of customizing the Krang publish process. By overriding the three methods fill_template(), find_template() and publish(), there's a lot that can be done to change how a story publishes itself.
At this point, if you want to learn more about how the publish process works, and what can be done, read the POD and the code itself for Krang::ElementClass and Krang::Publisher. Good luck!