Skip to content

Instantly share code, notes, and snippets.

@ChristianPeters
Created April 16, 2012 12:29
Show Gist options
  • Save ChristianPeters/2398394 to your computer and use it in GitHub Desktop.
Save ChristianPeters/2398394 to your computer and use it in GitHub Desktop.
Adapted for Rails Asset Pipeline: Split CSS files so that there are no more than a given number of selectors in one style sheet. This is a tool to cope with Internet Explorer's limitation of max. 4095 selectors per stylesheet.
#...
module MyProject
class Application < Rails::Application
config.assets.precompile += %w( ie6.css ie6_portion2.css ie7.css ie7_portion2.css ie8.css ie8_portion2.css ie9.css ie9_portion2.css)
#...
require 'css_splitter'
Rails.application.assets.register_engine '.split2', CssSplitter::SprocketsEngine
//= include 'ie8.css'
-#...
-# Rails Dev Boost compiles all templates that are included here regardless of browser requests
-# FIXME: Consider to remove Rails Dev Boost and this switch after updating to Rails 3.2
-# Switch testing_for to e.g. :ie7 when testing for ie7
- testing_for = nil
- if !Rails.env.development? || testing_for == :ie6
/[if IE 6]
= stylesheet_link_tag :ie6
= stylesheet_link_tag :ie6_portion2
- if !Rails.env.development? || testing_for == :ie7
/[if IE 7]
= stylesheet_link_tag :ie7
= stylesheet_link_tag :ie7_portion2
- if !Rails.env.development? || testing_for == :ie8
/[if IE 8]
= stylesheet_link_tag :ie8
= stylesheet_link_tag :ie8_portion2
- if !Rails.env.development? || [:ie6, :ie7, :ie8].include?(testing_for)
/[if lt IE 9]
= javascript_include_tag "//html5shim.googlecode.com/svn/trunk/html5.js"
= javascript_include_tag 'ie/selectivizr'
- if !Rails.env.development? || testing_for == :ie9
/[if IE 9]
= stylesheet_link_tag :ie9
= stylesheet_link_tag :ie9_portion2
require 'tilt'
module CssSplitter
class SprocketsEngine < Tilt::Template
def self.engine_initialized?
true
end
def prepare
end
def evaluate(scope, locals, &block)
part = scope.pathname.extname =~ /(\d+)$/ && $1 || 0
CssSplitter.split_string data, part.to_i
end
end
MAX_SELECTORS_DEFAULT = 4095
def self.split(infile, outdir = File.dirname(infile), max_selectors = MAX_SELECTORS_DEFAULT)
raise "infile could not be found" unless File.exists? infile
rules = IO.readlines(infile, "}")
return if rules.first.nil?
charset_statement, rules[0] = rules.first.partition(/^\@charset[^;]+;/)[1,2]
return if rules.nil?
file_id = 1 # The infile remains the first file
selectors_count = 0
output = nil
rules.each do |rule|
rule_selectors_count = count_selectors_of_rule rule
selectors_count += rule_selectors_count
# Nothing happens until the selectors limit is reached for the first time
if selectors_count > max_selectors
# Close current file if there is already one
output.close if output
# Prepare next file
file_id += 1
filename = File.join(outdir, File.basename(infile, File.extname(infile)) + "_#{file_id.to_s}" + File.extname(infile))
output = File.new(filename, "w")
output.write charset_statement
# Reset count with current rule count
selectors_count = rule_selectors_count
end
output.write rule if output
end
end
def self.split_string(css_string, part = 1, max_selectors = MAX_SELECTORS_DEFAULT)
rules = split_string_into_rules(css_string)
extract_part rules, part, max_selectors
end
def self.split_string_into_rules(css_string)
strip_comments(css_string).chomp.scan /[^}]*}/
end
def self.extract_part(rules, part = 1, max_selectors = MAX_SELECTORS_DEFAULT)
return if rules.first.nil?
charset_statement, rules[0] = rules.first.partition(/^\@charset[^;]+;/)[1,2]
return if rules.nil?
output = charset_statement
selectors_count = 0
selector_range = max_selectors * (part - 1) + 1 .. max_selectors * part
rules.each do |rule|
rule_selectors_count = count_selectors_of_rule rule
selectors_count += rule_selectors_count
if selector_range.cover? selectors_count
output << rule
elsif selectors_count > selector_range.end
break
end
end
output
end
def self.count_selectors(css_file)
raise "file could not be found" unless File.exists? css_file
rules = IO.readlines(css_file, '}')
return if rules.first.nil?
charset_statement, rules[0] = rules.first.partition(/^\@charset[^;]+;/)[1,2]
return if rules.first.nil?
rules.sum {|rule| count_selectors_of_rule(rule)}.tap do |result|
puts File.basename(css_file) + " contains #{result} selectors."
end
end
def self.count_selectors_of_rule(rule)
strip_comments(rule).partition(/\{/).first.scan(/,/).count.to_i + 1
end
private
def self.strip_comments(s)
s.gsub(/\/\/.*$/, "").gsub(/\/\*.*?\*\//, "")
end
end
@ChristianPeters
Copy link
Author

Okay, I think I have been missing an important piece.

it's just that assets:precompile task doesn't produce the secondary ie file.

That's right, the asset pipeline cannot produce extra files. It has to be there upfront.

Have a closer look at the CssSplitter::SprocketsEngine#evaluate:

    def evaluate(scope, locals, &block)
      part = scope.pathname.extname =~ /(\d+)$/ && $1 || 0
      CssSplitter.split_string data, part.to_i
    end

All it does is evaluating / manipulating an existing asset file. The workaround we came up with is providing the same file multiple times with the same content but based on the file extension different parts are extracted using the CssSplitter.

So we have a ie8_portion2.css.split2:

//= include 'ie8.css'

See complete asset folder structure

The regex matches the 2 of the file extension split2 and hands it on to the CssSplitter: CssSplitter.split_string(data, part.to_i)

The result is a compiled ie8_portion2.css - including only the second part > 4095 selectors.

You could add further files if your CSS file is bigger than 8190 selectors.

This mechanism works fine on a Rails 3.1 project with asset pipeline turned on and fully automated asset compilation during deployment. Let me know if this does not work for you.

@jhilden
Copy link

jhilden commented Sep 10, 2012

Hi @ChristianPeters,

pretty nifty workaround with the .splitx file endings.

Any plans to turn this into a gem?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment