class CssParser::Parser

allow_file_uris

Permit file:// URIs via load_uri!. Boolean, default is false. When false (the default), a caller that passes a file:// URI to load_uri! — directly or via a CSS @import resolved against a file:// base_uri — is refused, closing the local-file-disclosure vector when the URI is influenced by user input. load_file! is unaffected: it is the explicit local-file API and takes a caller-supplied path. Independent of allow_local_network.
[allow_local_network] Permit http(s) fetches against loopback / private / link-local / cloud-metadata addresses. Boolean, default is false. When false (the default), outbound HTTP requests are routed through ssrf_filter, which resolves the host and rejects unsafe IP ranges. Set to true only when the destination is known to be safe (e.g. local fixture servers in tests). Independent of allow_file_uris.
[io_exceptions] Throw an exception if a link can not be found. Boolean, default is true.
[import] Follow @import rules. Boolean, default is true.
[absolute_paths] Convert relative paths to absolute paths (href, src and url(''). Boolean, default is false.
When calling Parser#new there are some configuaration options:
All CSS is converted to UTF-8.
== Parser class

def add_block!(block, options = {})

parser.add_block!(css)
parser = CssParser::Parser.new

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

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

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

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

Add a raw block of CSS.
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.empty? or media_types.intersect?(options[:only_media_types])
      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

def add_rule!(*args, selectors: nil, block: nil, filename: nil, offset: nil, media_types: :all) # rubocop:disable Metrics/ParameterLists

rubocop:disable Metrics/ParameterLists
+offset+ should be Range object representing the start and end byte locations where the rule was found in the file.
+filename+ can be a string or uri pointing to the file or url location.
optional fields for source location for source location
+media_types+ can be a symbol or an array of symbols. default to :all

reference too.
and +media_types+. Optional pass +filename+ , +offset+ for source
Add a CSS rule by setting the +selectors+, +declarations+
def add_rule!(*args, selectors: nil, block: nil, filename: nil, offset: nil, media_types: :all) # rubocop:disable Metrics/ParameterLists
  if args.any?
    media_types = nil
    if selectors || block || filename || offset || media_types
      raise ArgumentError, "don't mix positional and keyword arguments arguments"
    end
    warn '[DEPRECATION] `add_rule!` with positional arguments is deprecated. ' \
         'Please use keyword arguments instead.', uplevel: 1
    case args.length
    when 2
      selectors, block = args
    when 3
      selectors, block, media_types = args
    else
      raise ArgumentError
    end
  end
  begin
    rule_set = RuleSet.new(
      selectors: selectors, block: block,
      offset: offset, filename: filename
    )
    add_rule_set!(rule_set, media_types)
  rescue ArgumentError => e
    raise e if @options[:rule_set_exceptions]
  end
end

def add_rule_set!(ruleset, media_types = :all)

+media_types+ can be a symbol or an array of symbols.

Add a CssParser RuleSet object.
def add_rule_set!(ruleset, media_types = :all)
  raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)
  media_types = [media_types] unless media_types.is_a?(Array)
  media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt) }
  @rules << {media_types: media_types, rules: ruleset}
end

def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)

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

Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.
def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)
  warn '[DEPRECATION] `add_rule_with_offsets!` is deprecated. Please use `add_rule!` instead.', uplevel: 1
  add_rule!(
    selectors: selectors, block: declarations, media_types: media_types,
    filename: filename, offset: offset
  )
end

def circular_reference_check(path) # rubocop:disable Naming/PredicateMethod

rubocop:disable Naming/PredicateMethod
TODO: fix rubocop
otherwise returns true/false.
Raises a CircularReferenceError exception if io_exceptions are on,

Check that a path hasn't been loaded already
def circular_reference_check(path) # rubocop:disable Naming/PredicateMethod
  path = path.to_s
  if @loaded_uris.include?(path)
    raise CircularReferenceError, "can't load #{path} more than once" if @options[:io_exceptions]
    false
  else
    @loaded_uris << path
    true
  end
end

def cleanup_block(block, options = {}) # :nodoc:

:nodoc:
Returns a string.

Strip comments and clean up blank lines from a block of CSS.
def cleanup_block(block, options = {}) # :nodoc:
  # Strip CSS comments
  utf8_block = block.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: ' ')
  utf8_block = ignore_pattern(utf8_block, STRIP_CSS_COMMENTS_RX, options)
  # Strip HTML comments - they shouldn't really be in here but
  # some people are just crazy...
  utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options)
  # Strip lines containing just whitespace
  utf8_block.gsub!(/^\s+$/, '') unless options[:capture_offsets]
  utf8_block
end

def compact! # :nodoc:

