/html_namespacing

Automatically adds HTML class attributes to partial HTML code

Primary LanguageC

HTML Namespacing

HTML Namespacing automatically adds HTML class attributes to partial HTML code.

The intent is for HTML, CSS, and JavaScript files to be “namespaced” according to their location in the filesystem. That way, CSS and JavaScript can be scoped to individual HTML files–even automatically.

Installing

gem install adamh-html_namespacing --source http://gems.github.com

Using

HTML Namespacing can be used on its own for a snippet of code:

require 'rubygems'
require 'html_namespacing'

html = '<p>Here is some <em>HTML</em>!</p>'

# Returns '<p class="foo">Here is some <em>HTML</em>!</p>'
namespaced = HtmlNamespacing::add_namespace_to_html(html, 'foo')

Details

Only root tags will be namespaced. For instance, all p tags in this example will have a class added, but no other tags:

<p>This is a root tag. <b>This tag is nested.</b></p>
<p>This is another root tag.</p>

Because XML, DOCTYPE, comments, and html tags do not allow the class attribute, the following HTML will pass through unchanged:

<?xml version="1.0"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Here is a comment -->
<html>
  <head>
    <title>Hello</title>
  </head>
  <body>
    <p>Blah blah blah</p>
  </body>
</html>

(The following elements do not support the class attribute: html, head, base, meta, title, link, script, noscript, style.)

Though the actual namespacing functions are written in pure C with no dependencies, only Ruby bindings are available at this time.

Integrating in a Rails project

HTML namespacing is meant to integrate into a framework. Here is a Rails example:

In config/environment.rb:

config.gem 'html_namespacing'

In config/initializers/html_namespacing.rb:

HtmlNamespacing::Plugin::Rails.install

Now, all templates will have HTML namespacing applied. For instance, with a views/foos/show.html.erb like this:

<p>
  <b>Name:</b>
  <%=h @foo.name %>
</p>

<%= render(:partial => @foo) %>

<%= link_to 'Edit', edit_foo_path(@foo) %> |
<%= link_to 'Back', foos_path %>

The following HTML might be generated (depending on the _details partial and the data in the actual Foo object):

<p class="foos-show">
  <b>Name:</b>
  Foo
</p>

<p class="foos-_foo foos-show">
  <b>Description:</b>
  Bar
</p>

<a href="/foos/1/edit" class="hellos-show">Edit</a> |
<a href="/foos" class="hellos-show">Back</a>

Integrating with Haml and Sass in Rails

With Haml:haml-lang.com/ and Sass:sass-lang.com/ namespacing can be automated even further.

In your Rails project, implement the following:

In config/environment.rb:

config.gem 'haml', :version => '2.2.0'
config.gem 'adamh-html_namespacing', :library => 'html_namespacing'

In config/initializers/html_namespacing.rb:

HtmlNamespacing::Plugin::Rails.install
HtmlNamespacing::Plugin::Sass.install

Then add stylesheet_link_tag(:all, :recursive => true) to your layout.

Now, all templates will have HTML namespacing applied, and Sass files in SASS_DIR/views (where SASS_DIR is Sass’s :template_location, default public/stylesheets/sass) will also be HTML-namespaced. For example:

With a partial, app/views/foos/show.html.haml like this:

%p
  %strong Name:
  %span.name&= @foo.name

= render(:partial => @foo)

