Ever wondered how Radiant does its magic? What do all those tags mean and how do they make stuff happen? How to use the tags has already been discussed, but not how they work.
In the second of our “How To” series, I’m going to walk you through how to read and understand Radiant’s Radius tag definitions (i.e. code). For this How To, it helps to understand Ruby and know some stuff about Rails, but just in case you don’t, I’ll fill you in on the needed knowledge as we go.
Reading and Understanding Tags
Radius tags are what give you the hooks into cool functionality offered by Radiant. There are a number of tags which are available to every page and some that are specific to individual behaviors. Let’s start by analyzing one of the built-in tags that can be found in the file app/models/page_context.rb
. These tags are available to any page, snippet, or layout in Radiant.
Anatomy of a Tag Definition: r:content
The r:content
tag in Radiant displays the content of a given “part” of a page—those tabs you see when you are editing one. With no part
attribute specified, it will display the “body” part/tab. Let’s take a look at how it works.
If you want to follow along, please look at the source of page_context.rb
around line 194. This is a fairly complex tag, but we’ll take it slow.
define_tag 'content' do |tag|
This is the standard tag definition. It begins with define_tag
which says we’re going to define a new tag. The next thing is 'content'
, which is the name of the tag. If this tag were scoped (we’ll talk about that more below), like children:each
, we would have a string of colon-separated names. The next two things are do |tag|
which signifies we’re going to start a block that has a single parameter named tag
. Blocks are a Ruby feature that lets you define code that can be reused in different contexts, kind of like anonymous functions in Java or Javascript. For our purposes, though, just know that the do |tag|
means, “here comes the tag definition, so listen up and give me a hook into the context called tag
”.
This type of declaration is only valid in PageContext, whereas in your cool new behavior that you’ll be writing after this How To, the same tag would look like:
define_tags do
tag 'content' do |tag|
This is a subtle difference, but one to take note of. Also notice that the declaration in PageContext
takes place in the initialize
method, whereas the second one would be at the class level of your behavior (i.e. not inside initialize
). If that last sentence is gibberish to you, don’t worry about it; you don’t need to understand that to get the rest of this How To.
The end of each tag definition is marked by the end
keyword, which you can see on line 216 (Actually, all blocks like these are terminated with end
).
Okay, now that we have our tag definition started, let’s look at what happens next. We’ll take the first two lines.
page = tag.locals.page
part_name = tag_part_name(tag)
The first line creates a variable named page
that refers to tag.locals.page
. The nice thing about the tag
parameter that gets passed in is all the goodies that come along with it. tag.locals
is a collection of variables that refer to the current context, and were set by tags that occur outside the current tag. So if you need to get information about where you are and what’s around, tag.locals
will tell you. In this case, we’re grabbing the page
variable to figure out what the current page is. Interestingly enough, the page
variable refers to a Rails model named Page
. (Models are objects that abstract and encapsulate information that a web application (RadiantCMS) uses. In this case, it’s a web page that you can browse to.)
So once we have this Page
, we can get all kinds of information about what content was typed into the current page, when it was published, who wrote it, etc. The next line calls a function that extracts the designated part. Parts, or PagePart
models, are the tabs that you see when editing a page. The second line of code will call a custom function that gives us the value of the tag’s attribute part
. Jump down to line 508 to see what the function does. The function clues us into another useful item on the tag
variable, tag.attr
. This gives us access to the attributes that the user put on the tag in their page. The function grabs the “part” attribute, or in case it wasn’t defined, gives us “body”. Head back to line 197 and we’ll go on.
boolean_attr = proc do |attribute_name, default|
attribute = (tag.attr[attribute_name] || default).to_s
raise TagError.new(%{`#{attribute_name}' attribute of `content' tag must be set to either "true" or "false"}) unless attribute =~ /true|false/i
(attribute.downcase == 'true') ? true : false
end
We’re going to gloss over these lines a little bit because they involve some Ruby “magic”. Remember when I talked about blocks before? Here we’re defining a block inline and assigning it to a variable so we can use it later in the tag definition. Overall, this code says:
- Make me a block with two parameters,
attribute_name
and default
, and hold onto that block in the variable boolean_attr
. - Inside this block, grab the tag’s attribute of the name that we passed in and keep a copy of it, or the default value, in the
attribute
variable. - If somebody gave us something other than “true” or “false”, throw up a red flag (
TagError
). - Assuming there’s no error, if the attribute is the text “true”, give me back the boolean
true
value, otherwise give me false
.
If you look at the next line, we can see how the block we just created is used.
inherit = boolean_attr['inherit', false]
This says, get the value of the attribute “inherit” as a boolean or give me false, then store it into our local variable “inherit”. The “inherit” attribute on the r:content
tag says that if the selected page part doesn’t exist, get whatever the parent or closest ancestor page has; and that’s exactly what the next few lines of code do.
part_page = page
if inherit
while (part_page.part(part_name).nil? and (not part_page.parent.nil?)) do
part_page = part_page.parent
end
end
- Start with the current page, store it in
part_page
. - If we’re supposed to inherit the content from an ancestor page,
- Search through the ancestor pages until one of them has the required part and hold onto that in
part_page
.
The next line checks another boolean attribute, this time it’s contextual
.
contextual = boolean_attr['contextual', true]
The contextual
attribute says whether an inherited page part, that is, if the current page doesn’t have it and we used the inherited
attribute, should be rendered in the context of the current page. In most cases, this is what we want, so it defaults to true. If it’s false, we’ll render it in the context of the page it belongs to.
Our next section of code finally does the output of the content, now that we’re all prepared.
if inherit and contextual
part = part_page.part(part_name)
page.behavior.render_snippet(part) unless part.nil?
else
part_page.behavior.render_page_part(part_name)
end
A quick analysis:
- If we inherited the part and we’re rendering it in the context of the current page,
- Get the part of the page that we found.
- Use the current page’s behavior to render the part, unless it’s empty.
- Otherwise, render the page part in the context of the page it came from.
There you have it! An analysis of one of the built-in tag definitions.
Tying up Loose Ends
We saw a lot of code above, but unfortunately, we didn’t see everything you’ll need to know to read tag definitions or to create your own. Here’s a few more pointers.
tag.locals
is very flexible, and anything you set will be available to any tags nested inside the current one. So, if you want all the containing tags to have access to your local foo
variable, set tag.locals.foo = foo
and then those tags can access it. Set tag.locals.page
if you want to tell containing tags to refer to a different page. This is how r:children:each
works.tag.globals
works just like tag.locals
, except it generally isn’t changed in the context. If you want the current page that is being rendered (ignoring the context), access tag.globals.page
.tag.attr
, as we saw above, contains key-value pairs for every attribute that the author put on the Radius tag. So if I wrote
, the tag.attr['part']
would be assigned “mine” inside the ‘content’ tag definition.- Whatever is mentioned on the last line of the tag definition is what will be passed back to the parent tags in the nesting order, and ultimately output on your page. So if I created a tag like so:
·
define_tag "foo" do |tag|
·
"bar"
end
It would output “bar” (without the quotes) anytime I use the r:foo
tag in a page.
- You can find out if the tag was called as a singleton or a container (singleton tags end in a slash and don’t contain other tags). If it’s a singleton tag,
tag.single?
will return true, if it’s a container, tag.double?
will return true. - Most tags either output something or redefine the context and then pass on the output to tags contained inside, or a little of both. To process the output of the contained tags and text, we call
tag.expand
. Take, for example, this simple tag definition that allows you to access the parent page:
·
define_tag "parent" do |tag|
·
tag.locals.page = tag.locals.page.parent
·
tag.expand unless tag.locals.page.nil?
end
Notice how we reset the context to the parent page (assigned tag.locals.page
), then evaluated the contents inside the r:parent
tag if the parent page exists.
- In addition to the context created by
tag.locals
, tags can have a scoped context. This is signified by the colons in the tag definition. The colons signify the nesting level of the tag. So if I write this in my page,
Radiant would look for a tag definition for “foo:bar”, and if it can’t find that, one named “bar”. In addition to this, it will call the tag definition for “foo”, which should have a call to tag.expand
which ultimately renders “foo:bar” or “bar”. So, in a sense, the above tag is equivalent to:
In this way, you can redefine how certain tags are rendered in different nesting combinations. If I wanted to change how the r:link
tag operates inside my foo
tag, I’d create a tag definition for foo:link
, then do what I want inside that tag definition.