class CssParser::Parser

Parser class

All CSS is converted to UTF-8.

When calling Parser#new there are some configuaration options:

absolute_paths

Convert relative paths to absolute paths (href, src and url(''). Boolean, default is false.

import

Follow @import rules. Boolean, default is true.

io_exceptions

Throw an exception if a link can not be found. Boolean, default is true.

Constants

MAX_REDIRECTS
RE_AT_IMPORT_RULE

Initial parsing

STRIP_CSS_COMMENTS_RX
STRIP_HTML_COMMENTS_RX
USER_AGENT

Attributes

folded_declaration_cache[R]
loaded_uris[R]

Array of CSS files that have been loaded.

Public Class Methods

new(options = {}) click to toggle source
# File lib/css_parser/parser.rb, line 37
def initialize(options = {})
  @options = {:absolute_paths => false,
              :import => true,
              :io_exceptions => true,
              :capture_offsets => false}.merge(options)

  # array of RuleSets
  @rules = []

  @redirect_count = nil

  @loaded_uris = []

  # unprocessed blocks of CSS
  @blocks = []
  reset!
end

Public Instance Methods

[](selector, media_types = :all)
Alias for: find_by_selector
add_block!(block, options = {}) click to toggle source

Add a raw block of CSS.

In order to follow +@import+ rules you must supply either a :base_dir or :base_uri option.

Use the :media_types option to set the media type(s) for this block. Takes an array of symbols.

Use the :only_media_types option to selectively follow +@import+ rules. Takes an array of symbols.

Example

css = <<-EOT
  body { font-size: 10pt }
  p { margin: 0px; }
  @media screen, print {
    body { line-height: 1.2 }
  }
EOT

parser = CssParser::Parser.new
parser.add_block!(css)
# File lib/css_parser/parser.rb, line 117
def add_block!(block, options = {})
  options = {:base_uri => nil, :base_dir => nil, :charset => nil, :media_types => :all, :only_media_types => :all}.merge(options)
  options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}
  options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}

  block = cleanup_block(block, options)

  if options[:base_uri] and @options[:absolute_paths]
    block = CssParser.convert_uris(block, options[:base_uri])
  end

  # Load @imported CSS
  if @options[:import]
    block.scan(RE_AT_IMPORT_RULE).each do |import_rule|
      media_types = []
      if media_string = import_rule[-1]
        media_string.split(/[,]/).each do |t|
          media_types << CssParser.sanitize_media_query(t) unless t.empty?
        end
      else
        media_types = [:all]
      end

      next unless options[:only_media_types].include?(:all) or media_types.length < 1 or (media_types & options[:only_media_types]).length > 0

      import_path = import_rule[0].to_s.gsub(/['"]*/, '').strip

      import_options = { :media_types => media_types }
      import_options[:capture_offsets] = true if options[:capture_offsets]

      if options[:base_uri]
        import_uri = Addressable::URI.parse(options[:base_uri].to_s) + Addressable::URI.parse(import_path)
        import_options[:base_uri] = options[:base_uri]
        load_uri!(import_uri, import_options)
      elsif options[:base_dir]
        import_options[:base_dir] = options[:base_dir]
        load_file!(import_path, import_options)
      end
    end
  end

  # Remove @import declarations
  block = ignore_pattern(block, RE_AT_IMPORT_RULE, options)

  parse_block_into_rule_sets!(block, options)
end
add_rule!(selectors, declarations, media_types = :all) click to toggle source

Add a CSS rule by setting the selectors, declarations and media_types.

media_types can be a symbol or an array of symbols.

# File lib/css_parser/parser.rb, line 167
def add_rule!(selectors, declarations, media_types = :all)
  rule_set = RuleSet.new(selectors, declarations)
  add_rule_set!(rule_set, media_types)
end
add_rule_set!(ruleset, media_types = :all) click to toggle source

Add a CssParser RuleSet object.

media_types can be a symbol or an array of symbols.

# File lib/css_parser/parser.rb, line 185
def add_rule_set!(ruleset, media_types = :all)
  raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)

  media_types = [media_types] unless Array === media_types
  media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt)}

  @rules << {:media_types => media_types, :rules => ruleset}
end
add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all) click to toggle source

Add a CSS rule by setting the selectors, declarations, filename, offset and media_types.

filename can be a string or uri pointing to the file or url location. offset should be Range object representing the start and end byte locations where the rule was found in the file. media_types can be a symbol or an array of symbols.