:nodoc:
Merge declarations with the same selector.
def compact! # :nodoc:
  []
end

def css_node_to_h(hash, key, val)

passed hash
recurse through nested nodes and return them as Hashes nested in
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].include?(':')
      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

def each_rule_set(media_types = :all) # :yields: rule_set, media_types

:yields: rule_set, media_types
+media_types+ can be a symbol or an array of symbols.

Iterate through RuleSet objects.
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

def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types

:yields: selectors, declarations, specificity, media_types
See RuleSet#each_selector for +options+.
+media_types+ can be a symbol or an array of symbols.

Iterate through CSS selectors.
def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types
  return to_enum(__method__, all_media_types, options) 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

def fetch_via_net_http(uri, redirect_count = 0) # :nodoc:

:nodoc:
cannot be followed even on this opt-in code path.
the URI scheme on every redirect hop so a `Location: file://...`
Net::HTTP path used only when `allow_local_network: true`. Validates
def fetch_via_net_http(uri, redirect_count = 0) # :nodoc:
  # Internal invariant: this method is the implementation of the
  # `allow_local_network: true` branch of `read_remote_file`. If it
  # is ever reached without that flag set, a future change has
  # bypassed the SSRF gate; refuse to fetch rather than silently
  # connect. The recursive call on a redirect inherits this guard
  # because the option does not change mid-request.
  unless @options[:allow_local_network]
    raise "BUG: #{self.class}##{__method__} reached with " \
          'allow_local_network=false (SSRF gate bypassed)'
  end
  raise RemoteFileError, uri.to_s unless REMOTE_ALLOWED_SCHEMES.include?(uri.scheme)
  raise RemoteFileError, uri.to_s if redirect_count > MAX_REDIRECTS
  http = Net::HTTP.new(uri.host, uri.port || uri.default_port)
  http.use_ssl = (uri.scheme == 'https')
  res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent]})
  if res.code.to_i >= 300 && res.code.to_i < 400 && res['Location']
    redirect_uri = Addressable::URI.parse(Addressable::URI.escape(res['Location']))
    return fetch_via_net_http(redirect_uri, redirect_count + 1)
  end
  res
end

def find_by_selector(selector, media_types = :all)

Returns an array of declarations.

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

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

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

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

Get declarations by selector.
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

def find_rule_sets(selectors, media_types = :all)

Finds the rule sets that match the given selectors
def find_rule_sets(selectors, media_types = :all)
  rule_sets = []
  selectors.each do |selector|
    selector = selector.gsub(/\s+/, ' ').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

def get_folded_declaration(block_hash) # :nodoc:

:nodoc:
Retrieve a folded declaration block from the internal cache.
def get_folded_declaration(block_hash) # :nodoc:
  @folded_declaration_cache[block_hash] ||= nil
end

def ignore_pattern(css, regex, options)

Returns a string.

Remove a pattern from a given string
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

def initialize(options = {})

def initialize(options = {})
  @options = {
    absolute_paths: false,
    import: true,
    io_exceptions: true,
    rule_set_exceptions: true,
    capture_offsets: false,
    user_agent: USER_AGENT,
    allow_local_network: false,
    allow_file_uris: false
  }.merge(options)
  # array of RuleSets
  @rules = []
  @loaded_uris = []
  # unprocessed blocks of CSS
  @blocks = []
  reset!
end

def load_file!(file_name, options = {}, deprecated = nil)

Load a local CSS file.
def load_file!(file_name, options = {}, deprecated = nil)
  opts = {base_dir: nil, media_types: :all}
  if options.is_a? Hash
    opts.merge!(options)
  else
    warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
         'Please use keyword arguments instead.', uplevel: 1
    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 = File.read(file_name)
  opts[:filename] = file_name if opts[:capture_offsets]
  opts[:base_dir] = File.dirname(file_name)
  add_block!(src, opts)
end

def load_string!(src, options = {}, deprecated = nil)

Load a local CSS string.
def load_string!(src, options = {}, deprecated = nil)
  opts = {base_dir: nil, media_types: :all}
  if options.is_a? Hash
    opts.merge!(options)
  else
    warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \
         'Please use keyword arguments instead.', uplevel: 1
    opts[:base_dir] = options if options.is_a? String
    opts[:media_types] = deprecated if deprecated
  end
  add_block!(src, opts)
end

def load_uri!(uri, options = {}, deprecated = nil)

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

See add_block! for options.

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

