Testing CSS @imports
I previously wrote a script to check files that are @imported exist in CSS stylesheets. I've turned that into a set of examples for our RSpec-based test suite. Fire the code into something like spec/views/stylesheets/import_spec.rb.
require File.dirname(__FILE__) + '/../../spec_helper'
describe "Stylesheet" do
stylesheet_root = File.expand_path(RAILS_ROOT + '/public')
stylesheets = Dir[File.join(stylesheet_root, "**", "*.css")]
stylesheets.each do |stylesheet|
describe stylesheet do
it "should not @import files that don't exist" do
missing_imports = []
imports = File.read(stylesheet).split(/\n|\r/).grep(/\@import url\((.*)\)/)
imports.each do |import|
desired_path = import.scan(/url\((["'\ ])?(.*)\1\)/).to_a.first.to_a.last
desired_root = desired_path[0,1] == "/" ? stylesheet_root : File.dirname(stylesheet)
filesystem_path = File.expand_path(File.join(desired_root, desired_path))
if !File.exists?(filesystem_path)
missing_imports << { :path => filesystem_path, :directive => import }
end
end
if missing_imports.any?
exception = []
missing_imports.each do |import|
exception << "Missing @import file (#{import[:path]}) required for #{import[:directive]}"
end
raise exception.join("\n")
end
end
end
end
end
Content_for is the new GOTO
I don't like content_for. Your view code jumps around up and down files and makes it hard to work out what's going on. It smells a lot like GOTO. When was the last time you saw someone recommend you use a GOTO?
content_for :javascript and content_for :css
Use of content_for can be easily avoided, at least for including CSS and Javascript files. Include the controller name and action name in the body tag in your layout and properly qualify your CSS declarations.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><%= page_title %></title>
<meta http-equiv="Content-Language" content="English" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" href="/stylesheets/simple.css" media="screen" />
</head>
<body id="<%= "#{controller.controller_name.tableize.singularize}_#{controller.action_name}" %> class="<%= "#{controller.controller_name.tableize.singularize} #{controller.action_name}" %>">
<%= yield %>
</body>
</html>
Say you were looking at the Posts views in your app, you can now style these using something like this.
.post.index .article .title {
font-size: 1.25em;
}
.post.show .article .title {
font-size: 0.9em;
}
Or, in case you need to support browsers that don't let you specify two classes as a selector for a single element, you can write it like this.
#post_index .article .title {
font-size: 1.25em;
}
#post_show .article .title {
font-size: 0.9em;
}
Since all your Javascript is unobtrusive anyway (right?), it should be pretty easy to qualify the selectors used there with the same CSS selectors shown above.
As an added bonus, by specifying your Javascript / CSS like the above you can package it all in one Javascript or CSS file on deployment to your production environment and save yourself a bunch of HTTP requests.
Make sure you're @importing files that exist
A few people near me have heard the start of my rumblings about optimising the number of HTTP requests required for each page. There are a variety of reasons that you might want to do this, but that discussion is for another post. For now simply assume that I don't like unnecessary HTTP requests. Extrapolating from that you would reasonably assume that I really don't like wasted HTTP requests such as might happen when the url in a CSS @import directive 404's. If you assumed that, you'd be right.
I got fed up of tracking down the source of these errors in a few applications that I maintain so I fired up a TextMate session and scraped together this nasty little snippet of Ruby to do my dirty work for me.
#! /usr/bin/env ruby
css_root = File.expand_path(`pwd`.strip)
css_files = Dir[File.join(css_root, "**", "*.css")]
missing_imports = Hash.new([])
css_files.each_with_index do |css_file, index|
imports = File.read(css_file).split(/\n|\r/).grep(/\@import url\((.*)\)/)
imports.each do |import|
desired_path = import.scan(/url\((["'\ ])?(.*)\1\)/).to_a.first.to_a.last
desired_root = desired_path[0,1] == "/" ? css_root : File.dirname(css_file)
filesystem_path = File.expand_path(File.join(desired_root, desired_path))
if !File.exists?(filesystem_path)
missing_imports[css_file] += [{ :path => filesystem_path, :directive => import }]
end
end
end
if missing_imports.any?
puts "Missing files declared as imports in CSS:\n\n"
missing_imports.keys.each do |origin|
puts "Origin: #{origin}"
missing_imports[origin].each do |import|
puts "Missing @import file: #{import[:path]}"
puts "Directive: #{import[:directive]}"
end
puts ""
end
else
puts "No imported files are missing. Well done."
end
Run it in the directory which serves your document root - for Rails applications this would be RAILS_ROOT/public/ - and it'll either spit out a list of missing files you've tried to @import or tell you that you've done well.
Missing files declared as imports in CSS:
Origin: /Users/craig/projects/1.8/public/stylesheets/.../find_by_service.css
Missing @import file: /Users/craig/projects/1.8/public/stylesheets/.../a_to_z.css
Directive: @import url('.../a_to_z.css');
Now, just to be clear, I don't like @import directives. I'd prefer they were completely removed from the CSS. They are popular with a lot of people though so I'll compromise for the moment: if you must use them, please make sure they're going to work.
