Ruby Metaprogramming is Awesome

by Jim Gay

If you’ve come to read about how wrong I am about metaprogramming, don’t worry, I’m sure I’ll follow-up with a post about how bad it is to do metaprogramming and how it causes you real, physical pain.

If, however, you’ve come to find out about how awesome metaprogramming is, then you can tell by the title of this article that you are in the right place! Now that’s authoritative!

The best part about this article is that it is for you! That’s right, newbie. I’m not going in-depth, I’m merely going to discuss how a dash of metaprogramming solved an annoying problem.

I do a lot of work with Radiant. And Radiant manages pages well, but there are plenty of extensions that allow you to do other things. You can, for example, add the ability to upload files and whatnot. Or you can add the ability to edit stylesheets and javascripts.

That’s pretty cool. Radiant does a good job of managing pages in the pages table. But stylesheets and javascripts are not pages. Some of the samples that Radiant bundles have stylesheets as regular pages in the page tree and are tied to an almost blank layout that sets a particular content-type like text/css. Yuck. So Chris Parrish, the author of SNS, added text_assets to handle other types of content like stylesheets and javascripts.

One of his reasons for creating this extension is the weirdness of having a stylesheet in your page tree: “No more confused users wonder what those fancy pages are.” Saaaweet! Thanks, Chris.

All is well until you find out that the SNS extension doesn’t let you use your typical Radius tags within javascripts or stylesheets like:

#something { background: url('< r:assets:url title="my_image" />'); }

Nooooooo! Why, God, why!?!

Well, with SNS, you’re dealing with a TextAsset and not a Page. But if you’re familiar with Radiant, you know that page tags aren’t only used on pages, they’re used on snippets and layouts too. So what’s the deal with that?

All of the radius tags that you use in Radiant are evaluated in the context of a single page. Pages, snippets, and layouts are all used in reference to rendering a Page. But SNS renders a TextAsset and as a result, it doesn’t have any of the fancy tags added. Afterall, it’s class TextAsset < ActiveRecord::Base and not class TextAsset < Page.

You read that right: text assets are not pages. Bummer, dude.

Well, what if you could include the same modules as are included in the Page model? Then you could use those tags in your stylesheets.

Fire up your console because you’re about to see how to do it. In a rails console (for a Radiant instance), try this:

Page.included_modules

And you’ll get back a bunch that don’t matter for you. Let’s simplify that:

>> Page.included_modules.select{|m| m.to_s =~ /Tags$/}
=> [TextileTags, SmartyPantsTags, MarkdownTags, StandardTags]

Awesome. All we need to do is include all of those modules into TextAsset and we’re done! Almost.

Since we’re dealing with the SNS extension, you’ll also see that it includes Sns::PageTags which adds tags to output stylesheets and javascripts. Those, we don’t need. So we can filter them out and include them into TextAsset:

TextAsset.class_eval {
  Page.included_modules.select{|m| m.to_s =~ /Tags$/}.reject{|m| m == Sns::PageTags }.each do |mod|
    include mod
  end
}

Bingo! Now we’re in business. Except for the fact that it doesn’t work.

So we need to update the context in which these tags are evaluated. Since they are all written expecting to be included in a Page model, the global variables for the tags need to have knowledge of a page.

TextAssetContext.class_eval {
  def initialize(text_asset)
    super()
    globals.page = text_asset # This is the important line that adds the hook for all existing tags for pages.
    globals.text_asset = text_asset
    text_asset.tags.each do |name|
      define_tag(name) { |tag_binding| text_asset.render_tag(name, tag_binding) }
    end
  end
}

There you have it. All it took was some classeval and some looping over includedmodules.

page tags in stylesheets And now you’ll have happy users. Because even though Chris solved the problem of weird stylesheet pages in the page tree, the solution introduced a problem where editors of the site expect the radius tags to simply work everywhere. Now they do.

The above code assumes that the only valuable tags are those whose modules have “Tags” at the end of their names. That’s a reasonable expectation, but we can go even further by inspecting the included modules of the included modules to see if Radiant::Taggable is there:

TextAsset.class_eval {
  Page.included_modules.reject{|m| m == Sns::PageTags }.each do |mod|
    if mod.included_modules.any? {|inc| inc == Radiant::Taggable }
      include mod
    end
  end
}

So go ahead and install the page attachments, or paperclipped, or whatever extension with sns and just do script/extension install snspagehook, or better yet: gem install radiant-snspagehook-extension. Please let me know if you find any problems with it.

Comments

Wes Gamble said on Friday, July 02, 2010:

Rails 2.3.8
Radiant 0.9.1

When I install this gem and include it in my config file using:

config.gem ‘radiant-sns_page_hook-extension’, :lib => ‘sns_page_hook_extension’, :version => ‘>= 1.0.0’

and restart my server, I get:

/Users/weyus/Documents/workspace/koached-content/vendor/radiant/lib/radiant/extension.rb:106:in `inherited’: undefined method `to_name’ for SnsPageHookExtension (NoMethodError)

Not sure what the problem is.

Jim Gay said on Saturday, July 03, 2010:

Wes,

Can you try this:

config.gem ‘radiant-sns_page_hook-extension’, :lib =&gt;false

Amr said on Sunday, August 22, 2010:

Is it possible to edit /css or /js slug path through Settings ext??
Its really needed!

Jim Gay said on Wednesday, August 25, 2010:

Amr, SNS has details about how to edit those paths. http://github.com/radiant/radiant-sns-extension/blob/master/lib/tasks/sns_extension_tasks.rake#L81

omer said on Thursday, October 28, 2010:

Whenever I tried,

uninitialized constant SnsPageHookExtension::TextAsset

What is problem?

omer said on Thursday, October 28, 2010:

hello,
I had problem about installation sns-extension
I solved problem
I installed sns before
I am new on radiant, please update documentation for sns extension
omer

Jim Gay said on Thursday, October 28, 2010:

Omer,
This article assumes that you are using SNS. I'm glad you got it working, but it's pretty clear about that.

1999 - 2014 © Saturn Flyer LLC 2321 S. Buchanan St. Arlington, VA 22206

Call Jim Gay at 571 403 0338