Load a remote CSS file.
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
    warn '[DEPRECATION] `load_uri!` with positional arguments is deprecated. ' \
         'Please use keyword arguments instead.', uplevel: 1
    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]
  # file:// is handled here, not inside read_remote_file. The
  # remote-read path must never service file:// URIs, so a 3xx
  # `Location: file://...` redirect cannot be turned into a local
  # File.read.
  #
  # file:// via `load_uri!` is also gated by `allow_file_uris`:
  # an attacker who can influence a URI passed here (e.g. via a CSS
  # @import resolved against an attacker-controlled base_uri) could
  # otherwise turn it into arbitrary local file disclosure. Callers
  # that legitimately need to load local files should use
  # `load_file!` (the explicit local-file API).
  src = if uri.scheme == 'file'
          unless @options[:allow_file_uris]
            raise RemoteFileError, uri.to_s if @options[:io_exceptions]
            return
          end
          read_local_file(uri)
        else
          src_and_charset, = read_remote_file(uri) # skip charset
          src_and_charset
        end
  add_block!(src, opts) if src
end

def parse_block_into_rule_sets!(block, options = {}) # :nodoc:

:nodoc:
def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
  current_media_queries = [:all]
  if options[:media_types]
    current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }
  end
  in_declarations = 0
  block_depth = 0
  in_charset = false # @charset is ignored for now
  in_string = false
  in_at_media_rule = false
  in_media_block = false
  current_selectors = +''
  current_media_query = +''
  current_declarations = +''
  # once we are in a rule, we will use this to store where we started if we are capturing offsets
  rule_start = nil
  start_offset = nil
  end_offset = nil
  scanner = StringScanner.new(block)
  until scanner.eos?
    # save the regex offset so that we know where in the file we are
    start_offset = scanner.pos
    token = scanner.scan(RULESET_TOKENIZER_RX)
    end_offset = scanner.pos
    if token.start_with?('"') # found un-escaped double quote
      in_string = !in_string
    end
    if in_declarations > 0
      # too deep, malformed declaration block
      if in_declarations > 1
        in_declarations -= 1 if token.include?('}')
        next
      end
      if !in_string && token.include?('{')
        in_declarations += 1
        next
      end
      current_declarations << token
      if !in_string && token.include?('}')
        current_declarations.gsub!(/\}\s*$/, '')
        in_declarations -= 1
        current_declarations.strip!
        unless current_declarations.empty?
          add_rule_options = {
            selectors: current_selectors, block: current_declarations,
            media_types: current_media_queries
          }
          if options[:capture_offsets]
            add_rule_options[:filename] = options[:filename]
            add_rule_options[:offset] = rule_start..end_offset
          end
          add_rule!(**add_rule_options)
        end
        current_selectors = +''
        current_declarations = +''
        # restart our search for selectors and declarations
        rule_start = nil if options[:capture_offsets]
      end
    elsif /@media/i.match?(token)
      # found '@media', reset current media_types
      in_at_media_rule = true
      current_media_queries = []
    elsif in_at_media_rule
      if token.include?('{')
        block_depth += 1
        in_at_media_rule = false
        in_media_block = true
        current_media_queries << CssParser.sanitize_media_query(current_media_query)
        current_media_query = +''
      elsif token.include?(',')
        # new media query begins
        token.tr!(',', ' ')
        token.strip!
        current_media_query << token << ' '
        current_media_queries << CssParser.sanitize_media_query(current_media_query)
        current_media_query = +''
      else
        token.strip!
        # special-case the ( and ) tokens to remove inner-whitespace
        # (eg we'd prefer '(width: 500px)' to '( width: 500px )' )
        case token
        when '('
          current_media_query << token
        when ')'
          current_media_query.sub!(/ ?$/, token)
        else
          current_media_query << token << ' '
        end
      end
    elsif in_charset or /@charset/i.match?(token)
      # iterate until we are out of the charset declaration
      in_charset = !token.include?(';')
    elsif !in_string && token.include?('}')
      block_depth -= 1
      # reset the current media query scope
      if in_media_block
        current_media_queries = [:all]
        in_media_block = false
      end
    elsif !in_string && token.include?('{')
      current_selectors.strip!
      in_declarations += 1
    else
      # if we are in a selector, add the token to the current selectors
      current_selectors << token
      # mark this as the beginning of the selector unless we have already marked it
      rule_start = start_offset if options[:capture_offsets] && rule_start.nil? && /^[^\s]+$/.match?(token)
    end
  end
  # check for unclosed braces
  return unless in_declarations > 0
  add_rule_options = {
    selectors: current_selectors, block: current_declarations,
    media_types: current_media_queries
  }
  if options[:capture_offsets]
    add_rule_options[:filename] = options[:filename]
    add_rule_options[:offset] = rule_start..end_offset
  end
  add_rule!(**add_rule_options)
