module CssSplitter class Splitter MAX_SELECTORS_DEFAULT = 4095 # returns the specified split of the passed css_string def self.split_string(css_string, split = 1, max_selectors = MAX_SELECTORS_DEFAULT) rules = split_string_into_rules(css_string) extract_part rules, split, max_selectors end # splits string into array of rules (also strips comments) def self.split_string_into_rules(css_string) partial_rules = strip_comments(css_string).chomp.scan /[^}]*}/ whole_rules = [] bracket_balance = 0 in_media_query = false partial_rules.each do |rule| if rule =~ /^@media/ in_media_query = true elsif bracket_balance == 0 in_media_query = false end if bracket_balance == 0 || in_media_query whole_rules << rule else whole_rules.last << rule end bracket_balance += get_rule_bracket_balance rule end whole_rules end # extracts the specified part of an overlong CSS string def self.extract_part(rules, part = 1, max_selectors = MAX_SELECTORS_DEFAULT) return if rules.first.nil? charset_statement, rules[0] = extract_charset(rules.first) return if rules.nil? output = charset_statement || "" selectors_count = 0 selector_range = max_selectors * (part - 1) + 1 .. max_selectors * part # e.g (4096..8190) current_media = nil selectors_in_media = 0 first_hit = true rules.each do |rule| media_part = extract_media(rule) if media_part current_media = media_part selectors_in_media = 0 end rule_selectors_count = count_selectors_of_rule rule selectors_count += rule_selectors_count if rule =~ /\A\s*}\z$/ current_media = nil # skip the line if the close bracket is the first rule for the new file next if first_hit end if selector_range.cover? selectors_count # add rule to current output if within selector_range if media_part output << media_part elsif first_hit && current_media output << current_media end selectors_in_media += rule_selectors_count if current_media.present? output << rule first_hit = false elsif selectors_count > selector_range.end # stop writing to output break end end if current_media.present? and selectors_in_media > 0 output << '}' end output end # count selectors of one individual CSS rule def self.count_selectors_of_rule(rule) parts = strip_comments(rule).partition(/\{/) parts.second.empty? ? 0 : parts.first.scan(/,/).count.to_i + 1 end # count selectors of a CSS stylesheet (not used by SprocketsEngine) def self.count_selectors(css_file) raise "file could not be found" unless File.exists? css_file rules = split_string_into_rules(File.read css_file) return if rules.first.nil? rules.sum{ |rule| count_selectors_of_rule(rule) } end private def self.extract_media(rule) if rule.sub!(/^\s*(@media[^{]*{)([^{}]*{[^}]*})$/) { $2 } $1 end end # extracts potential charset declaration from the first rule def self.extract_charset(rule) if rule.include?('charset') rule.partition(/^\@charset[^;]+;/)[1,2] else [nil, rule] end end def self.strip_comments(s) s.gsub(/\/\*.*?\*\//m, "") end def self.get_rule_bracket_balance ( rule ) rule.scan( /}/ ).size - rule.scan( /{/ ).size end end end