# File lib/css_parser/parser.rb, line 177
def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)
  rule_set = OffsetAwareRuleSet.new(filename, offset, selectors, declarations)
  add_rule_set!(rule_set, media_types)
end
each_rule_set(media_types = :all) { |rule_set, media_types| ... } click to toggle source

Iterate through RuleSet objects.

media_types can be a symbol or an array of symbols.

# File lib/css_parser/parser.rb, line 210
def each_rule_set(media_types = :all) # :yields: rule_set, media_types
  media_types = [:all] if media_types.nil?
  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}

  @rules.each do |block|
    if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }
      yield(block[:rules], block[:media_types])
    end
  end
end
each_selector(all_media_types = :all, options = {}) { |selectors, declarations, specificity, media_types| ... } click to toggle source

Iterate through CSS selectors.

media_types can be a symbol or an array of symbols. See RuleSet#each_selector for options.

# File lib/css_parser/parser.rb, line 246
def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
  return to_enum(:each_selector) unless block_given?

  each_rule_set(all_media_types) do |rule_set, media_types|
    rule_set.each_selector(options) do |selectors, declarations, specificity|
      yield selectors, declarations, specificity, media_types
    end
  end
end
find_by_selector(selector, media_types = :all) click to toggle source

Get declarations by selector.

media_types are optional, and can be a symbol or an array of symbols. The default value is :all.

Examples

find_by_selector('#content')
=> 'font-size: 13px; line-height: 1.2;'

find_by_selector('#content', [:screen, :handheld])
=> 'font-size: 13px; line-height: 1.2;'

find_by_selector('#content', :print)
=> 'font-size: 11pt; line-height: 1.2;'

Returns an array of declarations.

# File lib/css_parser/parser.rb, line 71
def find_by_selector(selector, media_types = :all)
  out = []
  each_selector(media_types) do |sel, dec, spec|
    out << dec if sel.strip == selector.strip
  end
  out
end
Also aliased as: []
find_rule_sets(selectors, media_types = :all) click to toggle source

Finds the rule sets that match the given selectors

# File lib/css_parser/parser.rb, line 81
def find_rule_sets(selectors, media_types = :all)
  rule_sets = []

  selectors.each do |selector|
    selector.gsub!(/\s+/, ' ')
    selector.strip!
    each_rule_set(media_types) do |rule_set, media_type|
      if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector)
        rule_sets << rule_set
      end
    end
  end

  rule_sets
end
load_file!(file_name, options = {}, deprecated = nil) click to toggle source

Load a local CSS file.

# File lib/css_parser/parser.rb, line 466
def load_file!(file_name, options = {}, deprecated = nil)
  opts = {:base_dir => nil, :media_types => :all}

  if options.is_a? Hash
    opts.merge!(options)
  else
    opts[:base_dir] = options if options.is_a? String
    opts[:media_types] = deprecated if deprecated
  end

  file_name = File.expand_path(file_name, opts[:base_dir])
  return unless File.readable?(file_name)
  return unless circular_reference_check(file_name)

  src = IO.read(file_name)

  opts[:filename] = file_name if opts[:capture_offsets]
  opts[:base_dir] = File.dirname(file_name)

  add_block!(src, opts)
end
load_string!(src, options = {}, deprecated = nil) click to toggle source

Load a local CSS string.

# File lib/css_parser/parser.rb, line 489
def load_string!(src, options = {}, deprecated = nil)
  opts = {:base_dir => nil, :media_types => :all}

  if options.is_a? Hash
    opts.merge!(options)
  else
    opts[:base_dir] = options if options.is_a? String
    opts[:media_types] = deprecated if deprecated
  end

  add_block!(src, opts)
end
load_uri!(uri, options = {}, deprecated = nil) click to toggle source

Load a remote CSS file.

You can also pass in file://test.css

See add_block! for options.

Deprecated: originally accepted three params: `uri`, `base_uri` and `media_types`

# File lib/css_parser/parser.rb, line 437
def load_uri!(uri, options = {}, deprecated = nil)
  uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme

  opts = {:base_uri => nil, :media_types => :all}

  if options.is_a? Hash
    opts.merge!(options)
  else
    opts[:base_uri] = options if options.is_a? String
    opts[:media_types] = deprecated if deprecated
  end

  if uri.scheme == 'file' or uri.scheme.nil?
    uri.path = File.expand_path(uri.path)
    uri.scheme = 'file'
  end

  opts[:base_uri] = uri if opts[:base_uri].nil?

  # pass on the uri if we are capturing file offsets
  opts[:filename] = uri.to_s if opts[:capture_offsets]

  src, = read_remote_file(uri) # skip charset
  if src
    add_block!(src, opts)
  end