end

def read_local_file(uri) # :nodoc:

:nodoc:
branch (GHSA-9pmc-p236-855h).
from the remote read path — so an HTTP redirect cannot reach this
Read a local file:// URI. Called only from `load_uri!` — never
def read_local_file(uri) # :nodoc:
  # Internal invariant: this method is the implementation of the
  # `allow_file_uris: true` branch of `load_uri!`. If it is ever
  # reached without that flag set, a future change has bypassed the
  # LFI gate; refuse to read rather than silently leak.
  unless @options[:allow_file_uris]
    raise "BUG: #{self.class}##{__method__} reached with " \
          'allow_file_uris=false (LFI gate bypassed)'
  end
  return nil unless circular_reference_check(uri.to_s)
  path = uri.path
  path.gsub!(%r{^/}, '') if Gem.win_platform?
  File.read(path, mode: 'rb')
rescue
  raise RemoteFileError, uri.to_s if @options[:io_exceptions]
  nil
end

def read_remote_file(uri) # :nodoc:

:nodoc:
++
TODO: add option to fail silently or throw and exception on a 404
--
remains closed even on this opt-in path.
redirect to `file://` (the original GHSA-9pmc-p236-855h sink)
is still validated on every redirect hop, so cross-scheme
check is bypassed and plain `Net::HTTP` is used — but the scheme
When `allow_local_network: true` is set on the Parser, the SSRF

- re-validates scheme and IP on every redirect hop.
via literal IPs and via CNAME / attacker-controlled A records);
other range typically used for internal services (defeats SSRF
resolved IP is loopback, RFC-1918, link-local, multicast, or any
- resolves the hostname with `Resolv` and rejects requests whose
`file://` / `gopher://` / `dict://` etc.);
- rejects any scheme other than http/https (defeats redirect-to-
`SsrfFilter.get`, which:
In the default (secure) configuration, requests are issued via

Returns the file's data and character set in an array.

Download a remote http(s) file into a string.
def read_remote_file(uri) # :nodoc:
  uri = Addressable::URI.parse(uri.to_s)
  unless circular_reference_check(uri.to_s)
    return nil, nil
  end
  unless REMOTE_ALLOWED_SCHEMES.include?(uri.scheme)
    raise RemoteFileError, uri.to_s if @options[:io_exceptions]
    return nil, nil
  end
  begin
    res = if @options[:allow_local_network]
            fetch_via_net_http(uri)
          else
            SsrfFilter.get(
              uri.to_s,
              scheme_whitelist: REMOTE_ALLOWED_SCHEMES,
              max_redirects: MAX_REDIRECTS,
              headers: {'User-Agent' => @options[:user_agent]}
            )
          end
    if res.code.to_i >= 400
      raise RemoteFileError, uri.to_s if @options[:io_exceptions]
      return '', nil
    end
    charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'
    src = res.body
    src.encode!('UTF-8', charset) if charset
    [src, charset]
  rescue
    raise RemoteFileError, uri.to_s if @options[:io_exceptions]
    [nil, nil]
  end
end

def remove_rule_set!(ruleset, media_types = :all)

+media_types+ can be a symbol or an array of symbols.

Remove a CssParser RuleSet object.
def remove_rule_set!(ruleset, media_types = :all)
  raise ArgumentError unless ruleset.is_a?(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

def reset! # :nodoc:

:nodoc:
def reset! # :nodoc:
  @folded_declaration_cache = {}
  @css_source = ''
  @css_rules = []
  @css_warnings = []
end

def rules_by_media_query

A hash of { :media_query => rule_sets }
def rules_by_media_query
  rules_by_media = {}
  @rules.each do |block|
    block[:media_types].each do |mt|
      unless rules_by_media.key?(mt)
        rules_by_media[mt] = []
      end
      rules_by_media[mt] << block[:rules]
    end
  end
  rules_by_media
end

def save_folded_declaration(block_hash, folded_declaration) # :nodoc:

:nodoc:
Save a folded declaration block to the internal cache.
def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
  @folded_declaration_cache[block_hash] = folded_declaration
end

def to_h(which_media = :all)

Output all CSS rules as a Hash
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

def to_s(which_media = :all)

Output all CSS rules as a single stylesheet.
def to_s(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|
    media_block = (media_type != :all)
    out << "@media #{media_type} {" if media_block
    media_styles.each do |media_style|
      if media_block
        out.push("  #{media_style[0]} {\n    #{media_style[1]}\n  }")
      else
        out.push("#{media_style[0]} {\n#{media_style[1]}\n}")
      end
    end
    out << '}' if media_block
  end
  out << ''
  out.join("\n")
end