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.
gem install adamh-html_namespacing --source http://gems.github.com
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')
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.
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>
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.)
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
.
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>
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.
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 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.
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; JavaScript files are concatenated automatically in html_namespacing_javascript_tag()
.
These and similar strategies should be considered when building an HTML namespacing framework.
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).