= link_to('Edit', edit_foo_path(@foo)
|
= link_to('Back', foos_path

And a Sass file in public/stylesheets/sass/views/foos/show.sass:

strong
  :font-size 1.3em
span.name
  :font-style italic
a&
  :font-weight bold

The Sass rules will only apply to their corresponding partial.

(Note: to target root-level elements in a Sass partial, use the “&” rule, which is standard Sass and will equate to “.NAMESPACE” in this context.)

Options

Available options to HtmlNamespacing::Plugin::Rails.install are:

:path_to_namespace_callback: Ruby lambda function which accepts a relative path (e.g., “foo/bar”) and returns an HTML namespacing string (e.g., “foo-bar”). The default is:

lambda { |path| path.gsub('/', '-') }

If the callback returns nil, HTML namespacing will not be applied.

:handle_exception_callback: Ruby lambda function which accepts an Exception, an ActionView::Template, and an ActionView::Base. The default behavior is:

lambda { |exception, template, view| raise(exception) }

If your :handle_exception_callback does not raise an exception, the template will be rendered as if HtmlNamespacing were not installed.

:javascript: If set, enable html_namespacing_javascript_tag() (see below). You will need to include HtmlNamespacing::Plugin::Rails::Helpers in your helpers to gain access to this method.

:javascript_root: Root of namespaced JavaScript files, if you are using html_namespacing_javascript_tag() (see below).

:template_formats: If set (to an Array), apply HTML namespacing under the given (String) formats. Default is ['html'].

:javascript_optional_suffix: optional suffix for your JavaScript files, if you are using html_namespacing_javascript_tag(). For instance, if :javascript_optional_suffix is 'compressed' and you rendered the partial app/views/foos/_foo.html.haml, then the (presumably minified) file app/javascripts/views/foos/_foo.js.compressed will be included if it exists (otherwise the standard app/javascripts/views/foos/_foo.js, otherwise nothing).

Available options to HtmlNamespacing::Plugin::Sass.install are:

:prefix (default views): subdirectory of Sass directory for which we want to namespace our Sass. :callback: See :template_to_namespace_callback above; this callback is similar, though it accepts a string path rather than an ActionView::Template.

Why?

HTML namespacing gives huge benefits when writing CSS and JavaScript: especially if the CSS and JavaScript components are automatically namespaced in a similar manner.

Imagine we have the following namespaced HTML:

<div class="foos-show">
  <p>In the show partial</p>
  <div class="foos-_details">
    <p>In the details partial</p>
    <div class="foos-_description">
      <p>In the description</p>
    </div>
  </div>
</div>

CSS Namespacing

We can set three CSS rules:

.foos-show p { font-size: 1.1em; }
.foos-_details p { font-weight: bold; }
.foos-_description p { font-style: italic; }

In such an example, the top paragraph would be large, the second would be large and bold, and the third would be large, bold, and italic.

The benefit comes when automating these namespaces. For instance, if the CSS rules were placed in stylesheets/views/foos/show.css, stylesheets/views/foos/_details.css, and stylesheets/views/foos/_description.css, respectively, and a preprocessor inserted the namespace before (or within) every rule in the file, all CSS namespacing would be done for free, so you would be able to write:

stylesheets/views/foos/show.css:

p { font-size: 1.1em; }

stylesheets/views/foos/_details.css:

p { font-weight: bold; }

stylesheets/views/foos/_description.css:

p { font-style: italic; }

Thus, the namespaces would never need to be explicitly mentioned. The framework used to generate such CSS is left (for now) as an exercise to the reader.

jQuery Namespacing

Use the following Rails helper, possibly in your default layout:

html_namespacing_javascript_tag(:jquery)

Afterwards, similar to our namespaced CSS framework above, you can easily namespace JavaScript behavior:

app/javascripts/views/foos/_description.js:

$NS().find('p').click(function() { alert("Yup, you clicked!"); });

You just write code specific to one Rails partial without actually writing the partial’s name anywhere, and you will not need to rewrite any selectors if you rename or move the partial (as long as you rename or move the corresponding JavaScript file).

(All namespaced JavaScript partials will be included inline, all in separate jQuery(document).ready() blocks. Two local variables will be available: NS, the namespace string, and $NS(), a function returning a jQuery object containing all elements at the root of the namespace.)

In a more complex setup, you may prefer to add another extension, like so:

html_namespacing_javascript_tag(:jquery, :format => :mobile)

This will find app/javascripts/views/foos/_description.mobile.js and will ignore app/javascripts/views/foos/_description.js. Neglecting the second parameter, html_namespacing_javascript_tag() would include _description.js and exclude _description.mobile.js: only single-extension JavaScript will be included. To include both, use:

html_namespacing_javascript_tag(:jquery, :format => [nil, :mobile])

Caching and JavaScript Namespacing

Caching throws a wrench into operations. html_namespacing_javascript_tag() works by watching which templates get rendered; when reading from the cache, not all templates will have render() called on them.

The general solution is to cache the list of rendered templates along with the data being cached. A new view method, html_rendering_rendered_paths, returns an Array to be used for this purpose. As a practical example, here is a plugin which makes JavaScript namespacing work with cache_advance:

class HtmlNamespacingCachePlugin
  def self.key_suffix
    'HTML_NAMESPACING_DATA'
  end

  def cache_it_options(view)
    { :view => view }
  end

  def before_render(cache_name, cache_key, options)
    options[:html_namespacing_rendered_paths_length] = paths(options).length
  end

  def after_render(cache_name, cache_key, data, options)
    old_length = options[:html_namespacing_rendered_paths_length]
    new_length = paths(options).length

    delta = paths(options)[old_length..new_length]
    unless delta.empty?
      key = cache_key + self.class.key_suffix
      Rails.cache.write(key, delta)
    end
  end

  def after_read(cache_name, cache_key, data, options)
    key = cache_key + self.class.key_suffix
    if delta = Rails.cache.read(key)
      paths(options).concat(delta)
    end
  end

  protected

  def paths(options)
    options[:view].html_namespacing_rendered_paths
  end
end

Other plugins are welcome; I will consider integrating them into this project.

Tips and Tricks

You may find that HTML namespacing works best when each HTML partial is wrapped in its own div tag. Both CSS’s child selectors and jQuery’s find() function will ordinarily ignore the element with the namespace class, only selecting sub-elements. For instance, with this HTML:

<div class="foos-show">
  <p>Hello</p>
</div>

If you wrote the following CSS:

.foos-show div { font-weight: bold; }

Or this Sass:

div
  :font-weight bold

Or the following JavaScript (following the $NS example above):

var $div = $NS().find('div');

Nothing will be selected, because the div element is not a child. Instead, you need the following CSS:

div.foos-show { font-weight: bold; }

Or this sass:

div&
  :font-weight bold

Or the following JavaScript:

var $div = $NS().filter('div');

Also, watch out for nesting: selectors with the foos-show namespace will match anything inside any partials rendered by foos/show.html.erb. As a rule of thumb to circumvent this problem: the wider the namespaces’s scope, the less CSS and JavaScript you should write which depends on it. (Use sub-partials very liberally.)

HTML namespacing produces plenty of tiny CSS and/or JavaScript files. Best practice is to bundle all namespaced files together: by virtue of being namespaced, it is safe to concatenate them. (Advanced CSS users should be thoughtful about cascading order and specificity and anybody bundling JavaScript files together should wrap each with (function() { ... })(); to prevent variable leakage.) CSS files can be concatenated using a tool such as {asset_library}[http://github.com/adamh/asset_library]; JavaScript files are concatenated automatically in html_namespacing_javascript_tag().

These and similar strategies should be considered when building an HTML namespacing framework.

License

I believe in software freedom, not any abomination thereof. This project is released under the Public Domain, meaning I relinquish my copyright (to any extend the law allows) and grant you all rights to use, modify, sell, or otherwise take advantage of my software.

However, I do kindly request that, as a favor, you refrain from using my software as part of an evil plan involving velociraptors and mind-controlling robots (even though I would not be legally entitled to sue you for doing so).