Ruby Metaprogramming is Awesome

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:


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

>>{|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 {{|m| m.to_s =~ /Tags$/}.reject{|m| m == Sns::PageTags }.each do |mod|
        include mod

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() = 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) }

There you have it. All it took was some class_eval and some looping over included_modules.

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

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