end
remove_rule_set!(ruleset, media_types = :all) click to toggle source

Remove a CssParser RuleSet object.

media_types can be a symbol or an array of symbols.

# File lib/css_parser/parser.rb, line 197
def remove_rule_set!(ruleset, media_types = :all)
  raise ArgumentError unless ruleset.kind_of?(CssParser::RuleSet)

  media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt)}

  @rules.reject! do |rule|
    rule[:media_types] == media_types && rule[:rules].to_s == ruleset.to_s
  end
end
rules_by_media_query() click to toggle source

A hash of { :media_query => rule_sets }

# File lib/css_parser/parser.rb, line 286
def rules_by_media_query
  rules_by_media = {}
  @rules.each do |block|
    block[:media_types].each do |mt|
      unless rules_by_media.has_key?(mt)
        rules_by_media[mt] = []
      end
      rules_by_media[mt] << block[:rules]
    end
  end

  rules_by_media
end
to_h(which_media = :all) click to toggle source

Output all CSS rules as a Hash

# File lib/css_parser/parser.rb, line 222
def to_h(which_media = :all)
  out = {}
  styles_by_media_types = {}
  each_selector(which_media) do |selectors, declarations, specificity, media_types|
    media_types.each do |media_type|
      styles_by_media_types[media_type] ||= []
      styles_by_media_types[media_type] << [selectors, declarations]
    end
  end

  styles_by_media_types.each_pair do |media_type, media_styles|
    ms = {}
    media_styles.each do |media_style|
      ms = css_node_to_h(ms, media_style[0], media_style[1])
    end
    out[media_type.to_s] = ms
  end
  out
end
to_s(which_media = :all) click to toggle source

Output all CSS rules as a single stylesheet.

# File lib/css_parser/parser.rb, line 257
def to_s(which_media = :all)
  out = String.new
  styles_by_media_types = {}
  each_selector(which_media) do |selectors, declarations, specificity, media_types|
    media_types.each do |media_type|
      styles_by_media_types[media_type] ||= []
      styles_by_media_types[media_type] << [selectors, declarations]
    end
  end

  styles_by_media_types.each_pair do |media_type, media_styles|
    media_block = (media_type != :all)
    out << "@media #{media_type} {\n" if media_block

    media_styles.each do |media_style|
      if media_block
        out << "  #{media_style[0]} {\n    #{media_style[1]}\n  }\n"
      else
        out << "#{media_style[0]} {\n#{media_style[1]}\n}\n"
      end
    end

    out << "}\n" if media_block
  end

  out
end

Protected Instance Methods

circular_reference_check(path) click to toggle source

Check that a path hasn't been loaded already

Raises a CircularReferenceError exception if io_exceptions are on, otherwise returns true/false.

# File lib/css_parser/parser.rb, line 509
def circular_reference_check(path)
  path = path.to_s
  if @loaded_uris.include?(path)
    raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
    return false
  else
    @loaded_uris << path
    return true
  end
end
ignore_pattern(css, regex, options) click to toggle source

Remove a pattern from a given string

Returns a string.

# File lib/css_parser/parser.rb, line 523
def ignore_pattern(css, regex, options)
  # if we are capturing file offsets, replace the characters with spaces to retail the original positions
  return css.gsub(regex) { |m| ' ' * m.length } if options[:capture_offsets]

  # otherwise just strip it out
  css.gsub(regex, '')
end

Private Instance Methods

css_node_to_h(hash, key, val) click to toggle source

recurse through nested nodes and return them as Hashes nested in passed hash

# File lib/css_parser/parser.rb, line 658
def css_node_to_h(hash, key, val)
  hash[key.strip] = '' and return hash if val.nil?
  lines = val.split(';')
  nodes = {}
  lines.each do |line|
    parts = line.split(':', 2)
    if (parts[1] =~ /:/)
      nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])
    else
      nodes[parts[0].to_s.strip] =parts[1].to_s.strip
    end
  end
  hash[key.strip] = nodes
  hash
end