Full Code of premailer/css_parser for AI

master cee10f16b11d cached
38 files
174.7 KB
48.8k tokens
247 symbols
1 requests
Download .txt
Repository: premailer/css_parser
Branch: master
Commit: cee10f16b11d
Files: 38
Total size: 174.7 KB

Directory structure:
gitextract_kcgfa2ip/

├── .editorconfig
├── .github/
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .jrubyrc
├── .rubocop.yml
├── .vscode/
│   └── tasks.json
├── CHANGELOG.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── css_parser.gemspec
├── lib/
│   ├── css_parser/
│   │   ├── parser.rb
│   │   ├── regexps.rb
│   │   ├── rule_set.rb
│   │   └── version.rb
│   └── css_parser.rb
└── test/
    ├── fixtures/
    │   ├── complex.css
    │   ├── import-circular-reference.css
    │   ├── import-malformed.css
    │   ├── import-with-media-types.css
    │   ├── import1.css
    │   ├── simple.css
    │   └── subdir/
    │       └── import2.css
    ├── rule_set/
    │   ├── declarations/
    │   │   └── test_value.rb
    │   └── test_declarations.rb
    ├── test_css_parser_basic.rb
    ├── test_css_parser_loading.rb
    ├── test_css_parser_media_types.rb
    ├── test_css_parser_misc.rb
    ├── test_css_parser_offset_capture.rb
    ├── test_css_parser_regexps.rb
    ├── test_helper.rb
    ├── test_merging.rb
    ├── test_rule_set.rb
    ├── test_rule_set_creating_shorthand.rb
    └── test_rule_set_expanding_shorthand.rb

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
Why and what is being done.

## Pre-Merge Checklist
- [ ] CHANGELOG.md updated with short summary


================================================
FILE: .github/workflows/ci.yml
================================================
name: Run css_parser CI

on:
  pull_request:
  push:
    branches:
      - master

jobs:
  test:
    name: Test ruby version matrix
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ruby-version: ['3.3', '3.4', '4.0', 'jruby']

    steps:
      - uses: actions/checkout@v4

      - run: rm Gemfile.lock

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: ${{ matrix.ruby-version }}
          bundler-cache: true

      - run: bundle exec rake test

  rubocop:
    name: Run rubocop
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.3' # lowest supported Ruby version
          bundler-cache: true

      - run: bundle exec rake rubocop


================================================
FILE: .gitignore
================================================
/pkg/
/bin/
.ruby-version


================================================
FILE: .jrubyrc
================================================
cext.enabled=true


================================================
FILE: .rubocop.yml
================================================
plugins:
  - rubocop-performance
  - rubocop-rake

AllCops:
  TargetRubyVersion: 3.3 # lowest supported version
  NewCops: enable

Layout/ArgumentAlignment:
  EnforcedStyle: with_fixed_indentation

Layout/AccessModifierIndentation:
  EnforcedStyle: outdent

Layout/LineLength:
  Enabled: false

Layout/SpaceInsideHashLiteralBraces:
  EnforcedStyle: no_space

Metrics/AbcSize:
  Enabled: false

Metrics/BlockLength:
  Enabled: false

Metrics/BlockNesting:
  Enabled: false

Metrics/ClassLength:
  Enabled: false

Metrics/CyclomaticComplexity:
  Enabled: false

Metrics/MethodLength:
  Enabled: false

Metrics/ModuleLength:
  Enabled: false

Metrics/PerceivedComplexity:
  Enabled: false

Style/AndOr:
  Enabled: false

Style/Documentation:
  Enabled: false

Style/IfUnlessModifier:
  Enabled: false

Style/Not:
  Enabled: false

Style/NumericPredicate:
  Enabled: false

Style/OpenStructUse:
  Exclude:
    - 'test/**/*'

Style/RedundantFreeze:
  Enabled: false

Style/RescueStandardError:
  EnforcedStyle: implicit

Style/SafeNavigation:
  Enabled: false

Style/StringLiterals:
  Exclude:
    - 'test/**/*'

Style/WordArray:
  Enabled: false

Style/SymbolArray:
  EnforcedStyle: brackets

Style/HashSyntax:
  EnforcedShorthandSyntax: never

Naming/BlockForwarding:
  EnforcedStyle: explicit

Style/CollectionQuerying:
  Enabled: false


================================================
FILE: .vscode/tasks.json
================================================
{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
      {
        "label": "Bundle dependencies and stubs",
        "type": "shell",
        "command": "bundle",
        "args": [
          "--binstubs"
        ],
        "isBackground": true,
        "problemMatcher": []
      },
      {
        "label": "Test",
        "type": "shell",
        "command": "${workspaceRoot}/bin/rake",
        "problemMatcher": [],
        "group": {
          "_id": "test",
          "isDefault": false
        }
      },
      {
        "label": "Benchmark",
        "type": "shell",
        "command": "${workspaceRoot}/bin/rake",
        "args": [
          "benchmark"
        ],
        "problemMatcher": []
      }
    ]
}


================================================
FILE: CHANGELOG.md
================================================
## Ruby CSS Parser CHANGELOG

### Unreleased

### Version 2.2.0
* Accept CSS `<number>` values with an omitted integer part (e.g. `.1`) inside `rgb()`/`rgba()`/`hsl()`/`hsla()`. Previously `RE_COLOUR_NUMERIC` and `RE_COLOUR_NUMERIC_ALPHA` required at least one digit before the decimal point, which caused colours such as `rgba(0,0,0,.1)` to be silently dropped during shorthand expansion (`background-color` from `background:`, `border-*-color` from `border:`).

### Version 2.1.0
* Validate ssl when pulling files via https

### Version 2.0.0
* Drop ruby <3.2, fix a memory leak

### Version v1.21.1

* Prefer `!important` rules over non-`!important` rules in the same ruleset
* Minor performance improvements

### Version v1.21.0

* Minor performance improvements

### Version v1.20.0

* Remove `iconv` conditional require

### Version v1.19.1

* Fix error when parsing values consisting of `!important` only

### Version v1.19.0

* Deprecate `load_uri!`, `load_file!` and `load_string!` positional arguments over keyword argument
* Deprecate `add_rule!` (positional arguments)and `add_rule_with_offsets!` for `add_rule!` (keyword argument)
* RuleSet initialize now takes keyword argument, positional arguments are still supported but deprecated
* Removed OffsetAwareRuleSet, it's a RuleSet with optional attributes filename and offset
* Improved performance of block parsing by using StringScanner
* Improve `RuleSet#parse_declarations!` performance by using substring search istead of regexps
* Fix error when parsing values consisting of `!important` only

### Version v1.18.0

 * Drop Ruby 2.7 compatibility for parity with Premailer [#149](https://github.com/premailer/css_parser/pull/149)

### Version v1.17.1

 * Improve security by using `File.read` instead of `IO.read` [#149](https://github.com/premailer/css_parser/pull/149)

### Version v1.17.0

 * Added `user_agent` as an option to Parser [#146](https://github.com/premailer/css_parser/pull/146)

### Version v1.16.0

 * Fix parsing space-less media query features like `@media(width:123px)` [#141](https://github.com/premailer/css_parser/pull/141)

### Version v1.15.0

 * Fix parsing background shorthands in ruby 3.2 [#140](https://github.com/premailer/css_parser/pull/140)

### Version v1.14.0

 * Fix parsing of multiline URL values for rule sets [#97](https://github.com/premailer/css_parser/pull/97)

### Version v1.13.0

 * Drop suppor for EOL ruby versions
 * fix regex deprecation

### Version v1.12.0

 * Improve exception message for missing value [#131](https://github.com/premailer/css_parser/pull/131)
 * `:rule_set_exceptions` option added [#132](https://github.com/premailer/css_parser/pull/132)

### Version 1.11.0

 * Do not combine border styles width/color/style are not all present

### Version 1.10.0

 * Allow CSS functions to be used in CssParser::RuleSet#expand_dimensions_shorthand! [#126](https://github.com/premailer/css_parser/pull/126)

### Version 1.9.0

 * Misc cleanup [#122](https://github.com/premailer/css_parser/pull/122)

### Version 1.8.0

 * Internal refactoring around ruleset [diff](https://github.com/premailer/css_parser/compare/v1.7.1...v1.8.0)

### Version 1.7.1

 * Force UTF-8 encoding; do not strip out UTF-8 chars. [#106](https://github.com/premailer/css_parser/pull/106)

### Version 1.7.0

 * No longer support ruby versions 1.9 2.0 2.1
 * Memory allocation improvements

### Version 1.6.0

 * Handles font-size/ line-height shorthand with spaces

### Version 1.5.0

 * Extended color keywords support (https://www.w3.org/TR/css3-color/).
 * `remove_rule_set!` method added.
 * `:capture_offsets` feature added.

### Version 1.4.10

 * Include uri in RemoteFileError message.
 * Prevent to convert single declarations to their respective shorthand.
 * Fix Ruby warnings.

### Version 1.4.9

 * Support for vrem, vh, vw, vmin, vmax and vm box model units.
 * Replace obsolete calls with actual ones.
 * Fix some Ruby warnings.

### Version 1.4.8

 * Allow to get CSS rules as Hash using `to_hash` method.
 * Updates to support Ruby 1.9 and JRuby.
 * utf-8 related update.

### Version 1.4.7

 * background-position shorthand fix.

### Version 1.4.6

 * Normalize whitespace in selectors and queries.
 * Strip spaces from keys.
 * More checks on ordering.

### Version 1.4.5

 * Maintenance release.

### Version 1.4.4

 * More robust redirection handling, refs #47.

### Version 1.4.3

 * Look for redirects, MAX_REDIRECTS set to 3, refs #36.
 * Fix border style expanding, refs #58.
 * load_string! described, refs #70.

### Version 1.4.2

 * Ship license with package, refs #69.

### Version 1.4.1

 * Fix background shorthands, refs #66.

### Version 1.4.0

 * Add support for background-size in the shorthand property @mitio

### Version 1.3.6

 * Fix bug not setting general rules after media query @jievans.
 * We doesn't support Ruby 1.8 anymore.
 * Run tests on Ruby 2.0 and Ruby 2.1.
 * Respect the :import option.

### Version 1.3.5

 * Use URI#request_uri instead of URI#path @duckinator.
 * Media_query_support @mzsanford
 * Don't require open-uri @aripollak
 * Symbols not sortable on 1.8.7 @morten
 * Improve create_dimensions_shorthand performance @aaronjensen
 * Fixes hash ordering in tests @morten

### Version 1.3.4

 * Enable code highlighting for tests @grosser
 * Fix error in media query parsing @smgt
 * Add test to missing cleaning of media type in parsing @smgt

### Version 1.3.3

 * Require version before requiring classes that depend on it @morten

### Version 1.3.2

 * Fix them crazy requires and only define version once @grosser
 * Apply ocd @grosser

### Version 1.3.1

 * More tests (and fixes) for background gradients @fortnightlabs
 * Support declarations with `;` in them @flavorpill
 * Stricter detection of !important @flavorpill

### Version 1.3.0

 * Updates of gem by @grosser
 * Multiple selectors should properly calculate specificity @alexdunae
 * Specificity: The selector with the highest specificity may be in a compound selector statement? @morten
 * Selectors should not be registered with surrounding whitespace. @morten
 * Fix RE_GRADIENT reference @alexdunae
 * Add load_string! method tests @alexdunae
 * Gradient regexp tests @alexdunae
 * Edited rule set @mccuskk

### Version 1.2.6

 * JRuby and Ruby 1.9.3-preview1 compat

### Version 1.2.5

 * Fix merging of multiple !important rules to match the spec

### Version 1.2.3

 * First pass of media query support

### Version 1.2.2

 * Fix merging of multiple !important rules to match the spec

### Version 1.2.1

 * Better border shorthand handling
 * List shorthand handling
 * Malformed URI handling improvements
 * Use Bundler

### Version 1.2.0

 * Specificity improvements
 * RGBA, HSL and HSLA support
 * Bug fixes

### Version 1.1.9

 * Add remove_declaration! to RuleSet

### Version 1.1.8

 * Fix syntax error

### Version 1.1.7

 * Automatically close missing braces at the end of a block

### Version 1.1.6

 * Fix media type handling in add_block! and load_uri!

### Version 1.1.5

 * Fix merging of !important declarations

### Version 1.1.4

 * Ruby 1.9.2 compat

### Version 1.1.3

 * allow limiting by media type in add_block!

### Version 1.1.2

 * improve parsing of malformed declarations
 * improve support for local files
 * added support for loading over SSL
 * added support for deflate

### Version 1.1.1

 * Ruby 1.9 compatibility
 * @import regexp updates
 * various bug fixes

### Version 1.1.0

 * Added support for local @import
 * Better remote @import handling

### Version 1.0.1

 * Fallback for declarations without sort order

### Version 1.0.0

 * Various test fixes and udpate for Ruby 1.9 (thanks to Tyler Cunnion)
 * Allow setting CSS declarations to nil

### Version 0.9

 * Initial version forked from Premailer project

### TODO: Future

 * re-implement caching on CssParser.merge
 * correctly parse http://www.webstandards.org/files/acid2/test.html


================================================
FILE: Gemfile
================================================
# frozen_string_literal: true

# Keep Gemfile.lock in repo. Reason: https://grosser.it/2015/08/14/check-in-your-gemfile-lock/

source 'https://rubygems.org'

gemspec

gem 'benchmark-ips'
gem 'bump'
gem 'maxitest'
gem 'memory_profiler'
gem 'mocha'
gem 'ostruct'
gem 'rake'
gem 'rubocop', '~> 1.84.2' # locked down so we don't get accidental new cops
gem 'rubocop-performance'
gem 'rubocop-rake'
gem 'webrick'


================================================
FILE: MIT-LICENSE
================================================
=== Ruby CSS Parser License

Copyright (c) 2007-11 Alex Dunae

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

================================================
FILE: README.md
================================================
# Ruby CSS Parser [![Build Status](https://github.com/premailer/css_parser/workflows/Run%20css_parser%20CI/badge.svg)](https://github.com/ojab/css_parser/actions?query=workflow%3A%22Run+css_parser+CI%22) [![Gem Version](https://badge.fury.io/rb/css_parser.svg)](https://badge.fury.io/rb/css_parser)

Load, parse and cascade CSS rule sets in Ruby.

# Setup

```Bash
gem install css_parser
```

# Usage

```Ruby
require 'css_parser'
include CssParser

parser = CssParser::Parser.new
parser.load_uri!('http://example.com/styles/style.css')

parser = CssParser::Parser.new
parser.load_uri!('file://home/user/styles/style.css')

# load a remote file, setting the base_uri and media_types
parser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', media_types: [:screen, :handheld]})

# load a local file, setting the base_dir and media_types
parser.load_file!('print.css', '~/styles/', :print)

# load a string
parser = CssParser::Parser.new
parser.load_string! 'a { color: hotpink; }'

# lookup a rule by a selector
parser.find_by_selector('#content')
#=> 'font-size: 13px; line-height: 1.2;'

# lookup a rule by a selector and media type
parser.find_by_selector('#content', [:screen, :handheld])

# iterate through selectors by media type
parser.each_selector(:screen) do |selector, declarations, specificity|
  ...
end

# add a block of CSS
css = <<-EOT
  body { margin: 0 1em; }
EOT

parser.add_block!(css)

# output all CSS rules in a single stylesheet
parser.to_s
=> #content { font-size: 13px; line-height: 1.2; }
   body { margin: 0 1em; }

# capturing byte offsets within a file
parser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', capture_offsets: true)
content_rule = parser.find_rule_sets(['#content']).first
content_rule.filename
#=> 'http://example.com/styles/styles.css'
content_rule.offset
#=> 10703..10752

# capturing byte offsets within a string
parser.load_string!('a { color: hotpink; }', {filename: 'index.html', capture_offsets: true)
content_rule = parser.find_rule_sets(['a']).first
content_rule.filename
#=> 'index.html'
content_rule.offset
#=> 0..21
```

# Testing

```Bash
bundle
bundle exec rake
```

Runs on Ruby 3.0/JRuby 9.4 or above.

# Credits

By Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-11.

License: MIT

Thanks to [all the wonderful contributors](http://github.com/premailer/css_parser/contributors) for their updates.

Made on Vancouver Island.


================================================
FILE: Rakefile
================================================
# frozen_string_literal: true

require 'bundler/setup'
require 'bundler/gem_tasks'
require 'rake/testtask'
require 'rubocop/rake_task'
require 'bump/tasks'

task default: [:rubocop, :test]

Rake::TestTask.new do |test|
  test.pattern = 'test/**/test*.rb'
  test.verbose = true
end

RuboCop::RakeTask.new do |t|
  # allow you to run "$ rake rubocop -a" to autofix
  t.options << '-a' if ARGV.include?('-a')
  t.options << '-A' if ARGV.include?('-A')
end

desc 'Run a performance evaluation.'
task :benchmark do
  require 'css_parser'

  require 'benchmark/ips'
  require 'memory_profiler'

  fixtures_dir = Pathname.new(__dir__).join('test/fixtures')
  import_css_path = fixtures_dir.join('import1.css').to_s.freeze
  complex_css_path = fixtures_dir.join('complex.css').to_s.freeze

  Benchmark.ips do |x|
    x.report('import1.css loading') { CssParser::Parser.new.load_file!(import_css_path) }
    x.report('complex.css loading') { CssParser::Parser.new.load_file!(complex_css_path) }
  end

  puts

  report = MemoryProfiler.report { CssParser::Parser.new.load_file!(import_css_path) }
  puts "Loading `import1.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB"

  report = MemoryProfiler.report { CssParser::Parser.new.load_file!(complex_css_path) }
  puts "Loading `complex.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB"
end


================================================
FILE: css_parser.gemspec
================================================
# frozen_string_literal: true

name = 'css_parser'
require "./lib/#{name}/version"

Gem::Specification.new name, CssParser::VERSION do |s|
  s.summary = 'Ruby CSS parser.'
  s.description = 'A set of classes for parsing CSS in Ruby.'
  s.email    = 'code@dunae.ca'
  s.homepage = "https://github.com/premailer/#{name}"
  s.author = 'Alex Dunae'
  s.files = Dir.glob('lib/**/*') + ['MIT-LICENSE']
  s.license = 'MIT'
  s.required_ruby_version = '>= 3.3'

  s.metadata['changelog_uri'] = 'https://github.com/premailer/css_parser/blob/master/CHANGELOG.md'
  s.metadata['source_code_uri'] = 'https://github.com/premailer/css_parser'
  s.metadata['bug_tracker_uri'] = 'https://github.com/premailer/css_parser/issues'
  s.metadata['rubygems_mfa_required'] = 'true'

  s.add_dependency 'addressable'
end


================================================
FILE: lib/css_parser/parser.rb
================================================
# frozen_string_literal: true

require 'strscan'

module CssParser
  # Exception class used for any errors encountered while downloading remote files.
  class RemoteFileError < IOError; end

  # Exception class used if a request is made to load a CSS file more than once.
  class CircularReferenceError < StandardError; end

  # == Parser class
  #
  # All CSS is converted to UTF-8.
  #
  # When calling Parser#new there are some configuaration options:
  # [<tt>absolute_paths</tt>] Convert relative paths to absolute paths (<tt>href</tt>, <tt>src</tt> and <tt>url('')</tt>. Boolean, default is <tt>false</tt>.
  # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.
  # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.
  class Parser
    USER_AGENT = "Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)".freeze
    RULESET_TOKENIZER_RX = /\s+|\\{2,}|\\?[{}\s"]|[()]|.[^\s"{}()\\]*/.freeze
    STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
    STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze

    # Initial parsing
    RE_AT_IMPORT_RULE = /@import\s*(?:url\s*)?(?:\()?(?:\s*)["']?([^'"\s)]*)["']?\)?([\w\s,^\]()]*)\)?[;\n]?/.freeze

    MAX_REDIRECTS = 3

    # Array of CSS files that have been loaded.
    attr_reader   :loaded_uris

    def initialize(options = {})
      @options = {
        absolute_paths: false,
        import: true,
        io_exceptions: true,
        rule_set_exceptions: true,
        capture_offsets: false,
        user_agent: USER_AGENT
      }.merge(options)

      # array of RuleSets
      @rules = []

      @redirect_count = nil

      @loaded_uris = []

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

    # Get declarations by selector.
    #
    # +media_types+ are optional, and can be a symbol or an array of symbols.
    # The default value is <tt>:all</tt>.
    #
    # ==== 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.
    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
    alias [] find_by_selector

    # 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

    # 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)
    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

    # Add a CSS rule by setting the +selectors+, +declarations+
    # and +media_types+. Optional pass +filename+ , +offset+ for source
    # reference too.
    #
    # +media_types+ can be a symbol or an array of symbols. default to :all
    # optional fields for source location for source location
    # +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.
    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

    # 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.
    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

    # Add a CssParser RuleSet object.
    #
    # +media_types+ can be a symbol or an array of symbols.
    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

    # Remove a CssParser RuleSet object.
    #
    # +media_types+ can be a symbol or an array of symbols.
    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

    # Iterate through RuleSet objects.
    #
    # +media_types+ can be a symbol or an array of symbols.
    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

    # 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

    # Iterate through CSS selectors.
    #
    # +media_types+ can be a symbol or an array of symbols.
    # See RuleSet#each_selector for +options+.
    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

    # 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

    # 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

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

    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

    # 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`
    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]

      src, = read_remote_file(uri) # skip charset

      add_block!(src, opts) if src
    end

    # 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

    # 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

  protected

    # Check that a path hasn't been loaded already
    #
    # Raises a CircularReferenceError exception if io_exceptions are on,
    # otherwise returns true/false.
    # TODO: fix rubocop
    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

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

    # Strip comments and clean up blank lines from a block of CSS.
    #
    # Returns a string.
    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

    # Download a file into a string.
    #
    # Returns the file's data and character set in an array.
    #--
    # TODO: add option to fail silently or throw and exception on a 404
    #++
    def read_remote_file(uri) # :nodoc:
      if @redirect_count.nil?
        @redirect_count = 0
      else
        @redirect_count += 1
      end

      unless circular_reference_check(uri.to_s)
        @redirect_count = nil
        return nil, nil
      end

      if @redirect_count > MAX_REDIRECTS
        @redirect_count = nil
        return nil, nil
      end

      src = '', charset = nil

      begin
        uri = Addressable::URI.parse(uri.to_s)

        if uri.scheme == 'file'
          # local file
          path = uri.path
          path.gsub!(%r{^/}, '') if Gem.win_platform?
          src = File.read(path, mode: 'rb')
        else
          # remote file
          if uri.scheme == 'https'
            uri.port = 443 unless uri.port
            http = Net::HTTP.new(uri.host, uri.port)
            http.use_ssl = true
          else
            http = Net::HTTP.new(uri.host, uri.port)
          end

          res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent], 'Accept-Encoding' => 'gzip'})
          src = res.body
          charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'

          if res.code.to_i >= 400
            @redirect_count = nil
            raise RemoteFileError, uri.to_s if @options[:io_exceptions]

            return '', nil
          elsif res.code.to_i >= 300 and res.code.to_i < 400
            unless res['Location'].nil?
              return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))
            end
          end

          case res['content-encoding']
          when 'gzip'
            io = Zlib::GzipReader.new(StringIO.new(res.body))
            src = io.read
          when 'deflate'
            io = Zlib::Inflate.new
            src = io.inflate(res.body)
          end
        end

        if charset
          src.encode!('UTF-8', charset)
        end
      rescue
        @redirect_count = nil
        raise RemoteFileError, uri.to_s if @options[:io_exceptions]

        return nil, nil
      end

      @redirect_count = nil
      [src, charset]
    end

  private

    # 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

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

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

    # recurse through nested nodes and return them as Hashes nested in
    # passed hash
    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
  end
end


================================================
FILE: lib/css_parser/regexps.rb
================================================
# frozen_string_literal: true

module CssParser
  def self.regex_possible_values(*values)
    Regexp.new("([\s]*^)?(#{values.join('|')})([\s]*$)?", 'i')
  end

  # :stopdoc:
  # Base types
  RE_NL = Regexp.new('(\n|\r\n|\r|\f)')
  RE_NON_ASCII = Regexp.new('([\x00-\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING) # [^\0-\177]
  RE_UNICODE = Regexp.new('(\\\\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE | Regexp::NOENCODING)
  RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\[^\n\r\f0-9a-f])')
  RE_IDENT = Regexp.new("[-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*", Regexp::IGNORECASE | Regexp::NOENCODING)

  # General strings
  RE_STRING1 = /("(.[^\n\r\f"]*|\\#{RE_NL}|#{RE_ESCAPE})*")/.freeze
  RE_STRING2 = /('(.[^\n\r\f']*|\\#{RE_NL}|#{RE_ESCAPE})*')/.freeze
  RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)

  RE_INHERIT = regex_possible_values 'inherit'

  RE_URI = /(url\(\s*(\s*#{RE_STRING}\s*)\s*\))|(url\(\s*([!#$%&*\-~]|#{RE_NON_ASCII}|#{RE_ESCAPE})*\s*)\)/ixm.freeze
  URI_RX = /url\(("([^"]*)"|'([^']*)'|([^)]*))\)/im.freeze
  URI_RX_OR_NONE = Regexp.union(URI_RX, /none/i)
  RE_GRADIENT = /[-a-z]*gradient\([-a-z0-9 .,#%()]*\)/im.freeze

  # Initial parsing
  RE_AT_IMPORT_RULE = /@import\s+(url\()?["']?(.[^'"\s]*)["']?\)?([\w\s,^\])]*)\)?;?/.freeze

  #--
  # RE_AT_MEDIA_RULE = Regexp.new('(\"(.[^\n\r\f\\"]*|\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\")')

  # RE_AT_IMPORT_RULE = Regexp.new('@import[\s]*(' + RE_STRING.to_s + ')([\w\s\,]*)[;]?', Regexp::IGNORECASE) -- should handle url() even though it is not allowed
  #++
  IMPORTANT_IN_PROPERTY_RX = /\s*!important\b\s*/i.freeze

  RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'
  RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'
  RE_REPEAT = regex_possible_values 'repeat(\-x|\-y)*|no\-repeat'
  RE_LIST_STYLE_TYPE = regex_possible_values(
    'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',
    'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',
    'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',
    'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'
  )
  RE_IMAGE = Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i)

  STRIP_CSS_COMMENTS_RX = %r{/\*.*?\*/}m.freeze
  STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze

  # Special units
  BOX_MODEL_UNITS_RX = /(auto|inherit|0|(-*([0-9]+|[0-9]*\.[0-9]+)(rem|vw|vh|vm|vmin|vmax|e[mx]+|px|[cm]+m|p[tc+]|in|%)))([\s;]|\Z)/imx.freeze
  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\-]*(([0-9]*\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\%))', Regexp::IGNORECASE)
  RE_SINGLE_BACKGROUND_POSITION = /#{RE_LENGTH_OR_PERCENTAGE}|left|center|right|top|bottom/i.freeze
  RE_SINGLE_BACKGROUND_SIZE = /#{RE_LENGTH_OR_PERCENTAGE}|auto|cover|contain|initial|inherit/i.freeze
  RE_BACKGROUND_POSITION = /#{RE_SINGLE_BACKGROUND_POSITION}\s+#{RE_SINGLE_BACKGROUND_POSITION}|#{RE_SINGLE_BACKGROUND_POSITION}/.freeze
  RE_BACKGROUND_SIZE = %r{\s*/\s*(#{RE_SINGLE_BACKGROUND_SIZE}\s+#{RE_SINGLE_BACKGROUND_SIZE}|#{RE_SINGLE_BACKGROUND_SIZE})}.freeze
  FONT_UNITS_RX = /((x+-)*small|medium|larger*|auto|inherit|([0-9]+|[0-9]*\.[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|%)*)/i.freeze
  RE_BORDER_STYLE = /(\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\s*$)?/imx.freeze
  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)

  # Functions like calc, var, clamp, etc.
  RE_FUNCTIONS = /
    (
      [a-z0-9-]+        # function name
    )
    (?>
      \(                # opening parenthesis
        (?:
          ([^()]+)
          |             # recursion via subexpression
          \g<0>
        )*
      \)                # closing parenthesis
    )
  /imx.freeze

  # Patterns for specificity calculations
  NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC = /
    (?:\.\w+)                     # classes
    |
    \[(?:\w+)                       # attributes
    |
    (?::(?:                          # pseudo classes
      link|visited|active
      |hover|focus
      |lang
      |target
      |enabled|disabled|checked|indeterminate
      |root
      |nth-child|nth-last-child|nth-of-type|nth-last-of-type
      |first-child|last-child|first-of-type|last-of-type
      |only-child|only-of-type
      |empty|contains
    ))
  /ix.freeze
  ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC = /
    (?:(?:^|[\s+>~]+)\w+       # elements
    |
    :{1,2}(?:                    # pseudo-elements
      after|before
      |first-letter|first-line
      |selection
    )
  )/ix.freeze

  # Colours
  NAMED_COLOURS = %w[
    aliceblue
    antiquewhite
    aqua
    aquamarine
    azure
    beige
    bisque
    black
    blanchedalmond
    blue
    blueviolet
    brown
    burlywood
    cadetblue
    chartreuse
    chocolate
    coral
    cornflowerblue
    cornsilk
    crimson
    cyan
    darkblue
    darkcyan
    darkgoldenrod
    darkgray
    darkgreen
    darkgrey
    darkkhaki
    darkmagenta
    darkolivegreen
    darkorange
    darkorchid
    darkred
    darksalmon
    darkseagreen
    darkslateblue
    darkslategray
    darkslategrey
    darkturquoise
    darkviolet
    deeppink
    deepskyblue
    dimgray
    dimgrey
    dodgerblue
    firebrick
    floralwhite
    forestgreen
    fuchsia
    gainsboro
    ghostwhite
    gold
    goldenrod
    gray
    green
    greenyellow
    grey
    honeydew
    hotpink
    indianred
    indigo
    ivory
    khaki
    lavender
    lavenderblush
    lawngreen
    lemonchiffon
    lightblue
    lightcoral
    lightcyan
    lightgoldenrodyellow
    lightgray
    lightgreen
    lightgrey
    lightpink
    lightsalmon
    lightseagreen
    lightskyblue
    lightslategray
    lightslategrey
    lightsteelblue
    lightyellow
    lime
    limegreen
    linen
    magenta
    maroon
    mediumaquamarine
    mediumblue
    mediumorchid
    mediumpurple
    mediumseagreen
    mediumslateblue
    mediumspringgreen
    mediumturquoise
    mediumvioletred
    midnightblue
    mintcream
    mistyrose
    moccasin
    navajowhite
    navy
    oldlace
    olive
    olivedrab
    orange
    orangered
    orchid
    palegoldenrod
    palegreen
    paleturquoise
    palevioletred
    papayawhip
    peachpuff
    peru
    pink
    plum
    powderblue
    purple
    red
    rosybrown
    royalblue
    saddlebrown
    salmon
    sandybrown
    seagreen
    seashell
    sienna
    silver
    skyblue
    slateblue
    slategray
    slategrey
    snow
    springgreen
    steelblue
    tan
    teal
    thistle
    tomato
    turquoise
    violet
    wheat
    white
    whitesmoke
    yellow
    yellowgreen

    transparent
    inherit
    currentColor
  ].freeze
  # CSS <number> allows the integer part to be omitted (e.g. `.1`), per CSS Values & Units.
  # `(?:\d*\.)?\d+` accepts `1`, `1.5`, and `.5` while still rejecting bare `1.`.
  RE_COLOUR_NUMERIC = /\b(hsl|rgb)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze
  RE_COLOUR_NUMERIC_ALPHA = /\b(hsla|rgba)\s*\(-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?,-?\s*-?(?:\d*\.)?\d+%?\s*%?\)/i.freeze
  RE_COLOUR_HEX = /\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\b/.freeze
  RE_COLOUR_NAMED = /\s*\b(#{NAMED_COLOURS.join('|')})\b/i.freeze
  RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)
  # :startdoc:
end


================================================
FILE: lib/css_parser/rule_set.rb
================================================
# frozen_string_literal: true

require 'forwardable'

module CssParser
  class RuleSet
    # Patterns for specificity calculations
    RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\s+>]+)\w+|:(first-line|first-letter|before|after))/i.freeze
    RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\.\w+)|(\[\w+)|(:(link|first-child|lang))/i.freeze

    BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze
    LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze
    FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze
    FONT_WEIGHT_PROPERTIES = ['font-style', 'font-weight', 'font-variant'].freeze
    BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze
    BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze
    DIMENSION_DIRECTIONS = [:top, :right, :bottom, :left].freeze

    NUMBER_OF_DIMENSIONS = 4

    DIMENSIONS = [
      ['margin', %w[margin-top margin-right margin-bottom margin-left]],
      ['padding', %w[padding-top padding-right padding-bottom padding-left]],
      ['border-color', %w[border-top-color border-right-color border-bottom-color border-left-color]],
      ['border-style', %w[border-top-style border-right-style border-bottom-style border-left-style]],
      ['border-width', %w[border-top-width border-right-width border-bottom-width border-left-width]]
    ].freeze

    WHITESPACE_REPLACEMENT = '___SPACE___'

    # Tokens for parse_declarations!
    COLON = ':'.freeze
    SEMICOLON = ';'.freeze
    LPAREN = '('.freeze
    RPAREN = ')'.freeze
    IMPORTANT = '!important'.freeze
    class Declarations
      class Value
        attr_reader :value
        attr_accessor :important

        def initialize(value, important: nil)
          self.value = value
          @important = important unless important.nil?
        end

        def value=(value)
          value = value.to_s.sub(/\s*;\s*\Z/, '')
          self.important = !value.slice!(CssParser::IMPORTANT_IN_PROPERTY_RX).nil?
          value.strip!
          raise ArgumentError, 'value is empty' if value.empty?

          @value = value.freeze
        end

        def to_s
          important ? "#{value} !important" : value
        end

        def ==(other)
          return false unless other.is_a?(self.class)

          value == other.value && important == other.important
        end
      end

      extend Forwardable

      def_delegators :declarations, :each, :each_value

      def initialize(declarations = {})
        self.declarations = {}
        declarations.each { |property, value| add_declaration!(property, value) }
      end

      # Add a CSS declaration
      # @param [#to_s] property that should be added
      # @param [Value, #to_s] value of the property
      #
      # @example
      #   declarations['color'] = 'blue'
      #
      #   puts declarations['color']
      #   => #<CssParser::RuleSet::Declarations::Value:0x000000000305c730 @important=false, @order=1, @value="blue">
      #
      # @example
      #   declarations['margin'] = '0px auto !important'
      #
      #   puts declarations['margin']
      #   => #<CssParser::RuleSet::Declarations::Value:0x00000000030c1838 @important=true, @order=2, @value="0px auto">
      #
      # If the property already exists its value will be over-written unless it was !important and the new value
      # is not !important.
      # If the value is empty - property will be deleted
      def []=(property, value)
        property = normalize_property(property)
        currently_important = declarations[property]&.important

        if value.is_a?(Value) && (!currently_important || value.important)
          declarations[property] = value
        elsif value.to_s.strip.empty?
          delete property
        else
          value = Value.new(value)
          declarations[property] = value if !currently_important || value.important
        end
      rescue ArgumentError => e
        raise e.exception, "#{property} #{e.message}"
      end
      alias add_declaration! []=

      def [](property)
        declarations[normalize_property(property)]
      end
      alias get_value []

      def key?(property)
        declarations.key?(normalize_property(property))
      end

      def size
        declarations.size
      end

      # Remove CSS declaration
      # @param [#to_s] property property to be removed
      #
      # @example
      #   declarations.delete('color')
      def delete(property)
        declarations.delete(normalize_property(property))
      end
      alias remove_declaration! delete

      # Replace CSS property with multiple declarations
      # @param [#to_s] property property name to be replaces
      # @param [Hash<String => [String, Value]>] replacements hash with properties to replace with
      #
      # @example
      #  declarations = Declarations.new('line-height' => '0.25px', 'font' => 'small-caps', 'font-size' => '12em')
      #  declarations.replace_declaration!('font', {'line-height' => '1px', 'font-variant' => 'small-caps', 'font-size' => '24px'})
      #  declarations
      #  => #<CssParser::RuleSet::Declarations:0x00000000029c3018
      #  @declarations=
      #  {"line-height"=>#<CssParser::RuleSet::Declarations::Value:0x00000000038ac458 @important=false, @value="1px">,
      #   "font-variant"=>#<CssParser::RuleSet::Declarations::Value:0x00000000039b3ec8 @important=false, @value="small-caps">,
      #   "font-size"=>#<CssParser::RuleSet::Declarations::Value:0x00000000029c2c80 @important=false, @value="12em">}>
      def replace_declaration!(property, replacements, preserve_importance: false)
        property = normalize_property(property)
        raise ArgumentError, "property #{property} does not exist" unless key?(property)

        replacement_declarations = self.class.new(replacements)

        if preserve_importance
          importance = get_value(property).important
          replacement_declarations.each_value { |value| value.important = importance }
        end

        replacement_keys = declarations.keys
        replacement_values = declarations.values
        property_index = replacement_keys.index(property)

        # We should preserve subsequent declarations of the same properties
        # and prior important ones if replacement one is not important
        replacements = replacement_declarations.each.with_object({}) do |(key, replacement), result|
          existing = declarations[key]

          # No existing -> set
          unless existing
            result[key] = replacement
            next
          end

          # Replacement more important than existing -> replace
          if replacement.important && !existing.important
            result[key] = replacement
            replaced_index = replacement_keys.index(key)
            replacement_keys.delete_at(replaced_index)
            replacement_values.delete_at(replaced_index)
            property_index -= 1 if replaced_index < property_index
            next
          end

          # Existing is more important than replacement -> keep
          next if !replacement.important && existing.important

          # Existing and replacement importance are the same,
          # value which is declared later wins
          result[key] = replacement if property_index > replacement_keys.index(key)
        end

        return if replacements.empty?

        replacement_keys.delete_at(property_index)
        replacement_keys.insert(property_index, *replacements.keys)

        replacement_values.delete_at(property_index)
        replacement_values.insert(property_index, *replacements.values)

        self.declarations = replacement_keys.zip(replacement_values).to_h
      end

      def to_s(options = {})
        str = declarations.reduce(+'') do |memo, (prop, value)|
          importance = options[:force_important] || value.important ? ' !important' : ''
          memo << "#{prop}: #{value.value}#{importance}; "
        end
        # TODO: Clean-up regexp doesn't seem to work
        str.gsub!(/^[\s^({)]+|[\n\r\f\t]*|\s+$/mx, '')
        str.strip!
        str
      end

      def ==(other)
        return false unless other.is_a?(self.class)

        declarations == other.declarations && declarations.keys == other.declarations.keys
      end

    protected

      attr_reader :declarations

    private

      attr_writer :declarations

      def normalize_property(property)
        property = property.to_s.downcase
        property.strip!
        property
      end
    end

    extend Forwardable

    # optional field for storing source reference
    # File offset range
    attr_reader :offset
    # the local or remote location
    attr_accessor :filename

    # Array of selector strings.
    attr_reader :selectors

    # Integer with the specificity to use for this RuleSet.
    attr_accessor :specificity

    # @!method add_declaration!
    #   @see CssParser::RuleSet::Declarations#add_declaration!
    # @!method delete
    #   @see CssParser::RuleSet::Declarations#delete
    def_delegators :declarations, :add_declaration!, :delete
    alias []= add_declaration!
    alias remove_declaration! delete

    def initialize(*args, selectors: nil, block: nil, offset: nil, filename: nil, specificity: nil) # rubocop:disable Metrics/ParameterLists
      if args.any?
        if selectors || block || offset || filename || specificity
          raise ArgumentError, "don't mix positional and keyword arguments"
        end

        warn '[DEPRECATION] positional arguments are deprecated use keyword instead.', uplevel: 1

        case args.length
        when 2
          selectors, block = args
        when 3
          selectors, block, specificity = args
        when 4
          filename, offset, selectors, block = args
        when 5
          filename, offset, selectors, block, specificity = args
        else
          raise ArgumentError
        end
      end

      @selectors = []
      @specificity = specificity

      unless offset.nil? == filename.nil?
        raise ArgumentError, 'require both offset and filename or no offset and no filename'
      end

      @offset = offset
      @filename = filename

      parse_selectors!(selectors) if selectors
      parse_declarations!(block)
    end

    # Get the value of a property
    def get_value(property)
      return '' unless (value = declarations[property])

      "#{value};"
    end
    alias [] get_value

    # Iterate through selectors.
    #
    # Options
    # -  +force_important+ -- boolean
    #
    # ==== Example
    #   ruleset.each_selector do |sel, dec, spec|
    #     ...
    #   end
    def each_selector(options = {}) # :yields: selector, declarations, specificity
      decs = declarations.to_s(options)
      if @specificity
        @selectors.each { |sel| yield sel.strip, decs, @specificity }
      else
        @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }
      end
    end

    # Iterate through declarations.
    def each_declaration # :yields: property, value, is_important
      declarations.each do |property_name, value|
        yield property_name, value.value, value.important
      end
    end

    # Return all declarations as a string.
    def declarations_to_s(options = {})
      declarations.to_s(options)
    end

    # Return the CSS rule set as a string.
    def to_s
      "#{@selectors.join(',')} { #{declarations} }"
    end

    # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.
    def expand_shorthand!
      # border must be expanded before dimensions
      expand_border_shorthand!
      expand_dimensions_shorthand!
      expand_font_shorthand!
      expand_background_shorthand!
      expand_list_style_shorthand!
    end

    # Convert shorthand background declarations (e.g. <tt>background: url("chess.png") gray 50% repeat fixed;</tt>)
    # into their constituent parts.
    #
    # See http://www.w3.org/TR/CSS21/colors.html#propdef-background
    def expand_background_shorthand! # :nodoc:
      return unless (declaration = declarations['background'])

      value = declaration.value.dup

      replacement =
        if value.match(CssParser::RE_INHERIT)
          BACKGROUND_PROPERTIES.to_h { |key| [key, 'inherit'] }
        else
          {
            'background-image' => value.slice!(CssParser::RE_IMAGE),
            'background-attachment' => value.slice!(CssParser::RE_SCROLL_FIXED),
            'background-repeat' => value.slice!(CssParser::RE_REPEAT),
            'background-color' => value.slice!(CssParser::RE_COLOUR),
            'background-size' => extract_background_size_from(value),
            'background-position' => value.slice!(CssParser::RE_BACKGROUND_POSITION)
          }
        end

      declarations.replace_declaration!('background', replacement, preserve_importance: true)
    end

    def extract_background_size_from(value)
      size = value.slice!(CssParser::RE_BACKGROUND_SIZE)

      size.sub(%r{^\s*/\s*}, '') if size
    end

    # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)
    # Additional splitting happens in expand_dimensions_shorthand!
    def expand_border_shorthand! # :nodoc:
      BORDER_PROPERTIES.each do |k|
        next unless (declaration = declarations[k])

        value = declaration.value.dup

        replacement = {
          "#{k}-width" => value.slice!(CssParser::RE_BORDER_UNITS),
          "#{k}-color" => value.slice!(CssParser::RE_COLOUR),
          "#{k}-style" => value.slice!(CssParser::RE_BORDER_STYLE)
        }

        declarations.replace_declaration!(k, replacement, preserve_importance: true)
      end
    end

    # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)
    # into their constituent parts.  Handles margin, padding, border-color, border-style and border-width.
    def expand_dimensions_shorthand! # :nodoc:
      DIMENSIONS.each do |property, (top, right, bottom, left)|
        next unless (declaration = declarations[property])

        value = declaration.value.dup

        # RGB and HSL values in borders are the only units that can have spaces (within params).
        # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we
        # can split easily on spaces.
        #
        # TODO: rgba, hsl, hsla
        value.gsub!(RE_COLOUR) { |c| c.gsub(/(\s*,\s*)/, ',') }

        matches = split_value_preserving_function_whitespace(value)

        case matches.length
        when 1
          values = matches.to_a * 4
        when 2
          values = matches.to_a * 2
        when 3
          values = matches.to_a
          values << matches[1] # left = right
        when 4
          values = matches.to_a
        else
          raise ArgumentError, "Cannot parse #{value}"
        end

        replacement = [top, right, bottom, left].zip(values).to_h

        declarations.replace_declaration!(property, replacement, preserve_importance: true)
      end
    end

    # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)
    # into their constituent parts.
    def expand_font_shorthand! # :nodoc:
      return unless (declaration = declarations['font'])

      # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand
      font_props = {
        'font-style' => 'normal',
        'font-variant' => 'normal',
        'font-weight' => 'normal',
        'font-size' => 'normal',
        'line-height' => 'normal'
      }

      value = declaration.value.dup
      value.gsub!(%r{/\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)

      in_fonts = false

      matches = value.scan(/"(?:.*[^"])"|'(?:.*[^'])'|(?:\w[^ ,]+)/)
      matches.each do |m|
        m.strip!
        m.gsub!(/;$/, '')

        if in_fonts
          if font_props.key?('font-family')
            font_props['font-family'] += ", #{m}"
          else
            font_props['font-family'] = m
          end
        elsif /normal|inherit/i.match?(m)
          FONT_WEIGHT_PROPERTIES.each do |font_prop|
            font_props[font_prop] ||= m
          end
        elsif /italic|oblique/i.match?(m)
          font_props['font-style'] = m
        elsif /small-caps/i.match?(m)
          font_props['font-variant'] = m
        elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)
          font_props['font-weight'] = m
        elsif CssParser::FONT_UNITS_RX.match?(m)
          if m.include?('/')
            font_props['font-size'], font_props['line-height'] = m.split('/', 2)
          else
            font_props['font-size'] = m
          end
          in_fonts = true
        end
      end

      declarations.replace_declaration!('font', font_props, preserve_importance: true)
    end

    # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)
    # into their constituent parts.
    #
    # See http://www.w3.org/TR/CSS21/generate.html#lists
    def expand_list_style_shorthand! # :nodoc:
      return unless (declaration = declarations['list-style'])

      value = declaration.value.dup

      replacement =
        if CssParser::RE_INHERIT.match?(value)
          LIST_STYLE_PROPERTIES.to_h { |key| [key, 'inherit'] }
        else
          {
            'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),
            'list-style-position' => value.slice!(CssParser::RE_INSIDE_OUTSIDE),
            'list-style-image' => value.slice!(CssParser::URI_RX_OR_NONE)
          }
        end

      declarations.replace_declaration!('list-style', replacement, preserve_importance: true)
    end

    # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.
    def create_shorthand!
      create_background_shorthand!
      create_dimensions_shorthand!
      # border must be shortened after dimensions
      create_border_shorthand!
      create_font_shorthand!
      create_list_style_shorthand!
    end

    # Combine several properties into a shorthand one
    def create_shorthand_properties!(properties, shorthand_property) # :nodoc:
      values = []
      properties_to_delete = []
      properties.each do |property|
        next unless (declaration = declarations[property])
        next if declaration.important

        values << declaration.value
        properties_to_delete << property
      end

      return if values.length <= 1

      properties_to_delete.each do |property|
        declarations.delete(property)
      end

      declarations[shorthand_property] = values.join(' ')
    end

    # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and
    # converts them into a shorthand CSS <tt>background</tt> property.
    #
    # Leaves properties declared !important alone.
    def create_background_shorthand! # :nodoc:
      # When we have a background-size property we must separate it and distinguish it from
      # background-position by preceding it with a backslash. In this case we also need to
      # have a background-position property, so we set it if it's missing.
      # http://www.w3schools.com/cssref/css3_pr_background.asp
      if (declaration = declarations['background-size']) && !declaration.important
        declarations['background-position'] ||= '0% 0%'
        declaration.value = "/ #{declaration.value}"
      end

      create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'
    end

    # Combine border-color, border-style and border-width into border
    # Should be run after create_dimensions_shorthand!
    #
    # TODO: this is extremely similar to create_background_shorthand! and should be combined
    def create_border_shorthand! # :nodoc:
      values = BORDER_STYLE_PROPERTIES.filter_map do |property|
        next unless (declaration = declarations[property])
        next if declaration.important
        # can't merge if any value contains a space (i.e. has multiple values)
        # we temporarily remove any spaces after commas for the check (inside rgba, etc...)
        next if /\s/.match?(declaration.value.gsub(/,\s/, ',').strip)

        declaration.value
      end

      return if values.size != BORDER_STYLE_PROPERTIES.size

      BORDER_STYLE_PROPERTIES.each do |property|
        declarations.delete(property)
      end

      declarations['border'] = values.join(' ')
    end

    # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)
    # and converts them into shorthand CSS properties.
    def create_dimensions_shorthand! # :nodoc:
      return if declarations.size < NUMBER_OF_DIMENSIONS

      DIMENSIONS.each do |property, dimensions|
        values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|
          next unless (declaration = declarations[dimensions[index]])

          result[side] = declaration.value
        end

        # All four dimensions must be present
        next if values.size != dimensions.size

        new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip
        declarations[property] = new_value unless new_value.empty?

        # Delete the longhand values
        dimensions.each { |d| declarations.delete(d) }
      end
    end

    # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and
    # tries to convert them into a shorthand CSS <tt>font</tt> property.  All
    # font properties must be present in order to create a shorthand declaration.
    def create_font_shorthand! # :nodoc:
      return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }

      new_value = +''
      ['font-style', 'font-variant', 'font-weight'].each do |property|
        unless declarations[property].value == 'normal'
          new_value << declarations[property].value << ' '
        end
      end

      new_value << declarations['font-size'].value

      unless declarations['line-height'].value == 'normal'
        new_value << '/' << declarations['line-height'].value
      end

      new_value << ' ' << declarations['font-family'].value

      declarations['font'] = new_value.gsub(/\s+/, ' ')

      FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }
    end

    # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and
    # converts them into a shorthand CSS <tt>list-style</tt> property.
    #
    # Leaves properties declared !important alone.
    def create_list_style_shorthand! # :nodoc:
      create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style'
    end

  private

    attr_accessor :declarations

    def compute_dimensions_shorthand(values)
      # All four sides are equal, returning single value
      return [:top] if values.values.uniq.count == 1

      # `/* top | right | bottom | left */`
      return DIMENSION_DIRECTIONS if values[:left] != values[:right]

      # Vertical are the same & horizontal are the same, `/* vertical | horizontal */`
      return [:top, :left] if values[:top] == values[:bottom]

      [:top, :left, :bottom]
    end

    def parse_declarations!(block) # :nodoc:
      self.declarations = Declarations.new

      return unless block

      continuation = nil
      block.split(SEMICOLON) do |decs|
        decs = (continuation ? "#{continuation};#{decs}" : decs)
        if unmatched_open_parenthesis?(decs)
          # Semicolon happened within parenthesis, so it is a part of the value
          # the rest of the value is in the next segment
          continuation = decs
          next
        end

        next unless (colon = decs.index(COLON))

        property = decs[0, colon]
        value = decs[(colon + 1)..]
        property.strip!
        value.strip!
        next if property.empty? || value.empty? || value.casecmp?(IMPORTANT)

        add_declaration!(property, value)
        continuation = nil
      end
    end

    def unmatched_open_parenthesis?(declarations)
      (lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)
    end

    #--
    # TODO: way too simplistic
    #++
    def parse_selectors!(selectors) # :nodoc:
      @selectors = selectors.split(',').map do |s|
        s.gsub!(/\s+/, ' ')
        s.strip!
        s
      end
    end

    def split_value_preserving_function_whitespace(value)
      split_value = value.gsub(RE_FUNCTIONS) do |c|
        c.gsub!(/\s+/, WHITESPACE_REPLACEMENT)
        c
      end

      matches = split_value.strip.split(/\s+/)

      matches.each do |c|
        c.gsub!(WHITESPACE_REPLACEMENT, ' ')
      end
    end
  end
end


================================================
FILE: lib/css_parser/version.rb
================================================
# frozen_string_literal: true

module CssParser
  VERSION = '2.2.0'.freeze
end


================================================
FILE: lib/css_parser.rb
================================================
# frozen_string_literal: true

require 'addressable/uri'
require 'uri'
require 'net/https'
require 'digest/md5'
require 'zlib'
require 'stringio'

require 'css_parser/version'
require 'css_parser/rule_set'
require 'css_parser/regexps'
require 'css_parser/parser'

module CssParser
  # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules
  # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).
  #
  # Takes one or more RuleSet objects.
  #
  # Returns a RuleSet.
  #
  # ==== Cascading
  # If a RuleSet object has its +specificity+ defined, that specificity is
  # used in the cascade calculations.
  #
  # If no specificity is explicitly set and the RuleSet has *one* selector,
  # the specificity is calculated using that selector.
  #
  # If no selectors the specificity is treated as 0.
  #
  # If multiple selectors are present then the greatest specificity is used.
  #
  # ==== Example #1
  #   rs1 = RuleSet.new(nil, 'color: black;')
  #   rs2 = RuleSet.new(nil, 'margin: 0px;')
  #
  #   merged = CssParser.merge(rs1, rs2)
  #
  #   puts merged
  #   => "{ margin: 0px; color: black; }"
  #
  # ==== Example #2
  #   rs1 = RuleSet.new(nil, 'background-color: black;')
  #   rs2 = RuleSet.new(nil, 'background-image: none;')
  #
  #   merged = CssParser.merge(rs1, rs2)
  #
  #   puts merged
  #   => "{ background: none black; }"
  #--
  # TODO: declaration_hashes should be able to contain a RuleSet
  #       this should be a Class method
  def self.merge(*rule_sets)
    # in case called like CssParser.merge([rule_set, rule_set])
    rule_sets.flatten! if rule_sets[0].is_a?(Array)

    unless rule_sets.all?(CssParser::RuleSet)
      raise ArgumentError, 'all parameters must be CssParser::RuleSets.'
    end

    return rule_sets[0] if rule_sets.length == 1

    # Internal storage of CSS properties that we will keep
    properties = {}

    rule_sets.each do |rule_set|
      rule_set.expand_shorthand!

      specificity = rule_set.specificity
      specificity ||= rule_set.selectors.filter_map { |s| calculate_specificity(s) }.max || 0

      rule_set.each_declaration do |property, value, is_important|
        # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order
        if !properties.key?(property)
          properties[property] = {value: value, specificity: specificity, is_important: is_important}
        elsif is_important
          if !properties[property][:is_important] || properties[property][:specificity] <= specificity
            properties[property] = {value: value, specificity: specificity, is_important: is_important}
          end
        elsif properties[property][:specificity] < specificity || properties[property][:specificity] == specificity
          unless properties[property][:is_important]
            properties[property] = {value: value, specificity: specificity, is_important: is_important}
          end
        end
      end
    end

    merged = properties.each_with_object(RuleSet.new(nil, nil)) do |(property, details), rule_set|
      value = details[:value].strip
      rule_set[property.strip] = details[:is_important] ? "#{value.gsub(/;\Z/, '')}!important" : value
    end

    merged.create_shorthand!
    merged
  end

  # Calculates the specificity of a CSS selector
  # per http://www.w3.org/TR/CSS21/cascade.html#specificity
  #
  # Returns an integer.
  #
  # ==== Example
  #  CssParser.calculate_specificity('#content div p:first-line a:link')
  #  => 114
  #--
  # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.
  #++
  def self.calculate_specificity(selector)
    a = 0
    b = selector.scan('#').length
    c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length
    d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length

    "#{a}#{b}#{c}#{d}".to_i
  rescue
    0
  end

  # Make <tt>url()</tt> links absolute.
  #
  # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.
  #
  # "For CSS style sheets, the base URI is that of the style sheet, not that of the source document."
  # per http://www.w3.org/TR/CSS21/syndata.html#uri
  #
  # Returns a string.
  #
  # ==== Example
  #  CssParser.convert_uris("body { background: url('../style/yellow.png?abc=123') };",
  #               "http://example.org/style/basic.css").inspect
  #  => "body { background: url('http://example.org/style/yellow.png?abc=123') };"
  def self.convert_uris(css, base_uri)
    base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)

    css.gsub(URI_RX) do
      uri = Regexp.last_match(1).to_s.gsub(/["']+/, '')
      # Don't process URLs that are already absolute
      unless uri.match?(%r{^[a-z]+://}i)
        begin
          uri = base_uri.join(uri)
        rescue
          nil
        end
      end
      "url('#{uri}')"
    end
  end

  def self.sanitize_media_query(raw)
    mq = raw.to_s.gsub(/\s+/, ' ')
    mq.strip!
    mq = 'all' if mq.empty?
    mq.to_sym
  end
end


================================================
FILE: test/fixtures/complex.css
================================================
/*
Fonts:
	font-family:'Caslon 540 LT W01 Italic';
	font-family:'Caslon 540 LT W01 Roman';
	font-family:'Univers LT W01 53 Extended';
	font-family:'Univers LT W01 57 Condensed';
	font-family:'Univers LT W01 65 Bold';
*/


/*!
   http://meyerweb.com/eric/tools/css/reset/ 
   v2.0 | 20110126
   License: none (public domain)
*/
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td,article,aside,canvas,details,embed,figure,figcaption,footer,header,hgroup,menu,nav,output,ruby,section,summary,time,mark,audio,video{margin:0;padding:0;border:0;font-size:100%;font:inherit;vertical-align:baseline;}
/* HTML5 display-role reset for older browsers */
article,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}
body{line-height:1;}
ol,ul{list-style:none;}
blockquote,q{quotes:none;}
blockquote:before,blockquote:after,
q:before,q:after{content:'';content:none;}
table{border-collapse:collapse;border-spacing:0;}

/*!
 * Copyright (c) 2008, Yahoo! Inc. All rights reserved.
 * Code licensed under the BSD License:
 * http://developer.yahoo.net/yui/license.txt
 * version: 2.6.0
*/
body{font:13px/1.231 helvetica,arial,clean,sans-serif;*font-size:small;*font:x-small;}select,input,button,textarea{font:99% arial,helvetica,clean,sans-serif;}table{font-size:inherit;font:100%;}pre,code,kbd,samp,tt{font-family:monospace;*font-size:108%;line-height:100%;}

body {
	font: normal 11px/20px sans-serif;
	color: #3C3C46;
	background: #FFF none;

}


.debug #page {background-image:url("grid.png?1");}

.debug img { opacity: .5;}
.debug .cyclenav, .debug .cycleprev, .debug .services li { background-color: rgba(100,100,100,0.5);}

/** Frame **/
body:after, #page:after, .features:after, section:after, #study_viewer:after, #study:after,#study .gallerywrap:after, footer:after, #name_case_converter:after, footer div:after, form .field:after{clear:both;display:block;visibility:hidden;overflow:hidden;height:0;content:"\0020";}

#container {
	margin: 0 auto;
	background: #FFF none;
}

#page {
	width: 940px;
	margin: 0 auto;
}




header{width:940px;margin:60px auto;background:#fff url("header_bg.png") 0 50% repeat-x;}
header li{display:block;float:left;width:180px;height:10px;padding:50px 0;line-height: 10px;}
#nav{width:590px;height:110px;margin:0 auto;font:normal 10px/10px "Univers LT W01 53 Extended",sans-serif;letter-spacing:4px;text-transform:uppercase;}
#logo,#logo a{display:block;width:110px;height:110px;margin:0 auto;padding:0;}
#logo{padding:0 55px;}
#logo a{background:transparent url("sprites.png") -380px 0;text-indent:-9999em;}
#logo a:hover,#logo a:focus{background-position:-380px -109px;}
#logo a:active{background-position:-380px -220px;}
#nav_work{text-align:right;}



header, footer, nav, section{clear:both;zoom:1;}
em, i { font-style: italic;}
b, strong {font-weight: bold;}
a { color: #000; text-decoration: none;}
img {display: block;}
pre {
	margin: 20px;
	line-height: 1.5;
}

h1 {
	margin-top: 3px;
	margin-bottom: 17px;
	font: normal 70px/80px "Caslon 540 LT W01 Roman", Georgia, serif;
	text-align: center;

	text-shadow: -2px 2px 1px #FFFFFF, -3px 3px 1px #DCD7D2;

	/*filter: dropshadow(color=#cccccc, offx=3, offy=2);*/
	/*filter: Shadow(Color=#cccccc,Direction=135,Strenth=1);*/
}

h2 {
	height: 20px;
	margin: 50px 0 23px;
	font: normal 9px/20px "Univers LT W01 53 Extended", sans-serif;
	background: #FFF url("dashdots.png") 0 50% repeat-x;
	text-align: center;
	text-transform: uppercase;
	letter-spacing: 5px;
}

h2 span { padding: 0 30px;background: #FFF none;}

h3 {
	margin-bottom: 12px;
	font: normal 16px/20px "Univers LT W01 57 Condensed", sans-serif;
}


ol, ul { margin-bottom: 20px; list-style-position: inside; font: italic 11px/20px Arial, sans-serif;}

blockquote {
	margin: 20px;
	font: italic 11px/20px Arial, sans-serif;
}

ul {
	list-style-type: disc;
}

li {
	margin: 0 0 20px;
	
}

p {
	margin-bottom: 20px;
}


.lede {
	width: 820px;
	margin: 0 auto 100px;
	font: normal 18px/30px "Caslon 540 LT W01 Roman", Georgia, serif;
	text-align: center;
}

.lede em, .lede i, .lede a {
		font-family: "Garamond W01 Italic", serif;
}

a {
	outline: none;
	font-style: italic;
}

a:hover, a:focus {
	text-decoration: underline;
}

#nav a, .features a, footer a { font-style: normal;}


/* Feature boxes */
.features {
	clear: both;
	display: block;
	width: 900px;
	margin-bottom: 50px;
	padding: 0 20px;
	list-style: none;
	font-style: normal;
	background-color: transparent !important;
}

.features a:hover, .features a:focus {
	text-decoration: none;
}

.features li {
	float: left;
	width: 180px;
	margin: 0 60px 50px 0;
}

#contact .features h3 {font-size: 16px;}

#contact .features li {text-align: center;}

#studies .features, #contact .features {
	width: 940px;
	padding: 0 60px;
}
#studies .features li, #contact .features li {
	width: 220px;
	margin-right: 80px;
}
#studies .lede {margin: 0 auto 50px;
}

#studies nav {
	width: 940px;
	margin: 0;
}

#studies .fade_l {
	position: absolute;
	left: 0;
	top: 0;
	width: 40px;
	height: 100%;
	z-index: 1000;
	background: transparent url("fade_l.png") repeat-y;
}



#studies .fade_r {
	position: absolute;
	right: 0;
	top: 0;
	width: 40px;
	height: 100%;
	z-index: 1000;
	background: transparent url("fade_r.png") repeat-y;
}

.features h3 { margin-bottom: 3px;}

.services h3 {
	margin-left: -5px;
	margin-bottom: 12px;
	padding-left: 40px;
	background: #FFF url("sprites.png")  0 -250px no-repeat;
}


.services .strategy h3  {
	background-position: 0 -297px;
}

.services .id h3  {
	padding-left: 43px;
	background-position: 0 -347px;
}

.services .review h3  {
	padding-left: 45px;
	background-position: 0 -397px;
}

.services p {
	padding: 0 7px;
}

.features p { margin-bottom:0;}
.features .img {
	display: block;
	width: 228px;
	height: 168px;
	margin: 0 -5px 15px;
	border: 1px solid #e4e4e4;
}
.features .img:active {
background-color: #e4e4e4;	
}

/*.features .img:hover img {opacity: 1;}*/

.features img { margin: 4px; }
.js .features img {opacity: 1;}
.features .first { clear: both;}
.features .last { margin-right: 0 !important;}

.features .meta {
	margin-bottom: 0;
	font: italic 12px/30px "Caslon 540 LT W01 Italic", Georgia, sans-serif;
}



#contact .email { font-style: italic;}
#content .tel a { font-style: normal;}
#contact a:hover, #contact a:focus { text-decoration: underline;}


footer {
	width: 940px;
	margin: 0 auto 0;
	padding-bottom: 100px;
}


footer nav {
	width: 100%;
	height: 10px;
	margin: 0 0 50px;
	padding: 25px 0;
	text-align: center;
	font: normal 8px/10px "Univers LT W01 53 Extended", sans-serif;
	letter-spacing: 3px;
	text-transform: uppercase;
	background: #fff url("header_bg.png") 0 50% repeat-x;
}

footer nav a {
	display: inline;
	padding: 0 32px;
	text-align: center;
	
}

footer div {
	margin: 0 auto;
	font-size: 10px;
	text-align: center;

	color: #B3B3B3;
}

footer a { font-style: normal;}

footer img {
	display: block;
	margin: 20px auto;
}




#study h1, .code h1, .inside h1 {
	font-size: 50px;
	line-height: 60px;
	text-shadow: none;
}

.focus h2 {
	height: auto;
	margin: 0 0 12px;
	border: 0;
	font: normal 15px/20px "Univers LT W01 57 Condensed", sans-serif;
	text-transform: none;
	text-align: left;
	letter-spacing: 0;
	background: none;
}

.focus ul { list-style: none;}

.focus li {
	margin: 5px 0 10px;
	font: normal 12px/15px "Univers LT W01 65 Bold", sans-serif;
}

#study { 	margin-bottom: 80px;}
#study .prose, #study .focus {
	float: left;
}

#study .prose {
	width: 460px;
	margin: 0 40px 0 120px;
	font: normal 15px/20px "Caslon 540 LT W01 Roman", Georgia, serif;
}

#study .prose em, #study .prose i {
	font-family: "Garamond W01 Italic", serif;
}

#study .focus {
	width: 200px;
}

#study .visit {
	margin: 20px 0;
	font: normal 15px/20px "Univers LT W01 57 Condensed", sans-serif;
	text-transform: lowercase;
}

#study .visit a { text-decoration:none;font-style: normal;}


.prose a { text-decoration: underline;}



.gallerywrap .gallery,.cycleprev,.cyclenext{float:left;}
.cycleprev,.cyclenext{display:block;width:26px;height:26px;text-indent:-9999em;cursor:pointer;background:#FFF url("sprites.png") no-repeat;}
.cycleprev{margin-left:14px;margin-right:80px;background-position:-40px -60px;}
.cycleprev:hover,.cycleprev:focus{background-position:-70px -60px;}
.cyclenext{margin-right:14px;margin-left:80px;background-position:-100px -60px;}
.cyclenext:hover,.cyclenext:focus{background-position:-130px -60px;}


.cyclenav{width:700px;height:12px;margin:50px auto 50px;text-align:center;}
.cyclenav a{display:inline-block;width:12px;height:12px;margin:0 5px;text-indent:-9999em;background:#fff url("sprites.png") 0 -60px no-repeat;}
.cyclenav a:hover,.cyclenav a:focus,.cyclenav a.activeSlide{background-position:-15px -60px;}
#study .cyclenav { margin-top: 40px;}
#studies .studywrap .cycleprev { margin: 25px 40px 35px 34px;}
#studies .studywrap .cyclenext { margin: 25px 34px 35px 40px;}
#studies .studywrap .cyclenav { float: left; width: 740px;height: 26px;margin:30px auto 45px;}
#studies nav ul.features { background-color: #FFF;}
#studies nav { margin-bottom: 20px; background-color: #FFF;}

#study .cycleprev,#study .cyclenext{margin-top:235px;}



.gallerywrap {
	width: 100%;
}

.gallery {
	width: 700px;
	margin: 0 auto 50px;
}

.js .gallery img {
	display: none;
}

.js .gallery img:first-child {
	display: block;
}

#study .gallery img {
	width: 700px;
	height: 500px;
	margin: 0 auto;
}




/* Forms and code */
form {
	width: 700px;
	margin: 45px auto;
}

form .field {
	clear: left;
	margin: 0 0 50px;
}

label {
	display: block;
	float: left;
	width: 280px;
	margin: 0 20px 5px 0;
	font: normal 20px/20px "Univers LT W01 57 Condensed", sans-serif;
}

label .pull {
	display: inline-block;
	width: 25px;
	margin-left: -30px;
}

form .sublabel {
	display: block;
	margin: 10px 0 5px;
	font: normal 12px/15px sans-serif;
}

form .hint {
	margin: 5px 0;
	font: italic 11px/15px sans-serif;
}


input.text, textarea {
	width: 378px;
	padding: 0 10px;
	border: 1px solid #E4E4E4;
}

input.text {
		height: 38px;
}

textarea { padding: 10px; line-height: 20px; height: 218px;}

form div.button {
	padding-left: 300px;
}

button.submit {
	display: block;
	width: 85px;
	height: 85px;
	border: 0;
	text-align: left;
	text-indent: -9999em;
	background: #FFF url("sprites.png") 0 -150px;
	cursor: pointer;
}

button.submit:hover, button.submit:focus {
	background-position: -100px -150px;
}

button.submit:active {
	background-position: -200px -150px;
}


#name_case_converter textarea {
	height: 400px;
}

================================================
FILE: test/fixtures/import-circular-reference.css
================================================
@import "import-circular-reference.css";

body { color: black; background: white; }
p { margin: 0px; }


================================================
FILE: test/fixtures/import-malformed.css
================================================
.malformed.one:before {
  content: "\\";
  color: "red";
}

.wellformed.one {
  color: "green";
}

.malformed.two:before {
  content: "\"";
  color: "red";
}

.wellformed.two {
  color: "green";
}

.malformed.three:before {
  content: "{";
  color: "red";
}

.wellformed.three {
  color: "green";
}

.malformed.four:before {
  content: "}";
  color: "red";
}

.wellformed.four {
  color: "green";
}

================================================
FILE: test/fixtures/import-with-media-types.css
================================================
@import "simple.css" print, tv, screen;

div { color: lime; }


================================================
FILE: test/fixtures/import1.css
================================================
@import 'subdir/import2.css';

div { color: lime; }


================================================
FILE: test/fixtures/simple.css
================================================
body {
	color: black;
	background: white;
}

p { margin: 0px; }


================================================
FILE: test/fixtures/subdir/import2.css
================================================
@import "../simple.css";

a { text-decoration: none; }


================================================
FILE: test/rule_set/declarations/test_value.rb
================================================
# frozen_string_literal: true

require_relative '../../test_helper'
require 'minitest/spec'
require 'ostruct'

class RuleSetProperyTest < Minitest::Test
  describe '.new' do
    describe 'with invalid value' do
      it 'raises an error when empty' do
        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new('  ') }
        assert_equal 'value is empty', exception.message
      end

      it 'raises an error when nil' do
        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new(nil) }
        assert_equal 'value is empty', exception.message
      end

      it 'raises an error when contains only important declaration' do
        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new(' !important; ') }
        assert_equal 'value is empty', exception.message
      end
    end

    describe 'with valid value' do
      it 'remove semicolon at the end' do
        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value;').value
      end

      it 'removes important declarations' do
        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value !important').value
      end

      it 'strips value' do
        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('  value ').value
      end

      it 'does everything above' do
        assert_equal "value\t another one",
          CssParser::RuleSet::Declarations::Value.new("  \tvalue\t another one  \t!important  \t  ;  ").value
      end

      it 'freezes the string' do
        assert_equal true, CssParser::RuleSet::Declarations::Value.new('value').value.frozen?
      end
    end

    describe 'important' do
      describe 'when not set' do
        it 'is not important if value is not important' do
          assert_equal false, CssParser::RuleSet::Declarations::Value.new('value').important
        end

        it 'is important if value is not important' do
          assert_equal true, CssParser::RuleSet::Declarations::Value.new('value !important;').important
        end
      end

      describe 'when set' do
        it 'overrides value importance' do
          assert_equal false, CssParser::RuleSet::Declarations::Value.new('value !important;', important: false).important
          assert_equal true, CssParser::RuleSet::Declarations::Value.new('value', important: true).important
        end
      end
    end
  end

  describe 'important=' do
    it 'sets importance' do
      property = CssParser::RuleSet::Declarations::Value.new('value')
      assert_equal false, property.important

      property.important = true
      assert_equal true, property.important
    end
  end

  describe 'value=' do
    it 'sets normalized value' do
      property = CssParser::RuleSet::Declarations::Value.new('foo')
      assert_equal 'foo', property.value

      property.value = "  \tvalue\t another one  \t!important  \t  ;  "
      assert_equal "value\t another one", property.value
    end

    it 'sets importance' do
      property = CssParser::RuleSet::Declarations::Value.new('foo')
      assert_equal 'foo', property.value
      assert_equal false, property.important

      property.value = 'bar !important'

      assert_equal 'bar', property.value
      assert_equal true, property.important
    end

    it 'freezes the string' do
      property = CssParser::RuleSet::Declarations::Value.new('foo')
      property.value = 'bar'

      assert_equal true, CssParser::RuleSet::Declarations::Value.new('value').value.frozen?
    end

    it 'raises an exception when the value is empty' do
      assert_raises ArgumentError do
        CssParser::RuleSet::Declarations::Value.new
      end
    end
  end

  describe '#to_s' do
    it 'returns value if not important' do
      assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value').to_s
    end

    it 'returns value with important annotation if important' do
      assert_equal 'value !important', CssParser::RuleSet::Declarations::Value.new('value', important: true).to_s
    end
  end

  describe '#==' do
    it 'returns true if value & importance are the same' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other = CssParser::RuleSet::Declarations::Value.new('value', important: true)

      assert_equal property, other
    end

    it 'returns false if value is not a Declarations::Value' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other = OpenStruct.new(value: 'value', important: true)

      refute_equal other, property
    end

    it 'returns true if value is a Declarations::Value subclass and value are equal' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other_class = Class.new(CssParser::RuleSet::Declarations::Value)
      other = other_class.new('value', important: true)

      assert_equal property, other
    end

    it 'returns false if value is a Declarations::Value subclass and value are not equal' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other_class = Class.new(CssParser::RuleSet::Declarations::Value)
      other = other_class.new('other value', important: true)

      refute_equal other, property
    end

    it 'returns false if value is different' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other = CssParser::RuleSet::Declarations::Value.new('other value', important: true)

      refute_equal property, other
    end

    it 'returns false if importance is different' do
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)
      other = CssParser::RuleSet::Declarations::Value.new('value', important: false)

      refute_equal property, other
    end
  end
end


================================================
FILE: test/rule_set/test_declarations.rb
================================================
# frozen_string_literal: true

require_relative '../test_helper'

class RuleSetDeclarationsTest < Minitest::Test
  describe '.new' do
    describe 'when initial declarations is not given' do
      it 'initialized empty' do
        assert_equal 0, CssParser::RuleSet::Declarations.new.size
      end
    end

    describe 'when initial declarations is given' do
      it 'initialized with given declarations' do
        baz_property = CssParser::RuleSet::Declarations::Value.new('baz value', important: true)
        declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value', bar: 'bar value', baz: baz_property})

        assert_equal 3, declarations.size

        assert_equal CssParser::RuleSet::Declarations::Value.new('foo value'), declarations['foo']
        assert_equal CssParser::RuleSet::Declarations::Value.new('bar value'), declarations[:bar]
        assert_equal baz_property, declarations['baz']
      end
    end
  end

  describe '#[]=' do
    it 'normalizes property name' do
      declarations = CssParser::RuleSet::Declarations.new

      declarations[:'  fOo'] = 'foo value'

      assert_equal true, declarations.key?('foo')
      assert_equal 1, declarations.size
      assert_equal 'foo value', declarations['foo'].value
    end

    it 'assigns proper value if Declarations::Value is given' do
      declarations = CssParser::RuleSet::Declarations.new
      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)

      declarations['foo'] = property

      assert_equal property, declarations['foo']
    end

    it 'deletes property if given value is empty' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value', bar: 'bar value'})
      assert_equal 2, declarations.size

      declarations['foo'] = nil

      assert_equal 1, declarations.size
      assert_equal true, declarations.key?('bar')
    end

    it 'creates Declarations::Value with proper value if string is given' do
      declarations = CssParser::RuleSet::Declarations.new
      declarations['foo'] = 'foo value'

      assert_instance_of CssParser::RuleSet::Declarations::Value, declarations['foo']
      assert_equal 'foo value', declarations['foo'].value
    end

    it 'has alias #add_declaration!' do
      declarations = CssParser::RuleSet::Declarations.new

      assert_equal declarations.method(:[]=), declarations.method(:add_declaration!)
    end

    it 'raises an exception including the property when the value is empty' do
      declarations = CssParser::RuleSet::Declarations.new

      assert_raises ArgumentError, 'foo value is empty' do
        declarations['foo'] = '!important'
      end
    end
  end

  describe '#[]' do
    it 'returns property if exists' do
      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)
      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})

      assert_equal foo_value, declarations['foo']
    end

    it 'returns nil if not exists' do
      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)
      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})

      assert_nil declarations['bar']
    end

    it 'normalizes property name' do
      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)
      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})

      assert_equal foo_value, declarations[:'Foo ']
    end

    it 'has alias #get_value' do
      declarations = CssParser::RuleSet::Declarations.new

      assert_equal declarations.method(:[]), declarations.method(:get_value)
    end
  end

  describe '#key?' do
    it 'return true if key exists' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})

      assert_equal true, declarations.key?('foo')
    end

    it 'return false if key does not exists' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})

      assert_equal false, declarations.key?('bar')
    end

    it 'normalizes property name' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})

      assert_equal true, declarations.key?(:'foO ')
    end
  end

  describe '#size' do
    it 'returns declarations size' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})

      assert_equal 1, declarations.size

      declarations['bar'] = 'bar value'

      assert_equal 2, declarations.size

      declarations['foo'] = nil

      assert_equal 1, declarations.size
    end
  end

  describe '#delete' do
    it 'removes declaration if exists' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})
      assert_equal 1, declarations.size

      declarations.remove_declaration!('foo')

      assert_equal 0, declarations.size
    end

    it 'does nothing if declarations does not exist' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})

      assert_equal 1, declarations.size

      declarations.remove_declaration!('bar')

      assert_equal 1, declarations.size
    end

    it 'normalizes property name' do
      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})
      assert_equal 1, declarations.size

      declarations.remove_declaration!(:fOo)

      assert_equal 0, declarations.size
    end

    it 'has alias #remove_declaration!' do
      declarations = CssParser::RuleSet::Declarations.new

      assert_equal declarations.method(:delete), declarations.method(:remove_declaration!)
    end
  end

  describe '#replace_declaration!' do
    it 'raises an error when replaced property does not exist' do
      declarations = CssParser::RuleSet::Declarations.new

      exception = assert_raises(ArgumentError) { declarations.replace_declaration!('property_name', {}) }
      assert_equal 'property property_name does not exist', exception.message
    end

    it 'replaces declaration with normalized property name in place' do
      declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value')

      declarations.replace_declaration!("   bAr\t\n", {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})

      expected = CssParser::RuleSet::Declarations.new(
        'foo' => 'foo_value', 'bar1' => 'bar1_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
      )
      assert_equal expected, declarations
    end

    describe 'when `preserve_importance: false`' do
      it 'does not set importance when replaced property is important' do
        declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value !important')

        declarations.replace_declaration!('foo', {'bar' => 'bar_value', 'baz' => 'baz_value !important'})
        expected = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'baz' => 'baz_value !important'})

        assert_equal expected, declarations
      end

      it 'does not unset importance when replaced property is not important' do
        declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value')

        declarations.replace_declaration!('foo', {'bar' => 'bar_value', 'baz' => 'baz_value !important'})
        expected = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'baz' => 'baz_value !important'})

        assert_equal expected, declarations
      end
    end

    describe 'when `preserve_importance: true`' do
      it 'sets importance when replaced property is important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value !important', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'}, preserve_importance: true)
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar2' => 'bar2_value !important', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        assert_equal expected, declarations
      end

      it 'unsets importance when replaced property is not important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'
        )

        declarations.replace_declaration!(
          'bar',
          {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value !important'},
          preserve_importance: true
        )
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'
        )

        assert_equal expected, declarations
      end
    end

    describe 'when subsequent declarations for the replacement declarations exist' do
      it 'does not replace declarations when both are not important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'
        )

        assert_equal expected, declarations
      end

      it 'does not replace declarations when both are important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        assert_equal expected, declarations
      end

      it 'does not replace declaration when only replaced is important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'
        )

        assert_equal expected, declarations
      end

      it 'replaces declarations when only replacement is important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
        )

        assert_equal expected, declarations
      end
    end

    describe 'when prior declarations for the replacement declarations exist' do
      it 'replaces declarations when both are not important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'bar1' => 'old_bar1_value', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'bar1' => 'bar1_value', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
        )

        assert_equal expected, declarations
      end

      it 'replaces declarations when both are important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'bar1' => 'bar1_value !important', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
        )

        assert_equal expected, declarations
      end

      it 'does not replace declaration when only replaced is important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
        )

        assert_equal expected, declarations
      end

      it 'replaces declarations when only replacement is important' do
        declarations = CssParser::RuleSet::Declarations.new(
          'bar1' => 'old_bar1_value', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'
        )

        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})
        expected = CssParser::RuleSet::Declarations.new(
          'foo' => 'foo_value', 'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value', 'baz' => 'baz_value'
        )

        assert_equal expected, declarations
      end
    end
  end

  describe '#each' do
    describe 'when block is not given' do
      it 'returns enumerator with properties in order' do
        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value')
        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value')
        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value')

        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})

        assert_instance_of Enumerator, declarations.each
        assert_equal 3, declarations.each.size
        assert_equal [['foo', foo_value], ['bar', bar_value], ['baz', baz_value]], declarations.each.to_a
      end
    end

    describe 'when block is given' do
      it 'yields properties in order' do
        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value')
        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value')
        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value')

        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})

        mock = stub("Fake")
        mock.expects(:call).with('foo', foo_value).returns(true)
        mock.expects(:call).with('bar', bar_value).returns(true)
        mock.expects(:call).with('baz', baz_value).returns(true)

        declarations.each { |name, value| mock.call(name, value) }
      end
    end
  end

  describe '#to_s' do
    context 'when `force_important` is not passed' do
      it 'returns declarations with declared importance' do
        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)
        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value', important: false)
        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value', important: true)

        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})

        assert_equal 'foo: foo value !important; bar: bar value; baz: baz value !important;', declarations.to_s
      end
    end

    context 'when `force_important` is passed' do
      it 'returns declarations with important annotations' do
        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: false)
        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value', important: false)
        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value', important: false)

        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})

        assert_equal 'foo: foo value !important; bar: bar value !important; baz: baz value !important;',
          declarations.to_s({force_important: true})
      end
    end
  end

  describe '#==' do
    it 'returns true if declarations & their order are the same' do
      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}
      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)
      other = CssParser::RuleSet::Declarations.new(declarations_hash)

      assert_equal declarations, other
    end

    it 'returns false if other is not a Declarations' do
      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}
      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)
      other = OpenStruct.new(declarations: declarations_hash)

      refute_equal declarations, other
    end

    it 'returns true if value is a Declarations subclass and declarations are equal' do
      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}
      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)
      other_class = Class.new(CssParser::RuleSet::Declarations)
      other = other_class.new(declarations_hash)

      assert_equal declarations, other
    end

    it 'returns false if value is a Declarations subclass and value are not equal' do
      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})
      other_class = Class.new(CssParser::RuleSet::Declarations)
      other = other_class.new({'bar' => 'bar_value', 'foo' => 'foo_value'})

      refute_equal declarations, other
    end

    it 'returns false if declarations values are different' do
      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})
      other = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'foo' => 'other_foo_value'})

      refute_equal declarations, other
    end

    it 'returns false if declarations are the same and their order is different' do
      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})
      other = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'foo' => 'foo_value'})

      refute_equal declarations, other
    end
  end
end


================================================
FILE: test/test_css_parser_basic.rb
================================================
# frozen_string_literal: true

require_relative "test_helper"

# Test cases for reading and generating CSS shorthand properties
class CssParserBasicTests < Minitest::Test
  include CssParser

  def setup
    @cp = CssParser::Parser.new
    @css = <<-CSS
      html, body, p { margin: 0px; }
      p { padding: 0px; }
      #content { font: 12px/normal sans-serif; }
      .content { color: red; }
    CSS
  end

  def test_finding_by_selector
    @cp.add_block!(@css)
    assert_equal 'margin: 0px;', @cp.find_by_selector('body').join(' ')
    assert_equal 'margin: 0px; padding: 0px;', @cp.find_by_selector('p').join(' ')
    assert_equal 'font: 12px/normal sans-serif;', @cp.find_by_selector('#content').join(' ')
    assert_equal 'color: red;', @cp.find_by_selector('.content').join(' ')
  end

  def test_adding_block
    @cp.add_block!(@css)
    assert_equal 'margin: 0px;', @cp.find_by_selector('body').join
  end

  def test_adding_block_without_closing_brace
    @cp.add_block!('p { color: red;')
    assert_equal 'color: red;', @cp.find_by_selector('p').join
  end

  def test_adding_a_rule
    @cp.add_rule!(selectors: 'div', block: 'color: blue;')
    assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
  end

  def test_adding_a_rule_set
    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')
    @cp.add_rule_set!(rs)
    assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')
  end

  def test_removing_a_rule_set
    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')
    @cp.add_rule_set!(rs)
    rs2 = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')
    @cp.remove_rule_set!(rs2)
    assert_equal '', @cp.find_by_selector('div').join(' ')
  end

  def test_toggling_uri_conversion
    # with conversion
    cp_with_conversion = Parser.new(absolute_paths: true)
    cp_with_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
      base_uri: 'http://example.org/style/basic.css')

    assert_equal "background: url('http://example.org/style/yellow.png?abc=123');",
      cp_with_conversion['body'].join(' ')

    # without conversion
    cp_without_conversion = Parser.new(absolute_paths: false)
    cp_without_conversion.add_block!("body { background: url('../style/yellow.png?abc=123') };",
      base_uri: 'http://example.org/style/basic.css')

    assert_equal "background: url('../style/yellow.png?abc=123');",
      cp_without_conversion['body'].join(' ')
  end

  def test_converting_to_hash
    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')
    @cp.add_rule_set!(rs)
    hash = @cp.to_h
    assert_equal 'blue', hash['all']['div']['color']
  end
end


================================================
FILE: test/test_css_parser_loading.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for the CssParser's loading functions.
class CssParserLoadingTests < Minitest::Test
  include CssParser
  include WEBrick

  def setup
    # from http://nullref.se/blog/2006/5/17/testing-with-webrick
    @cp = Parser.new

    @uri_base = 'http://localhost:12000'

    @www_root = File.expand_path('fixtures', __dir__)

    @server_thread = Thread.new do
      s = WEBrick::HTTPServer.new(Port: 12_000, DocumentRoot: @www_root, Logger: Log.new(nil, BasicLog::FATAL), AccessLog: [])
      s.mount_proc('/redirect301') do |_request, response|
        response['Location'] = '/simple.css'
        raise WEBrick::HTTPStatus::MovedPermanently
      end
      s.mount_proc('/redirect302') do |_request, response|
        response['Location'] = '/simple.css'
        raise WEBrick::HTTPStatus::TemporaryRedirect
      end
      @port = s.config[:Port]
      begin
        s.start
      ensure
        s.shutdown
      end
    end

    sleep 1 # ensure the server has time to load
  end

  def teardown
    @server_thread.kill
    @server_thread.join(5)
    @server_thread = nil
  end

  def test_loading_301_redirect
    @cp.load_uri!("#{@uri_base}/redirect301")
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_loading_302_redirect
    @cp.load_uri!("#{@uri_base}/redirect302")
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_loading_a_local_file
    file_name = File.expand_path('fixtures/simple.css', __dir__)
    @cp.load_file!(file_name)
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_loading_a_local_file_with_scheme
    file_name = "file://#{__dir__}/fixtures/simple.css"
    @cp.load_uri!(file_name)
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_loading_a_remote_file
    @cp.load_uri!("#{@uri_base}/simple.css")
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  # http://github.com/premailer/css_parser/issues#issue/4
  def test_loading_a_remote_file_over_ssl
    @cp.load_uri!("https://dialect.ca/inc/screen.css")
    assert_includes(@cp.find_by_selector('body').join(' '), "margin: 0;")
  end

  def test_loading_a_string
    @cp.load_string!("p{margin:0px}")
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_following_at_import_rules_local
    base_dir = File.expand_path('fixtures', __dir__)
    @cp.load_file!('import1.css', base_dir: base_dir)

    # from '/import1.css'
    assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ')

    # from '/subdir/import2.css'
    assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ')

    # from '/subdir/../simple.css'
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_following_at_import_rules_remote
    @cp.load_uri!("#{@uri_base}/import1.css")

    # from '/import1.css'
    assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ')

    # from '/subdir/import2.css'
    assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ')

    # from '/subdir/../simple.css'
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_imports_disabled
    cp = Parser.new(import: false)
    cp.load_uri!("#{@uri_base}/import1.css")

    # from '/import1.css'
    assert_equal 'color: lime;', cp.find_by_selector('div').join(' ')

    # from '/subdir/import2.css'
    assert_equal '', cp.find_by_selector('a').join(' ')

    # from '/subdir/../simple.css'
    assert_equal '', cp.find_by_selector('p').join(' ')
  end

  def test_following_remote_import_rules
    css_block = '@import "http://example.com/css";'

    assert_raises CssParser::RemoteFileError do
      @cp.add_block!(css_block, base_uri: "#{@uri_base}/subdir/")
    end
  end

  def test_following_badly_escaped_import_rules
    css_block = '@import "http://example.com/css?family=Droid+Sans:regular,bold|Droid+Serif:regular,italic,bold,bolditalic&subset=latin";'

    assert_raises CssParser::RemoteFileError do
      @cp.add_block!(css_block, base_uri: "#{@uri_base}/subdir/")
    end
  end

  def test_loading_malformed_content_strings
    file_name = File.expand_path('fixtures/import-malformed.css', __dir__)
    @cp.load_file!(file_name)
    @cp.each_selector do |_sel, dec, _spec|
      assert_equal false, dec.include?('wellformed')
    end
  end

  def test_loading_malformed_css_brackets
    file_name = File.expand_path('fixtures/import-malformed.css', __dir__)
    @cp.load_file!(file_name)
    selector_count = 0
    @cp.each_selector do |_sel, _dec, _spec|
      selector_count += 1
    end

    assert_equal 8, selector_count
  end

  def test_following_at_import_rules_from_add_block
    css_block = '@import "../simple.css";'

    @cp.add_block!(css_block, base_uri: "#{@uri_base}/subdir/")

    # from 'simple.css'
    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')
  end

  def test_importing_with_media_types
    @cp.load_uri!("#{@uri_base}/import-with-media-types.css")

    # from simple.css with :screen media type
    assert_equal 'margin: 0px;', @cp.find_by_selector('p', :screen).join(' ')
    assert_equal '', @cp.find_by_selector('p', :tty).join(' ')
  end

  def test_local_circular_reference_exception
    assert_raises CircularReferenceError do
      @cp.load_file!(File.expand_path('fixtures/import-circular-reference.css', __dir__))
    end
  end

  def test_remote_circular_reference_exception
    assert_raises CircularReferenceError do
      @cp.load_uri!("#{@uri_base}/import-circular-reference.css")
    end
  end

  def test_suppressing_circular_reference_exceptions
    cp_without_exceptions = Parser.new(io_exceptions: false)

    cp_without_exceptions.load_uri!("#{@uri_base}/import-circular-reference.css")
  end

  def test_toggling_not_found_exceptions
    cp_with_exceptions = Parser.new(io_exceptions: true)

    err = assert_raises RemoteFileError do
      cp_with_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
    end

    assert_includes err.message, "#{@uri_base}/no-exist.xyz"

    cp_without_exceptions = Parser.new(io_exceptions: false)

    cp_without_exceptions.load_uri!("#{@uri_base}/no-exist.xyz")
  end
end


================================================
FILE: test/test_css_parser_media_types.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for the handling of media types
class CssParserMediaTypesTests < Minitest::Test
  include CssParser

  def setup
    @cp = Parser.new
  end

  def test_that_media_types_dont_include_all
    @cp.add_block!(<<-CSS)
      @media handheld {
        body { color: blue; }
        p { color: grey; }
      }
      @media screen {
        body { color: red; }
      }
    CSS
    rules = @cp.rules_by_media_query
    assert_equal ["handheld", "screen"], rules.keys.map(&:to_s).sort
  end

  def test_finding_by_media_type
    # from http://www.w3.org/TR/CSS21/media.html#at-media-rule
    @cp.add_block!(<<-CSS)
      @media print {
        body { font-size: 10pt }
      }
      @media screen {
        body { font-size: 13px }
      }
      @media screen, print {
        body { line-height: 1.2 }
      }
      @media screen, 3d-glasses, print and resolution > 90dpi {
        body { color: blue; }
      }
    CSS

    assert_equal 'font-size: 10pt; line-height: 1.2;', @cp.find_by_selector('body', :print).join(' ')
    assert_equal 'font-size: 13px; line-height: 1.2; color: blue;', @cp.find_by_selector('body', :screen).join(' ')
    assert_equal 'color: blue;', @cp.find_by_selector('body', :'print and resolution > 90dpi').join(' ')
  end

  def test_with_parenthesized_media_features
    @cp.add_block!(<<-CSS)
      body { color: black }
      @media(prefers-color-scheme: dark) {
        body { color: white }
      }
      @media(min-width: 500px) {
        body { color: blue }
      }
      @media screen and (width > 500px) {
        body { color: red }
      }
    CSS
    assert_equal [:all, :'(prefers-color-scheme: dark)', :'(min-width: 500px)', :'screen and (width > 500px)'], @cp.rules_by_media_query.keys
    assert_equal 'color: white;', @cp.find_by_selector('body', :'(prefers-color-scheme: dark)').join(' ')
    assert_equal 'color: blue;', @cp.find_by_selector('body', :'(min-width: 500px)').join(' ')
    assert_equal 'color: red;', @cp.find_by_selector('body', :'screen and (width > 500px)').join(' ')
  end

  def test_finding_by_multiple_media_types
    @cp.add_block!(<<-CSS)
      @media print {
        body { font-size: 10pt }
      }
      @media handheld {
        body { font-size: 13px }
      }
      @media screen, print {
        body { line-height: 1.2 }
      }
    CSS

    assert_equal 'font-size: 13px; line-height: 1.2;', @cp.find_by_selector('body', [:screen, :handheld]).join(' ')
  end

  def test_adding_block_with_media_types
    @cp.add_block!(<<-CSS, media_types: [:screen])
      body { font-size: 10pt }
    CSS

    assert_equal 'font-size: 10pt;', @cp.find_by_selector('body', :screen).join(' ')
    assert @cp.find_by_selector('body', :handheld).empty?
  end

  def test_adding_block_with_media_types_followed_by_general_rule
    @cp.add_block!(<<-CSS)
      @media print {
        body { font-size: 10pt }
      }

      body { color: black; }
    CSS

    assert_includes @cp.to_s, 'color: black;'
  end

  def test_adding_block_and_limiting_media_types1
    css = <<-CSS
      @import "import1.css", print
    CSS

    base_dir = Pathname.new(__dir__).join('fixtures')

    @cp.add_block!(css, only_media_types: :screen, base_dir: base_dir)
    assert @cp.find_by_selector('div').empty?
  end

  def test_adding_block_and_limiting_media_types2
    css = <<-CSS
      @import "import1.css", print and (color)
    CSS

    base_dir = Pathname.new(__dir__).join('fixtures')

    @cp.add_block!(css, only_media_types: 'print and (color)', base_dir: base_dir)
    assert_includes @cp.find_by_selector('div').join(' '), 'color: lime'
  end

  def test_adding_block_and_limiting_media_types
    css = <<-CSS
      @import "import1.css"
    CSS

    base_dir = Pathname.new(__dir__).join('fixtures')
    @cp.add_block!(css, only_media_types: :print, base_dir: base_dir)
    assert_equal '', @cp.find_by_selector('div').join(' ')
  end

  def test_adding_rule_set_with_media_type
    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: [:handheld, :tty])
    @cp.add_rule!(selectors: 'body', block: 'color: blue;', media_types: :screen)
    assert_equal 'color: black;', @cp.find_by_selector('body', :handheld).join(' ')
  end

  def test_adding_rule_set_with_media_query
    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: 'aural and (device-aspect-ratio: 16/9)')
    assert_equal 'color: black;', @cp.find_by_selector('body', 'aural and (device-aspect-ratio: 16/9)').join(' ')
    assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ')
  end

  def test_selecting_with_all_media_types
    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: [:handheld, :tty])
    assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ')
  end

  def test_to_s_includes_media_queries
    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: 'aural and (device-aspect-ratio: 16/9)')
    assert_equal "@media aural and (device-aspect-ratio: 16/9) {\n  body {\n    color: black;\n  }\n}\n", @cp.to_s
  end
end


================================================
FILE: test/test_css_parser_misc.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for the CssParser.
class CssParserTests < Minitest::Test
  include CssParser

  def setup
    @cp = Parser.new
  end

  def test_utf8
    css = <<-CSS
      .chinese { font-family: "Microsoft YaHei","微软雅黑"; }
    CSS

    @cp.add_block!(css)

    assert_equal 'font-family: "Microsoft YaHei","微软雅黑";', @cp.find_by_selector('.chinese').join(' ')
  end

  def test_at_page_rule
    # from http://www.w3.org/TR/CSS21/page.html#page-selectors
    css = <<-CSS
      @page { margin: 2cm }

      @page :first {
        margin-top: 10cm
      }
    CSS

    @cp.add_block!(css)

    assert_equal 'margin: 2cm;', @cp.find_by_selector('@page').join(' ')
    assert_equal 'margin-top: 10cm;', @cp.find_by_selector('@page :first').join(' ')
  end

  def test_should_ignore_comments
    # see http://www.w3.org/Style/CSS/Test/CSS2.1/current/html4/t040109-c17-comments-00-b.htm
    css = <<-CSS
      /* This is a CSS comment. */
      .one {color: green;} /* Another comment */
      /* The following should not be used:
      .one {color: red;} */
      .two {color: green; /* color: yellow; */}
      /**
      .three {color: red;} */
      .three {color: green;}
      /**/
      .four {color: green;}
      /*********/
      .five {color: green;}
      /* a comment **/
      .six {color: green;}
    CSS

    @cp.add_block!(css)
    @cp.each_selector do |_sel, decs, _spec|
      assert_equal 'color: green;', decs
    end
  end

  def test_parsing_blocks
    # dervived from http://www.w3.org/TR/CSS21/syndata.html#rule-sets
    css = <<-CSS
      div[name='test'] {

      color:

      red;

      }div:hover{coloR:red;
         }div:first-letter{color:red;/*color:blue;}"commented out"*/}

      p[example="public class foo\
      {\
          private string x;\
      \
          foo(int x) {\
              this.x = 'test';\
              this.x = "test";\
          }\
      \
      }"] { color: red }

      p { color:red}
    CSS

    @cp.add_block!(css)

    @cp.each_selector do |_sel, decs, _spec|
      assert_equal 'color: red;', decs
    end
  end

  def test_ignoring_malformed_declarations
    # dervived from http://www.w3.org/TR/CSS21/syndata.html#parsing-errors
    css = <<-CSS
      p { color:green }
      p { color:green; color }  /* malformed declaration missing ':', value */
      p { color:red;   color; color:green }  /* same with expected recovery */
      p { color:green; color: } /* malformed declaration missing value */
      p { color:red;   color:; color:green } /* same with expected recovery */
      p { color:green; color{;color:maroon} } /* unexpected tokens { } */
      p { color:red;   color{;color:maroon}; color:green } /* same with recovery */
    CSS

    @cp.add_block!(css)

    @cp.each_selector do |_sel, decs, _spec|
      assert_equal 'color: green;', decs
    end
  end

  def test_multiline_declarations
    css = <<-CSS
      @font-face {
        font-family: 'some_font';
        src: url(https://example.com/font.woff2) format('woff2'),
             url(https://example.com/font.woff) format('woff');
        font-style: normal;
      }
    CSS

    @cp.add_block!(css)
    @cp.each_selector do |selector, declarations, _spec|
      assert_equal '@font-face', selector
      assert_equal "font-family: 'some_font'; " \
                   "src: url(https://example.com/font.woff2) format('woff2'),url(https://example.com/font.woff) format('woff'); " \
                   "font-style: normal;", declarations
    end
  end

  def test_find_rule_sets
    css = <<-CSS
      h1, h2 { color: blue; }
      h1 { font-size: 10px; }
      h2 { font-size: 5px; }
      article  h3  { color: black; }
      article
      h3 { background-color: white; }
    CSS

    @cp.add_block!(css)
    assert_equal 2, @cp.find_rule_sets(["h2"]).size
    assert_equal 3, @cp.find_rule_sets(["h1", "h2"]).size
    assert_equal 2, @cp.find_rule_sets(["article h3"]).size
    assert_equal 2, @cp.find_rule_sets(["  article \t  \n  h3 \n "]).size
  end

  def test_calculating_specificity
    # from http://www.w3.org/TR/CSS21/cascade.html#specificity
    assert_equal 0,   CssParser.calculate_specificity('*')
    assert_equal 1,   CssParser.calculate_specificity('li')
    assert_equal 2,   CssParser.calculate_specificity('li:first-line')
    assert_equal 2,   CssParser.calculate_specificity('ul li')
    assert_equal 3,   CssParser.calculate_specificity('ul ol+li')
    assert_equal 11,  CssParser.calculate_specificity('h1 + *[rel=up]')
    assert_equal 13,  CssParser.calculate_specificity('ul ol li.red')
    assert_equal 21,  CssParser.calculate_specificity('li.red.level')
    assert_equal 100, CssParser.calculate_specificity('#x34y')

    # from http://www.hixie.ch/tests/adhoc/css/cascade/specificity/003.html
    assert_equal CssParser.calculate_specificity('div *'), CssParser.calculate_specificity('p')
    assert CssParser.calculate_specificity('body div *') > CssParser.calculate_specificity('div *')

    # other tests
    assert_equal 11, CssParser.calculate_specificity('h1[id|=123]')
  end

  def test_converting_uris
    base_uri = 'http://www.example.org/style/basic.css'
    ["body { background: url(yellow) };", "body { background: url('yellow') };",
     "body { background: url('/style/yellow') };",
     "body { background: url(\"../style/yellow\") };",
     "body { background: url(\"lib/../../style/yellow\") };"].each do |css|
      converted_css = CssParser.convert_uris(css, base_uri)
      assert_equal "body { background: url('http://www.example.org/style/yellow') };", converted_css
    end

    converted_css = CssParser.convert_uris("body { background: url(../style/yellow-dot_symbol$.png?abc=123&amp;def=456&ghi=789#1011) };", base_uri)
    assert_equal "body { background: url('http://www.example.org/style/yellow-dot_symbol$.png?abc=123&amp;def=456&ghi=789#1011') };", converted_css

    # taken from error log: 2007-10-23 04:37:41#2399
    converted_css = CssParser.convert_uris('.specs {font-family:Helvetica;font-weight:bold;font-style:italic;color:#008CA8;font-size:1.4em;list-style-image:url("images/bullet.gif");}', 'http://www.example.org/directory/file.html')
    assert_equal ".specs {font-family:Helvetica;font-weight:bold;font-style:italic;color:#008CA8;font-size:1.4em;list-style-image:url('http://www.example.org/directory/images/bullet.gif');}", converted_css
  end

  def test_ruleset_with_braces
    # parser = Parser.new
    # parser.add_block!("div { background-color: black !important; }")
    # parser.add_block!("div { background-color: red; }")
    #
    # rulesets = []
    #
    # parser['div'].each do |declaration|
    #   rulesets << RuleSet.new(selectors: 'div', block: declaration)
    # end
    #
    # merged = CssParser.merge(rulesets)
    #
    # result: # merged.to_s => "{ background-color: black !important; }"

    new_rule = RuleSet.new(selectors: 'div', block: "{ background-color: black !important; }")

    assert_equal 'div { background-color: black !important; }', new_rule.to_s
  end

  def test_content_with_data
    rule = RuleSet.new(selectors: 'div', block: '{content: url(data:image/png;base64,LOTSOFSTUFF)}')
    assert_includes rule.to_s, "image/png;base64,LOTSOFSTUFF"
  end

  def test_enumerator_empty
    assert_kind_of Enumerator, @cp.each_selector
  end

  def test_enumerator_nonempty
    @cp.add_block! 'body {color: black;}'

    assert_kind_of Enumerator, @cp.each_selector

    @cp.each_selector.each do |sel, desc, _spec|
      assert_equal 'body', sel
      assert_equal 'color: black;', desc
    end
  end

  def with_value_exception
    # Raise synthetic exception to test error handling because there is no known way to cause it naturally
    CssParser::RuleSet::Declarations::Value.stubs(:new).raises(ArgumentError.new('stub'))
    yield # TODO: do not pass a block instead
  end

  def test_catching_argument_exceptions_for_add_rule
    with_value_exception do
      cp_with_exceptions = Parser.new(rule_set_exceptions: true)
      assert_raises ArgumentError, 'stub' do
        cp_with_exceptions.add_rule!(selectors: 'body', block: 'background-color: blue')
      end

      cp_without_exceptions = Parser.new(rule_set_exceptions: false)
      cp_without_exceptions.add_rule!(selectors: 'body', block: 'background-color: blue')
    end
  end

  def test_catching_argument_exceptions_for_add_rule_positional
    with_value_exception do
      cp_with_exceptions = Parser.new(rule_set_exceptions: true)

      assert_raises ArgumentError, 'stub' do
        _, err = capture_io do
          cp_with_exceptions.add_rule!('body', 'background-color: blue')
        end
        assert_includes err, "DEPRECATION"
      end

      cp_without_exceptions = Parser.new(rule_set_exceptions: false)
      _, err = capture_io do
        cp_without_exceptions.add_rule!('body', 'background-color: blue')
      end
      assert_includes err, "DEPRECATION"
    end
  end

  def test_catching_argument_exceptions_for_add_rule_with_offsets
    with_value_exception do
      cp_with_exceptions = Parser.new(capture_offsets: true, rule_set_exceptions: true)

      assert_raises ArgumentError, 'stub' do
        _, err = capture_io do
          cp_with_exceptions.add_rule_with_offsets!('body', 'background-color: blue', 'inline', 1)
        end
        assert_includes err, "DEPRECATION"
      end

      cp_without_exceptions = Parser.new(capture_offsets: true, rule_set_exceptions: false)
      _, err = capture_io do
        cp_without_exceptions.add_rule_with_offsets!('body', 'background-color: blue', 'inline', 1)
      end
      assert_includes err, "DEPRECATION"
    end
  end
end


================================================
FILE: test/test_css_parser_offset_capture.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for the CssParser's loading functions.
class CssParserOffsetCaptureTests < Minitest::Test
  include CssParser

  def setup
    @cp = Parser.new
  end

  def test_capturing_offsets_for_local_file
    file_name = File.expand_path('fixtures/simple.css', __dir__)
    @cp.load_file!(file_name, capture_offsets: true)

    rules = @cp.find_rule_sets(['body', 'p'])

    # check that we found the body rule where we expected
    assert_equal 0, rules[0].offset.first
    assert_equal 43, rules[0].offset.last
    assert_equal file_name, rules[0].filename

    # and the p rule
    assert_equal 45, rules[1].offset.first
    assert_equal 63, rules[1].offset.last
    assert_equal file_name, rules[1].filename
  end

  # http://github.com/premailer/css_parser/issues#issue/4
  def test_capturing_offsets_from_remote_file
    # TODO: test SSL locally
    @cp.load_uri!("https://dialect.ca/inc/screen.css", capture_offsets: true)

    # there are a lot of rules in this file, but check some rule offsets
    rules = @cp.find_rule_sets(['#container', '#name_case_converter textarea'])
    assert_equal 2, rules.count

    assert_equal 2172, rules.first.offset.first
    assert_equal 2227, rules.first.offset.last
    assert_equal 'https://dialect.ca/inc/screen.css', rules.first.filename

    assert_equal 10_703, rules.last.offset.first
    assert_equal 10_752, rules.last.offset.last
    assert_equal 'https://dialect.ca/inc/screen.css', rules.last.filename
  end

  def test_capturing_offsets_from_string
    css = <<-CSS
      body { margin: 0px; }
      p { padding: 0px; }
      #content { font: 12px/normal sans-serif; }
      .content { color: red; }
    CSS
    @cp.load_string!(css, capture_offsets: true, filename: 'index.html')

    rules = @cp.find_rule_sets(['body', 'p', '#content', '.content'])
    assert_equal 4, rules.count

    assert_equal 6, rules[0].offset.first
    assert_equal 27, rules[0].offset.last
    assert_equal 'index.html', rules[0].filename

    assert_equal 34, rules[1].offset.first
    assert_equal 53, rules[1].offset.last
    assert_equal 'index.html', rules[1].filename

    assert_equal 60, rules[2].offset.first
    assert_equal 102, rules[2].offset.last
    assert_equal 'index.html', rules[2].filename

    assert_equal 109, rules[3].offset.first
    assert_equal 133, rules[3].offset.last
    assert_equal 'index.html', rules[3].filename
  end

  def test_capturing_offsets_with_imports
    base_dir = Pathname.new(__dir__).join('fixtures')
    @cp.load_file!('import1.css', base_dir: base_dir, capture_offsets: true)

    rules = @cp.find_rule_sets(['div', 'a', 'body', 'p'])

    # check that we found the div rule where we expected in the primary file
    assert_equal 'div', rules[0].selectors.join
    assert_equal 31, rules[0].offset.first
    assert_equal 51, rules[0].offset.last
    assert_equal base_dir.join('import1.css').to_s, rules[0].filename

    # check that the a rule in the first import is where we expect
    assert_equal 'a', rules[1].selectors.join
    assert_equal 26, rules[1].offset.first
    assert_equal 54, rules[1].offset.last
    assert_equal base_dir.join('subdir/import2.css').to_s, rules[1].filename

    # and the body rule in the second import
    assert_equal 'body', rules[2].selectors.join
    assert_equal 0, rules[2].offset.first
    assert_equal 43, rules[2].offset.last
    assert_equal base_dir.join('simple.css').to_s, rules[2].filename

    # as well as the p rule in the second import
    assert_equal 'p', rules[3].selectors.join
    assert_equal 45, rules[3].offset.first
    assert_equal 63, rules[3].offset.last
    assert_equal base_dir.join('simple.css').to_s, rules[3].filename
  end
end


================================================
FILE: test/test_css_parser_regexps.rb
================================================
# coding: iso-8859-1
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for CSS regular expressions
#
# see http://www.w3.org/TR/CSS21/syndata.html and
# http://www.w3.org/TR/CSS21/grammar.html
class CssParserRegexpTests < Minitest::Test
  def test_strings
    # complete matches
    [
      '"abcd"', '" A sd sédrcv \'dsf\' asd rfg asd"', '"A\ d??ef 123!"',
      "\"this is\\\n a test\"", '"back\67round"', '"r\000065 ed"',
      "'abcd'", "' A sd sedrcv \"dsf\" asd rf—&23$%#%$g asd'", "'A\\\n def 123!'",
      "'this is\\\n a test'", "'back\\67round'", "'r\\000065 ed'"
    ].each do |str|
      assert_equal str, str.match(CssParser::RE_STRING).to_s
    end

    test_string = "p { background: red url(\"url\\.'p'ng\"); }"
    assert_equal "\"url\\.'p'ng\"", test_string.match(CssParser::RE_STRING).to_s
  end

  def test_box_model_units
    %w[auto inherit 80px 90pt 80pc 80rem 80vh 70vm 60vw 1vmin 2vmax 0 2em 3ex 1cm 100mm 2in 120%].each do |str|
      assert_match(CssParser::BOX_MODEL_UNITS_RX, str)
    end
  end

  def test_unicode
    ['back\67round', 'r\000065 ed', '\00006C'].each do |str|
      assert_match(Regexp.new(CssParser::RE_UNICODE), str)
    end
  end

  def test_colour
    [
      'color: #fff', 'color:#f0a09c;', 'color: #04A', 'color: #04a9CE',
      'color: rgb(100, -10%, 300);', 'color: rgb(10,10,10)', 'color:rgb(12.7253%, -12%,0)',
      'color: hsla(-15, -77%, 19%, 5%);',
      'color: rgba(0,0,0,.1)', 'color: rgba(0, 0, 0, .5)', 'color: hsla(0, 0%, 0%, .05)',
      'color: black', 'color:Red;', 'color: AqUa;', 'color: blue   ', 'color: transparent',
      'color: darkslategray'
    ].each do |colour|
      assert_match(CssParser::RE_COLOUR, colour)
    end

    [
      'color: #fa', 'color:#f009c;', 'color: #04G', 'color: #04a9Cq',
      'color: rgb 100, -10%, 300;', 'color: rgb 10,10,10', 'color:rgb(12px, -12%,0)',
      'color:fuscia;', 'color: thick',
      'color:  alice_blue'
    ].each do |colour|
      refute_match(CssParser::RE_COLOUR, colour)
    end
  end

  def test_gradients
    [
      'linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',
      'linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)',
      '-o-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',
      '-moz-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',
      '-webkit-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',
      '-webkit-gradient(linear, left top, left bottom, color-stop(0, hsla(0, 0%, 0%, 0.00)), color-stop(1, hsla(0, 0%, 0%, 0.20)))',
      '-ms-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)'
    ].each do |grad|
      assert_match(CssParser::RE_GRADIENT, grad)
    end
  end

  def test_uris
    crazy_uri = 'http://www.example.com:80/~/redb%20all.png?test=test&test;test+test#test!'

    assert_equal "url('#{crazy_uri}')",
      "li { list-style: url('#{crazy_uri}') disc }".match(CssParser::RE_URI).to_s

    assert_equal "url(#{crazy_uri})",
      "li { list-style: url(#{crazy_uri}) disc }".match(CssParser::RE_URI).to_s

    assert_equal "url(\"#{crazy_uri}\")",
      "li { list-style: url(\"#{crazy_uri}\") disc }".match(CssParser::RE_URI).to_s
  end

  def test_important
    assert_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !important   ;")
    refute_match(CssParser::IMPORTANT_IN_PROPERTY_RX, "color: #f00 !importantish;")
  end

protected

  def load_test_file(filename)
    fh = File.new("fixtures/#{filename}", 'r')
    test_file = fh.read
    fh.close

    test_file
  end
end


================================================
FILE: test/test_helper.rb
================================================
# frozen_string_literal: true

require 'bundler/setup'
require 'maxitest/autorun'
require 'mocha/minitest'
require 'ostruct'
require 'net/http'
require 'webrick'
require 'css_parser'


================================================
FILE: test/test_merging.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

class MergingTests < Minitest::Test
  include CssParser

  def setup
    @cp = CssParser::Parser.new
  end

  def test_simple_merge
    rs1 = RuleSet.new(block: 'color: black;')
    rs2 = RuleSet.new(block: 'margin: 0px;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal '0px;', merged['margin']
    assert_equal 'black;', merged['color']
  end

  def test_merging_array
    rs1 = RuleSet.new(block: 'color: black;')
    rs2 = RuleSet.new(block: 'margin: 0px;')
    merged = CssParser.merge([rs1, rs2])
    assert_equal '0px;', merged['margin']
    assert_equal 'black;', merged['color']
  end

  def test_merging_with_compound_selectors
    @cp.add_block! "body { margin: 0; }"
    @cp.add_block! "h2   { margin: 5px; }"

    rules = @cp.find_rule_sets(["body", "h2"])
    assert_equal "margin: 5px;", CssParser.merge(rules).declarations_to_s

    @cp = CssParser::Parser.new
    @cp.add_block! "body { margin: 0; }"
    @cp.add_block! "h2,h1 { margin: 5px; }"

    rules = @cp.find_rule_sets(["body", "h2"])
    assert_equal "margin: 5px;", CssParser.merge(rules).declarations_to_s
  end

  def test_merging_multiple
    rs1 = RuleSet.new(block: 'color: black;')
    rs2 = RuleSet.new(block: 'margin: 0px;')
    rs3 = RuleSet.new(block: 'margin: 5px;')
    merged = CssParser.merge(rs1, rs2, rs3)
    assert_equal '5px;', merged['margin']
  end

  def test_multiple_selectors_should_have_proper_specificity
    rs1 = RuleSet.new(selectors: 'p, a[rel="external"]', block: 'color: black;')
    rs2 = RuleSet.new(selectors: 'a', block: 'color: blue;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'black;', merged['color']
  end

  def test_setting_specificity
    rs1 = RuleSet.new(block: 'color: red;', specificity: 20)
    rs2 = RuleSet.new(block: 'color: blue;', specificity: 10)
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'red;', merged['color']
  end

  def test_properties_should_be_case_insensitive
    rs1 = RuleSet.new(block: ' CoLor   : red  ;', specificity: 20)
    rs2 = RuleSet.new(block: 'color: blue;', specificity: 10)
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'red;', merged['color']
  end

  def test_merging_backgrounds
    rs1 = RuleSet.new(block: 'background-color: black;')
    rs2 = RuleSet.new(block: 'background-image: none;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'black none;', merged['background']
  end

  def test_merging_dimensions
    rs1 = RuleSet.new(block: 'margin: 3em;')
    rs2 = RuleSet.new(block: 'margin-left: 1em;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal '3em 3em 3em 1em;', merged['margin']
  end

  def test_merging_fonts
    rs1 = RuleSet.new(block: 'font: 11px Arial;')
    rs2 = RuleSet.new(block: 'font-weight: bold;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'bold 11px Arial;', merged['font']
  end

  def test_raising_error_on_bad_type
    assert_raises ArgumentError do
      CssParser.merge([1, 2, 3])
    end
  end

  def test_returning_early_with_only_one_params
    rs = RuleSet.new(block: 'font-weight: bold;')
    merged = CssParser.merge(rs)
    assert_equal rs.object_id, merged.object_id
  end

  def test_merging_important
    rs1 = RuleSet.new(block: 'color: black !important;')
    rs2 = RuleSet.new(block: 'color: red;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'black !important;', merged['color']
  end

  def test_prioritising_important_over_non_important_in_the_same_block
    rs1 = RuleSet.new(block: 'color: black !important; color: red;')
    merged = CssParser.merge(rs1)
    assert_equal 'black !important;', merged['color']
  end

  def test_prioritising_two_important_declarations_in_the_same_block
    rs1 = RuleSet.new(block: 'color: black !important; color: red !important;')
    merged = CssParser.merge(rs1)
    assert_equal 'red !important;', merged['color']
  end

  def test_merging_multiple_important
    rs1 = RuleSet.new(block: 'color: black !important;', specificity: 1000)
    rs2 = RuleSet.new(block: 'color: red !important;', specificity: 1)
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'black !important;', merged['color']

    rs3 = RuleSet.new(block: 'color: blue !important;', specificity: 1000)
    merged = CssParser.merge(rs1, rs2, rs3)
    assert_equal 'blue !important;', merged['color']
  end

  def test_merging_shorthand_important
    rs1 = RuleSet.new(block: 'background: black none !important;')
    rs2 = RuleSet.new(block: 'background-color: red;')
    merged = CssParser.merge(rs1, rs2)
    assert_equal 'black !important;', merged['background-color']
  end
end


================================================
FILE: test/test_rule_set.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for parsing CSS blocks
class RuleSetTests < Minitest::Test
  include CssParser

  def setup
    @cp = Parser.new
  end

  def test_setting_property_values
    rs = RuleSet.new

    rs['background-color'] = 'red'
    assert_equal('red;', rs['background-color'])

    rs['background-color'] = 'blue !important;'
    assert_equal('blue !important;', rs['background-color'])
  end

  def test_getting_property_values
    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')
    assert_equal('#fff;', rs['color'])
  end

  def test_getting_property_value_ignoring_case
    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')
    assert_equal('#fff;', rs['  ColoR '])
  end

  def test_each_selector
    expected = [
      {selector: "#content p", declarations: "color: #fff;", specificity: 101},
      {selector: "a", declarations: "color: #fff;", specificity: 1}
    ]

    actual = []
    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')
    rs.each_selector do |sel, decs, spec|
      actual << {selector: sel, declarations: decs, specificity: spec}
    end

    assert_equal(expected, actual)
  end

  def test_each_declaration
    expected = Set[
      {property: 'margin', value: '1px -0.25em', is_important: false},
      {property: 'background', value: 'white none no-repeat', is_important: true},
      {property: 'color', value: '#fff', is_important: false}
    ]

    actual = Set.new
    rs = RuleSet.new(block: 'color: #fff; Background: white none no-repeat !important; margin: 1px -0.25em;')
    rs.each_declaration do |prop, val, imp|
      actual << {property: prop, value: val, is_important: imp}
    end

    assert_equal(expected, actual)
  end

  def test_each_declaration_respects_order
    css_fragment = "margin: 0; padding: 20px; margin-bottom: 28px;"
    rs           = RuleSet.new(block: css_fragment)
    expected     = %w[margin padding margin-bottom]
    actual       = []
    rs.each_declaration { |prop, _val, _imp| actual << prop }
    assert_equal(expected, actual)
  end

  def test_each_declaration_containing_semicolons
    rs = RuleSet.new(block: "background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);" \
                            "background-repeat: no-repeat")
    assert_equal('url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);', rs['background-image'])
    assert_equal('no-repeat;', rs['background-repeat'])
  end

  def test_each_declaration_with_newlines
    expected = Set[
      {property: 'background-image', value: 'url(foo;bar)', is_important: false},
      {property: 'font-weight', value: 'bold', is_important: true}
    ]
    rs = RuleSet.new(block: "background-image\n:\nurl(foo;bar);\n\n\n\n\n;;font-weight\n\n\n:bold\n\n\n!important")
    actual = Set.new
    rs.each_declaration do |prop, val, imp|
      actual << {property: prop, value: val, is_important: imp}
    end
    assert_equal(expected, actual)
  end

  def test_selector_sanitization
    selectors = "h1, h2,\nh3 "
    rs = RuleSet.new(selectors: selectors, block: "color: #fff;")
    assert rs.selectors.member?("h3")
  end

  def test_multiple_selectors_to_s
    selectors = "#content p, a"
    rs = RuleSet.new(selectors: selectors, block: "color: #fff;")
    assert_match(/^\s*#content p,\s*a\s*\{/, rs.to_s)
  end

  def test_declarations_to_s
    declarations = 'color: #fff; font-weight: bold;'
    rs = RuleSet.new(selectors: '#content p, a', block: declarations)
    assert_equal(declarations.split.sort, rs.declarations_to_s.split.sort)
  end

  def test_important_declarations_to_s
    declarations = 'color: #fff; font-weight: bold !important;'
    rs = RuleSet.new(selectors: '#content p, a', block: declarations)
    assert_equal(declarations.split.sort, rs.declarations_to_s.split.sort)
  end

  def test_overriding_specificity
    rs = RuleSet.new(selectors: '#content p, a', block: 'color: white', specificity: 1000)
    rs.each_selector do |_sel, _decs, spec|
      assert_equal 1000, spec
    end
  end

  def test_important_without_value
    declarations = 'color: !important; background-color: #fff'
    rs = RuleSet.new(selectors: '#content p, a', block: declarations)
    assert_equal('background-color: #fff;', rs.declarations_to_s)
  end

  def test_not_raised_issue68
    ok = true
    begin
      RuleSet.new(selectors: 'td', block: 'border-top: 5px solid; border-color: #fffff0;')
    rescue
      ok = false
    end
    assert_equal true, ok
  end
end


================================================
FILE: test/test_rule_set_creating_shorthand.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

# Test cases for reading and generating CSS shorthand properties
class RuleSetCreatingShorthandTests < Minitest::Test
  include CssParser

  def setup
    @cp = CssParser::Parser.new
  end

  def test_border_width
    combined = create_shorthand('border-width': '1px')

    assert_equal '', combined['border']
    assert_equal '1px;', combined['border-width']
  end

  def test_border_width_with_border_color_with_spaces
    combined = create_shorthand(
      'border-width': '1px',
      'border-color': 'rgb(0 0 0 / 1)',
      'border-style': 'solid'
    )

    assert_equal '', combined['border']
    assert_equal '1px;', combined['border-width']
    assert_equal 'rgb(0 0 0 / 1);', combined['border-color']
  end

  # Border shorthand
  def test_combining_borders_into_shorthand
    properties = {
      'border-top-width' => 'auto',
      'border-right-width' => 'thin',
      'border-bottom-width' => 'auto',
      'border-left-width' => '0px'
    }

    combined = create_shorthand(properties)

    assert_equal('', combined['border'])
    assert_equal('auto thin auto 0px;', combined['border-width'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)

    # should not combine if any properties are missing
    properties.delete('border-top-width')

    combined = create_shorthand(properties)

    assert_equal '', combined['border-width']

    properties = {
      'border-width' => '22%',
      'border-color' => 'rgba(255, 0, 0)',
      'border-style' => 'solid'
    }
    combined = create_shorthand(properties)
    assert_equal '22% solid rgba(255, 0, 0);', combined['border']
    assert_equal '', combined['border-width']
    assert_equal '', combined['border-color']
    assert_equal '', combined['border-style']

    properties = {
      'border-top-style' => 'none',
      'border-right-style' => 'none',
      'border-bottom-style' => 'none',
      'border-left-style' => 'none'
    }
    combined = create_shorthand(properties)
    assert_equal '', combined['border']
    assert_equal 'none;', combined['border-style']

    properties = {
      'border-top-color' => '#bada55',
      'border-right-color' => '#000000',
      'border-bottom-color' => '#ffffff',
      'border-left-color' => '#ff0000'
    }
    combined = create_shorthand(properties)
    assert_equal '#bada55 #000000 #ffffff #ff0000;', combined['border-color']
  end

  # Dimensions shorthand
  def test_combining_dimensions_into_shorthand
    properties = {
      'margin-right' => 'auto', 'margin-bottom' => '0px', 'margin-left' => 'auto', 'margin-top' => '0px',
      'padding-right' => '1.25em', 'padding-bottom' => '11%', 'padding-left' => '3pc', 'padding-top' => '11.25ex'
    }

    combined = create_shorthand(properties)

    assert_equal('0px auto;', combined['margin'])
    assert_equal('11.25ex 1.25em 11% 3pc;', combined['padding'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)

    # should not combine if any properties are missing
    properties.delete('margin-right')
    properties.delete('padding-right')

    combined = create_shorthand(properties)

    assert_equal '', combined['margin']
    assert_equal '', combined['padding']
  end

  # Dimensions shorthand, auto property
  def test_combining_dimensions_into_shorthand_with_auto
    rs = RuleSet.new(selectors: '#page', block: "margin: 0; margin-left: auto; margin-right: auto;")
    rs.expand_shorthand!
    assert_equal('auto;', rs['margin-left'])
    rs.create_shorthand!
    assert_equal('0 auto;', rs['margin'])
  end

  # Font shorthand
  def test_combining_font_into_shorthand
    # should combine if all font properties are present
    properties = {
      "font-weight" => "300", "font-size" => "12pt",
      "font-family" => "sans-serif", "line-height" => "18px",
      "font-style" => "oblique", "font-variant" => "small-caps"
    }

    combined = create_shorthand(properties)
    assert_equal('oblique small-caps 300 12pt/18px sans-serif;', combined['font'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)

    # should not combine if any properties are missing
    properties.delete('font-weight')
    combined = create_shorthand(properties)
    assert_equal '', combined['font']
  end

  # Background shorthand
  def test_combining_background_into_shorthand
    properties = {
      'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray',
      'background-position' => 'center -10.2%', 'background-attachment' => 'fixed',
      'background-repeat' => 'no-repeat'
    }

    combined = create_shorthand(properties)

    assert_equal('gray url(\'chess.png\') no-repeat center -10.2% fixed;', combined['background'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)
  end

  def test_combining_background_with_size_into_shorthand
    properties = {
      'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray',
      'background-position' => 'center -10.2%', 'background-attachment' => 'fixed',
      'background-repeat' => 'no-repeat', 'background-size' => '50% 100%'
    }

    combined = create_shorthand(properties)

    assert_equal('gray url(\'chess.png\') no-repeat center -10.2% / 50% 100% fixed;', combined['background'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)
  end

  def test_combining_background_with_size_and_no_position_into_shorthand
    properties = {
      'background-image' => 'url(\'chess.png\')', 'background-color' => 'gray',
      'background-attachment' => 'fixed', 'background-repeat' => 'no-repeat',
      'background-size' => '50% 100%'
    }

    combined = create_shorthand(properties)

    assert_equal('gray url(\'chess.png\') no-repeat 0% 0% / 50% 100% fixed;', combined['background'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)
  end

  # List-style shorthand
  def test_combining_list_style_into_shorthand
    properties = {
      'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana',
      'list-style-position' => 'inside'
    }

    combined = create_shorthand(properties)

    assert_equal('katakana inside url(\'chess.png\');', combined['list-style'])

    # after creating shorthand, all long-hand properties should be deleted
    assert_properties_are_deleted(combined, properties)
  end

  def test_property_values_in_url
    rs = RuleSet.new(
      selectors: '#header',
      block: "background:url(http://example.com/1528/www/top-logo.jpg) no-repeat top right; " \
             "padding: 79px 0 10px 0;  text-align:left;"
    )
    rs.expand_shorthand!
    assert_equal('top right;', rs['background-position'])
    rs.create_shorthand!
    assert_equal('url(http://example.com/1528/www/top-logo.jpg) no-repeat top right;', rs['background'])
  end

  def test_a_single_property_is_not_shorted
    properties = {'background-color' => 'gray'}
    combined = create_shorthand(properties)

    assert_equal('gray;', combined['background-color'])
    assert_equal('', combined['background'])
  end

protected

  def assert_properties_are_deleted(ruleset, properties)
    properties.each_key do |property|
      assert_equal '', ruleset[property]
    end
  end

  def create_shorthand(properties)
    ruleset = RuleSet.new(nil, nil)
    properties.each do |property, value|
      ruleset[property] = value
    end
    ruleset.create_shorthand!
    ruleset
  end
end


================================================
FILE: test/test_rule_set_expanding_shorthand.rb
================================================
# frozen_string_literal: true

require_relative 'test_helper'

class RuleSetExpandingShorthandTests < Minitest::Test
  include CssParser

  def setup
    @cp = CssParser::Parser.new
  end

  # Dimensions shorthand
  def test_expanding_border_shorthand
    declarations = expand_declarations('border: none')
    assert_equal 'none', declarations['border-right-style']

    declarations = expand_declarations('border: 1px solid red')
    assert_equal '1px', declarations['border-top-width']
    assert_equal 'solid', declarations['border-bottom-style']

    # Regression: rgba/hsla with no leading zero on the alpha (e.g. `.1`) used
    # to fail the colour regex, causing border-*-color to be silently dropped
    # during shorthand expansion.
    declarations = expand_declarations('border: 1px solid rgba(0,0,0,.1)')
    assert_equal '1px', declarations['border-top-width']
    assert_equal 'solid', declarations['border-top-style']
    assert_equal 'rgba(0,0,0,.1)', declarations['border-top-color']
    assert_equal 'rgba(0,0,0,.1)', declarations['border-right-color']
    assert_equal 'rgba(0,0,0,.1)', declarations['border-bottom-color']
    assert_equal 'rgba(0,0,0,.1)', declarations['border-left-color']

    declarations = expand_declarations('border-color: red hsla(255, 0, 0, 5) rgb(2% ,2%,2%)')
    assert_equal 'red', declarations['border-top-color']
    assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color']
    assert_equal 'hsla(255,0,0,5)', declarations['border-left-color']

    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff #ff0000')

    assert_equal '#000000', declarations['border-top-color']
    assert_equal '#bada55', declarations['border-right-color']
    assert_equal '#ffffff', declarations['border-bottom-color']
    assert_equal '#ff0000', declarations['border-left-color']

    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff')

    assert_equal '#000000', declarations['border-top-color']
    assert_equal '#bada55', declarations['border-right-color']
    assert_equal '#ffffff', declarations['border-bottom-color']
    assert_equal '#bada55', declarations['border-left-color']

    declarations = expand_declarations('border-color: #000000 #bada55')

    assert_equal '#000000', declarations['border-top-color']
    assert_equal '#bada55', declarations['border-right-color']
    assert_equal '#000000', declarations['border-bottom-color']
    assert_equal '#bada55', declarations['border-left-color']

    declarations = expand_declarations('border: thin dot-dot-dash')
    assert_equal 'dot-dot-dash', declarations['border-left-style']
    assert_equal 'thin', declarations['border-left-width']
    assert_nil declarations['border-left-color']
  end

  # Dimensions shorthand
  def test_getting_dimensions_from_shorthand
    # test various shorthand forms
    ['margin: 0px auto', 'margin: 0px auto 0px', 'margin: 0px auto 0px'].each do |shorthand|
      declarations = expand_declarations(shorthand)
      assert_equal({"margin-right" => "auto", "margin-bottom" => "0px", "margin-left" => "auto", "margin-top" => "0px"}, declarations)
    end

    # test various units
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "margin: 0% -0.123#{unit} 9px -.9pc"
      declarations = expand_declarations(shorthand)
      assert_equal({"margin-right" => "-0.123#{unit}", "margin-bottom" => "9px", "margin-left" => "-.9pc", "margin-top" => "0%"}, declarations)
    end
  end

  # Font shorthand
  def test_getting_font_size_from_shorthand
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "font: 300 italic 11.25#{unit}/14px verdana, helvetica, sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal("11.25#{unit}", declarations['font-size'])
    end

    ['smaller', 'small', 'medium', 'large', 'x-large', 'auto'].each do |unit|
      shorthand = "font: 300 italic #{unit}/14px verdana, helvetica, sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal(unit, declarations['font-size'])
    end
  end

  def test_getting_font_families_from_shorthand
    shorthand = "font: 300 italic 12px/14px \"Helvetica-Neue-Light 45\", 'verdana', helvetica, sans-serif;"
    declarations = expand_declarations(shorthand)
    assert_equal("\"Helvetica-Neue-Light 45\", 'verdana', helvetica, sans-serif", declarations['font-family'])
  end

  def test_getting_font_weight_from_shorthand
    ['300', 'bold', 'bolder', 'lighter', 'normal'].each do |unit|
      shorthand = "font: #{unit} italic 12px sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal(unit, declarations['font-weight'])
    end

    # ensure normal is the default state
    ['font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',
     'font: small-caps normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand|
      declarations = expand_declarations(shorthand)
      assert_equal('normal', declarations['font-weight'], shorthand)
    end
  end

  def test_getting_font_variant_from_shorthand
    shorthand = "font: small-caps italic 12px sans-serif;"
    declarations = expand_declarations(shorthand)
    assert_equal('small-caps', declarations['font-variant'])
  end

  def test_getting_font_variant_from_shorthand_ensure_normal_is_the_default_state
    [
      'font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',
      'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'
    ].each do |shorthand|
      declarations = expand_declarations(shorthand)
      assert_equal('normal', declarations['font-variant'], shorthand)
    end
  end

  def test_getting_font_style_from_shorthand
    ['italic', 'oblique'].each do |unit|
      shorthand = "font: normal #{unit} bold 12px sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal(unit, declarations['font-style'])
    end

    # ensure normal is the default state
    ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;',
     'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand|
      declarations = expand_declarations(shorthand)
      assert_equal('normal', declarations['font-style'], shorthand)
    end
  end

  def test_getting_line_height_from_shorthand
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "font: 300 italic 12px/0.25#{unit} verdana, helvetica, sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal("0.25#{unit}", declarations['line-height'])
    end

    # ensure normal is the default state
    ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;',
     'font: normal 12px sans-serif;', 'font: 12px sans-serif;'].each do |shorthand|
      declarations = expand_declarations(shorthand)
      assert_equal('normal', declarations['line-height'], shorthand)
    end
  end

  def test_getting_line_height_from_shorthand_with_spaces
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "font: 300 italic 12px/ 0.25#{unit} verdana, helvetica, sans-serif;"
      declarations = expand_declarations(shorthand)
      assert_equal("0.25#{unit}", declarations['line-height'])
    end
  end

  # Background shorthand
  def test_getting_background_properties_from_shorthand
    expected = {
      "background-image" => "url('chess.png')", "background-color" => "gray", "background-repeat" => "repeat",
      "background-attachment" => "fixed", "background-position" => "50%"
    }

    shorthand = "background: url('chess.png') gray 50% repeat fixed;"
    declarations = expand_declarations(shorthand)
    assert_equal expected, declarations
  end

  def test_getting_background_position_from_shorthand
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "background: url('chess.png') gray 30% -0.15#{unit} repeat fixed;"
      declarations = expand_declarations(shorthand)
      assert_equal("30% -0.15#{unit}", declarations['background-position'])
    end

    ['left', 'center', 'right', 'top', 'bottom', 'inherit'].each do |position|
      shorthand = "background: url('chess.png') #000fff #{position} no-repeat fixed;"
      declarations = expand_declarations(shorthand)
      assert_equal(position, declarations['background-position'])
    end
  end

  def test_getting_background_size_from_shorthand
    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|
      shorthand = "background: url('chess.png') gray 30% -0.20/-0.15#{unit} auto repeat fixed;"
      declarations = expand_declarations(shorthand)
      assert_equal("-0.15#{unit} auto", declarations['background-size'])
    end

    ['cover', 'contain', 'auto', 'initial', 'inherit'].each do |size|
      shorthand = "background: url('chess.png') #000fff 0% 50% / #{size} no-repeat fixed;"
      declarations = expand_declarations(shorthand)
      assert_equal(size, declarations['background-size'])
    end
  end

  def test_getting_background_colour_from_shorthand
    [
      'blue', 'lime', 'rgb(10,10,10)', 'rgb (  -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit',
      # Regression: alpha without a leading zero (e.g. `.1`) used to fail the
      # colour regex and silently drop background-color.
      'rgba(0,0,0,.1)'
    ].each do |colour|
      shorthand = "background:#{colour} url('chess.png') center repeat fixed ;"
      declarations = expand_declarations(shorthand)
      assert_equal(colour, declarations['background-color'])
    end
  end

  def test_getting_background_attachment_from_shorthand
    ['scroll', 'fixed', 'inherit'].each do |attachment|
      shorthand = "background:#0f0f0f url('chess.png') center repeat #{attachment};"
      declarations = expand_declarations(shorthand)
      assert_equal(attachment, declarations['background-attachment'])
    end
  end

  def test_getting_background_repeat_from_shorthand
    ['repeat-x', 'repeat-y', 'no-repeat', 'inherit'].each do |repeat|
      shorthand = "background:#0f0f0f none #{repeat};"
      declarations = expand_declarations(shorthand)
      assert_equal(repeat, declarations['background-repeat'])
    end
  end

  def test_getting_background_image_from_shorthand
    ['url("chess.png")', 'url("https://example.org:80/~files/chess.png?123=abc&test#5")',
     'url(https://example.org:80/~files/chess.png?123=abc&test#5)',
     "url('https://example.org:80/~files/chess.png?123=abc&test#5')", 'none', 'inherit'].each do |image|
      shorthand = "background: #0f0f0f #{image} ;"
      declarations = expand_declarations(shorthand)
      assert_equal(image, declarations['background-image'])
    end
  end

  def test_getting_background_gradient_from_shorthand
    ['linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)',
     '-webkit-gradient(linear, left top, left bottom, color-stop(0, hsla(0, 0%, 0%, 0.00)), color-stop(1, hsla(0, 0%, 0%, 0.20)))',
     '-moz-linear-gradient(bottom, blue, red)'].each do |image|
      shorthand = "background: #0f0f0f #{image} repeat ;"
      declarations = expand_declarations(shorthand)
      assert_equal(image, declarations['background-image'])
    end
  end

  # List-style shorthand
  def test_getting_list_style_properties_from_shorthand
    expected = {
      'list-style-image' => 'url(\'chess.png\')', 'list-style-type' => 'katakana',
      'list-style-position' => 'inside'
    }

    shorthand = "list-style: katakana inside url('chess.png');"
    declarations = expand_declarations(shorthand)
    assert_equal expected, declarations
  end

  def test_getting_list_style_position_from_shorthand
    ['inside', 'outside'].each do |position|
      shorthand = "list-style: katakana #{position} url('chess.png');"
      declarations = expand_declarations(shorthand)
      assert_equal(position, declarations['list-style-position'])
    end
  end

  def test_getting_list_style_type_from_shorthand
    ['disc', 'circle', 'square', 'decimal', 'decimal-leading-zero', 'lower-roman', 'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha', 'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana', 'katakana', 'hira-gana-iroha', 'katakana-iroha', 'none'].each do |type|
      shorthand = "list-style: #{type} inside url('chess.png');"
      declarations = expand_declarations(shorthand)
      assert_equal(type, declarations['list-style-type'])
    end
  end

  def test_expanding_shorthand_with_replaced_properties_after
    shorthand = 'line-height: 0.25px !important; font-style: normal; font: small-caps italic 12px sans-serif; font-size: 12em;'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'line-height' => '0.25px',
      'font-style' => 'italic',
      'font-variant' => 'small-caps',
      'font-weight' => 'normal',
      'font-family' => 'sans-serif',
      'font-size' => '12em'
    }
    assert_equal expected_declarations, declarations
  end

  def test_expanding_important_shorthand_with_replaced_properties
    shorthand = 'line-height: 0.25px !important; font-style: normal; font: small-caps italic 12px sans-serif !important; font-size: 12em; font-family: emoji !important;'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'font-style' => 'italic',
      'font-variant' => 'small-caps',
      'font-weight' => 'normal',
      'line-height' => 'normal',
      'font-family' => 'emoji',
      'font-size' => '12px'
    }
    assert_equal expected_declarations, declarations
  end

  def test_functions_with_many_spaces
    shorthand = 'margin: calc(1em / 4 * var(--foo));'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'margin-top' => 'calc(1em / 4 * var(--foo))',
      'margin-bottom' => 'calc(1em / 4 * var(--foo))',
      'margin-left' => 'calc(1em / 4 * var(--foo))',
      'margin-right' => 'calc(1em / 4 * var(--foo))'
    }
    assert_equal expected_declarations, declarations
  end

  def test_functions_with_no_spaces
    shorthand = 'margin: calc(1em/4*4);'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'margin-top' => 'calc(1em/4*4)',
      'margin-bottom' => 'calc(1em/4*4)',
      'margin-left' => 'calc(1em/4*4)',
      'margin-right' => 'calc(1em/4*4)'
    }
    assert_equal expected_declarations, declarations
  end

  def test_functions_with_one_space
    shorthand = 'margin: calc(1em /4);'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'margin-top' => 'calc(1em /4)',
      'margin-bottom' => 'calc(1em /4)',
      'margin-left' => 'calc(1em /4)',
      'margin-right' => 'calc(1em /4)'
    }
    assert_equal expected_declarations, declarations
  end

  def test_functions_with_commas
    shorthand = 'margin: clamp(1rem, 2.5vw, 2rem)'
    declarations = expand_declarations(shorthand)
    expected_declarations = {
      'margin-top' => 'clamp(1rem, 2.5vw, 2rem)',
      'margin-bottom' => 'clamp(1rem, 2.5vw, 2rem)',
      'margin-left' => 'clamp(1rem, 2.5vw, 2rem)',
      'margin-right' => 'clamp(1rem, 2.5vw, 2rem)'
    }
    assert_equal expected_declarations, declarations
  end

protected

  def expand_declarations(declarations)
    ruleset = RuleSet.new(block: declarations)
    ruleset.expand_shorthand!

    collected = {}
    ruleset.each_declaration do |prop, val, _imp|
      collected[prop.to_s] = val.to_s
    end
    collected
  end
end
Download .txt
gitextract_kcgfa2ip/

├── .editorconfig
├── .github/
│   ├── PULL_REQUEST_TEMPLATE.md
│   └── workflows/
│       └── ci.yml
├── .gitignore
├── .jrubyrc
├── .rubocop.yml
├── .vscode/
│   └── tasks.json
├── CHANGELOG.md
├── Gemfile
├── MIT-LICENSE
├── README.md
├── Rakefile
├── css_parser.gemspec
├── lib/
│   ├── css_parser/
│   │   ├── parser.rb
│   │   ├── regexps.rb
│   │   ├── rule_set.rb
│   │   └── version.rb
│   └── css_parser.rb
└── test/
    ├── fixtures/
    │   ├── complex.css
    │   ├── import-circular-reference.css
    │   ├── import-malformed.css
    │   ├── import-with-media-types.css
    │   ├── import1.css
    │   ├── simple.css
    │   └── subdir/
    │       └── import2.css
    ├── rule_set/
    │   ├── declarations/
    │   │   └── test_value.rb
    │   └── test_declarations.rb
    ├── test_css_parser_basic.rb
    ├── test_css_parser_loading.rb
    ├── test_css_parser_media_types.rb
    ├── test_css_parser_misc.rb
    ├── test_css_parser_offset_capture.rb
    ├── test_css_parser_regexps.rb
    ├── test_helper.rb
    ├── test_merging.rb
    ├── test_rule_set.rb
    ├── test_rule_set_creating_shorthand.rb
    └── test_rule_set_expanding_shorthand.rb
Download .txt
SYMBOL INDEX (247 symbols across 17 files)

FILE: lib/css_parser.rb
  type CssParser (line 15) | module CssParser
    function merge (line 54) | def self.merge(*rule_sets)
    function calculate_specificity (line 109) | def self.calculate_specificity(selector)
    function convert_uris (line 133) | def self.convert_uris(css, base_uri)
    function sanitize_media_query (line 150) | def self.sanitize_media_query(raw)

FILE: lib/css_parser/parser.rb
  type CssParser (line 5) | module CssParser
    class RemoteFileError (line 7) | class RemoteFileError < IOError; end
    class CircularReferenceError (line 10) | class CircularReferenceError < StandardError; end
    class Parser (line 20) | class Parser
      method initialize (line 34) | def initialize(options = {})
      method find_by_selector (line 72) | def find_by_selector(selector, media_types = :all)
      method find_rule_sets (line 82) | def find_rule_sets(selectors, media_types = :all)
      method add_block! (line 117) | def add_block!(block, options = {})
      method add_rule! (line 172) | def add_rule!(*args, selectors: nil, block: nil, filename: nil, offs...
      method add_rule_with_offsets! (line 209) | def add_rule_with_offsets!(selectors, declarations, filename, offset...
      method add_rule_set! (line 220) | def add_rule_set!(ruleset, media_types = :all)
      method remove_rule_set! (line 232) | def remove_rule_set!(ruleset, media_types = :all)
      method each_rule_set (line 245) | def each_rule_set(media_types = :all) # :yields: rule_set, media_types
      method to_h (line 257) | def to_h(which_media = :all)
      method each_selector (line 281) | def each_selector(all_media_types = :all, options = {}) # :yields: s...
      method to_s (line 292) | def to_s(which_media = :all)
      method rules_by_media_query (line 323) | def rules_by_media_query
      method compact! (line 338) | def compact! # :nodoc:
      method parse_block_into_rule_sets! (line 342) | def parse_block_into_rule_sets!(block, options = {}) # :nodoc:
      method load_uri! (line 489) | def load_uri!(uri, options = {}, deprecated = nil)
      method load_file! (line 519) | def load_file!(file_name, options = {}, deprecated = nil)
      method load_string! (line 544) | def load_string!(src, options = {}, deprecated = nil)
      method circular_reference_check (line 566) | def circular_reference_check(path) # rubocop:disable Naming/Predicat...
      method ignore_pattern (line 581) | def ignore_pattern(css, regex, options)
      method cleanup_block (line 592) | def cleanup_block(block, options = {}) # :nodoc:
      method read_remote_file (line 613) | def read_remote_file(uri) # :nodoc:
      method save_folded_declaration (line 692) | def save_folded_declaration(block_hash, folded_declaration) # :nodoc:
      method get_folded_declaration (line 697) | def get_folded_declaration(block_hash) # :nodoc:
      method reset! (line 701) | def reset! # :nodoc:
      method css_node_to_h (line 710) | def css_node_to_h(hash, key, val)

FILE: lib/css_parser/regexps.rb
  type CssParser (line 3) | module CssParser
    function regex_possible_values (line 4) | def self.regex_possible_values(*values)

FILE: lib/css_parser/rule_set.rb
  type CssParser (line 5) | module CssParser
    class RuleSet (line 6) | class RuleSet
      class Declarations (line 37) | class Declarations
        class Value (line 38) | class Value
          method initialize (line 42) | def initialize(value, important: nil)
          method value= (line 47) | def value=(value)
          method to_s (line 56) | def to_s
          method == (line 60) | def ==(other)
        method initialize (line 71) | def initialize(declarations = {})
        method []= (line 95) | def []=(property, value)
        method [] (line 112) | def [](property)
        method key? (line 117) | def key?(property)
        method size (line 121) | def size
        method delete (line 130) | def delete(property)
        method replace_declaration! (line 148) | def replace_declaration!(property, replacements, preserve_importan...
        method to_s (line 203) | def to_s(options = {})
        method == (line 214) | def ==(other)
        method normalize_property (line 228) | def normalize_property(property)
      method initialize (line 257) | def initialize(*args, selectors: nil, block: nil, offset: nil, filen...
      method get_value (line 294) | def get_value(property)
      method each_selector (line 310) | def each_selector(options = {}) # :yields: selector, declarations, s...
      method each_declaration (line 320) | def each_declaration # :yields: property, value, is_important
      method declarations_to_s (line 327) | def declarations_to_s(options = {})
      method to_s (line 332) | def to_s
      method expand_shorthand! (line 337) | def expand_shorthand!
      method expand_background_shorthand! (line 350) | def expand_background_shorthand! # :nodoc:
      method extract_background_size_from (line 372) | def extract_background_size_from(value)
      method expand_border_shorthand! (line 380) | def expand_border_shorthand! # :nodoc:
      method expand_dimensions_shorthand! (line 398) | def expand_dimensions_shorthand! # :nodoc:
      method expand_font_shorthand! (line 435) | def expand_font_shorthand! # :nodoc:
      method expand_list_style_shorthand! (line 490) | def expand_list_style_shorthand! # :nodoc:
      method create_shorthand! (line 510) | def create_shorthand!
      method create_shorthand_properties! (line 520) | def create_shorthand_properties!(properties, shorthand_property) # :...
      method create_background_shorthand! (line 544) | def create_background_shorthand! # :nodoc:
      method create_border_shorthand! (line 561) | def create_border_shorthand! # :nodoc:
      method create_dimensions_shorthand! (line 583) | def create_dimensions_shorthand! # :nodoc:
      method create_font_shorthand! (line 607) | def create_font_shorthand! # :nodoc:
      method create_list_style_shorthand! (line 634) | def create_list_style_shorthand! # :nodoc:
      method compute_dimensions_shorthand (line 642) | def compute_dimensions_shorthand(values)
      method parse_declarations! (line 655) | def parse_declarations!(block) # :nodoc:
      method unmatched_open_parenthesis? (line 683) | def unmatched_open_parenthesis?(declarations)
      method parse_selectors! (line 690) | def parse_selectors!(selectors) # :nodoc:
      method split_value_preserving_function_whitespace (line 698) | def split_value_preserving_function_whitespace(value)

FILE: lib/css_parser/version.rb
  type CssParser (line 3) | module CssParser

FILE: test/rule_set/declarations/test_value.rb
  class RuleSetProperyTest (line 7) | class RuleSetProperyTest < Minitest::Test

FILE: test/rule_set/test_declarations.rb
  class RuleSetDeclarationsTest (line 5) | class RuleSetDeclarationsTest < Minitest::Test

FILE: test/test_css_parser_basic.rb
  class CssParserBasicTests (line 6) | class CssParserBasicTests < Minitest::Test
    method setup (line 9) | def setup
    method test_finding_by_selector (line 19) | def test_finding_by_selector
    method test_adding_block (line 27) | def test_adding_block
    method test_adding_block_without_closing_brace (line 32) | def test_adding_block_without_closing_brace
    method test_adding_a_rule (line 37) | def test_adding_a_rule
    method test_adding_a_rule_set (line 42) | def test_adding_a_rule_set
    method test_removing_a_rule_set (line 48) | def test_removing_a_rule_set
    method test_toggling_uri_conversion (line 56) | def test_toggling_uri_conversion
    method test_converting_to_hash (line 74) | def test_converting_to_hash

FILE: test/test_css_parser_loading.rb
  class CssParserLoadingTests (line 6) | class CssParserLoadingTests < Minitest::Test
    method setup (line 10) | def setup
    method teardown (line 39) | def teardown
    method test_loading_301_redirect (line 45) | def test_loading_301_redirect
    method test_loading_302_redirect (line 50) | def test_loading_302_redirect
    method test_loading_a_local_file (line 55) | def test_loading_a_local_file
    method test_loading_a_local_file_with_scheme (line 61) | def test_loading_a_local_file_with_scheme
    method test_loading_a_remote_file (line 67) | def test_loading_a_remote_file
    method test_loading_a_remote_file_over_ssl (line 73) | def test_loading_a_remote_file_over_ssl
    method test_loading_a_string (line 78) | def test_loading_a_string
    method test_following_at_import_rules_local (line 83) | def test_following_at_import_rules_local
    method test_following_at_import_rules_remote (line 97) | def test_following_at_import_rules_remote
    method test_imports_disabled (line 110) | def test_imports_disabled
    method test_following_remote_import_rules (line 124) | def test_following_remote_import_rules
    method test_following_badly_escaped_import_rules (line 132) | def test_following_badly_escaped_import_rules
    method test_loading_malformed_content_strings (line 140) | def test_loading_malformed_content_strings
    method test_loading_malformed_css_brackets (line 148) | def test_loading_malformed_css_brackets
    method test_following_at_import_rules_from_add_block (line 159) | def test_following_at_import_rules_from_add_block
    method test_importing_with_media_types (line 168) | def test_importing_with_media_types
    method test_local_circular_reference_exception (line 176) | def test_local_circular_reference_exception
    method test_remote_circular_reference_exception (line 182) | def test_remote_circular_reference_exception
    method test_suppressing_circular_reference_exceptions (line 188) | def test_suppressing_circular_reference_exceptions
    method test_toggling_not_found_exceptions (line 194) | def test_toggling_not_found_exceptions

FILE: test/test_css_parser_media_types.rb
  class CssParserMediaTypesTests (line 6) | class CssParserMediaTypesTests < Minitest::Test
    method setup (line 9) | def setup
    method test_that_media_types_dont_include_all (line 13) | def test_that_media_types_dont_include_all
    method test_finding_by_media_type (line 27) | def test_finding_by_media_type
    method test_with_parenthesized_media_features (line 49) | def test_with_parenthesized_media_features
    method test_finding_by_multiple_media_types (line 68) | def test_finding_by_multiple_media_types
    method test_adding_block_with_media_types (line 84) | def test_adding_block_with_media_types
    method test_adding_block_with_media_types_followed_by_general_rule (line 93) | def test_adding_block_with_media_types_followed_by_general_rule
    method test_adding_block_and_limiting_media_types1 (line 105) | def test_adding_block_and_limiting_media_types1
    method test_adding_block_and_limiting_media_types2 (line 116) | def test_adding_block_and_limiting_media_types2
    method test_adding_block_and_limiting_media_types (line 127) | def test_adding_block_and_limiting_media_types
    method test_adding_rule_set_with_media_type (line 137) | def test_adding_rule_set_with_media_type
    method test_adding_rule_set_with_media_query (line 143) | def test_adding_rule_set_with_media_query
    method test_selecting_with_all_media_types (line 149) | def test_selecting_with_all_media_types
    method test_to_s_includes_media_queries (line 154) | def test_to_s_includes_media_queries

FILE: test/test_css_parser_misc.rb
  class CssParserTests (line 6) | class CssParserTests < Minitest::Test
    method setup (line 9) | def setup
    method test_utf8 (line 13) | def test_utf8
    method test_at_page_rule (line 23) | def test_at_page_rule
    method test_should_ignore_comments (line 39) | def test_should_ignore_comments
    method test_parsing_blocks (line 64) | def test_parsing_blocks
    method test_ignoring_malformed_declarations (line 97) | def test_ignoring_malformed_declarations
    method test_multiline_declarations (line 116) | def test_multiline_declarations
    method test_find_rule_sets (line 135) | def test_find_rule_sets
    method test_calculating_specificity (line 152) | def test_calculating_specificity
    method test_converting_uris (line 172) | def test_converting_uris
    method test_ruleset_with_braces (line 190) | def test_ruleset_with_braces
    method test_content_with_data (line 210) | def test_content_with_data
    method test_enumerator_empty (line 215) | def test_enumerator_empty
    method test_enumerator_nonempty (line 219) | def test_enumerator_nonempty
    method with_value_exception (line 230) | def with_value_exception
    method test_catching_argument_exceptions_for_add_rule (line 236) | def test_catching_argument_exceptions_for_add_rule
    method test_catching_argument_exceptions_for_add_rule_positional (line 248) | def test_catching_argument_exceptions_for_add_rule_positional
    method test_catching_argument_exceptions_for_add_rule_with_offsets (line 267) | def test_catching_argument_exceptions_for_add_rule_with_offsets

FILE: test/test_css_parser_offset_capture.rb
  class CssParserOffsetCaptureTests (line 6) | class CssParserOffsetCaptureTests < Minitest::Test
    method setup (line 9) | def setup
    method test_capturing_offsets_for_local_file (line 13) | def test_capturing_offsets_for_local_file
    method test_capturing_offsets_from_remote_file (line 31) | def test_capturing_offsets_from_remote_file
    method test_capturing_offsets_from_string (line 48) | def test_capturing_offsets_from_string
    method test_capturing_offsets_with_imports (line 77) | def test_capturing_offsets_with_imports

FILE: test/test_css_parser_regexps.rb
  class CssParserRegexpTests (line 10) | class CssParserRegexpTests < Minitest::Test
    method test_strings (line 11) | def test_strings
    method test_box_model_units (line 26) | def test_box_model_units
    method test_unicode (line 32) | def test_unicode
    method test_colour (line 38) | def test_colour
    method test_gradients (line 60) | def test_gradients
    method test_uris (line 74) | def test_uris
    method test_important (line 87) | def test_important
    method load_test_file (line 94) | def load_test_file(filename)

FILE: test/test_merging.rb
  class MergingTests (line 5) | class MergingTests < Minitest::Test
    method setup (line 8) | def setup
    method test_simple_merge (line 12) | def test_simple_merge
    method test_merging_array (line 20) | def test_merging_array
    method test_merging_with_compound_selectors (line 28) | def test_merging_with_compound_selectors
    method test_merging_multiple (line 43) | def test_merging_multiple
    method test_multiple_selectors_should_have_proper_specificity (line 51) | def test_multiple_selectors_should_have_proper_specificity
    method test_setting_specificity (line 58) | def test_setting_specificity
    method test_properties_should_be_case_insensitive (line 65) | def test_properties_should_be_case_insensitive
    method test_merging_backgrounds (line 72) | def test_merging_backgrounds
    method test_merging_dimensions (line 79) | def test_merging_dimensions
    method test_merging_fonts (line 86) | def test_merging_fonts
    method test_raising_error_on_bad_type (line 93) | def test_raising_error_on_bad_type
    method test_returning_early_with_only_one_params (line 99) | def test_returning_early_with_only_one_params
    method test_merging_important (line 105) | def test_merging_important
    method test_prioritising_important_over_non_important_in_the_same_block (line 112) | def test_prioritising_important_over_non_important_in_the_same_block
    method test_prioritising_two_important_declarations_in_the_same_block (line 118) | def test_prioritising_two_important_declarations_in_the_same_block
    method test_merging_multiple_important (line 124) | def test_merging_multiple_important
    method test_merging_shorthand_important (line 135) | def test_merging_shorthand_important

FILE: test/test_rule_set.rb
  class RuleSetTests (line 6) | class RuleSetTests < Minitest::Test
    method setup (line 9) | def setup
    method test_setting_property_values (line 13) | def test_setting_property_values
    method test_getting_property_values (line 23) | def test_getting_property_values
    method test_getting_property_value_ignoring_case (line 28) | def test_getting_property_value_ignoring_case
    method test_each_selector (line 33) | def test_each_selector
    method test_each_declaration (line 48) | def test_each_declaration
    method test_each_declaration_respects_order (line 64) | def test_each_declaration_respects_order
    method test_each_declaration_containing_semicolons (line 73) | def test_each_declaration_containing_semicolons
    method test_each_declaration_with_newlines (line 80) | def test_each_declaration_with_newlines
    method test_selector_sanitization (line 93) | def test_selector_sanitization
    method test_multiple_selectors_to_s (line 99) | def test_multiple_selectors_to_s
    method test_declarations_to_s (line 105) | def test_declarations_to_s
    method test_important_declarations_to_s (line 111) | def test_important_declarations_to_s
    method test_overriding_specificity (line 117) | def test_overriding_specificity
    method test_important_without_value (line 124) | def test_important_without_value
    method test_not_raised_issue68 (line 130) | def test_not_raised_issue68

FILE: test/test_rule_set_creating_shorthand.rb
  class RuleSetCreatingShorthandTests (line 6) | class RuleSetCreatingShorthandTests < Minitest::Test
    method setup (line 9) | def setup
    method test_border_width (line 13) | def test_border_width
    method test_border_width_with_border_color_with_spaces (line 20) | def test_border_width_with_border_color_with_spaces
    method test_combining_borders_into_shorthand (line 33) | def test_combining_borders_into_shorthand
    method test_combining_dimensions_into_shorthand (line 88) | def test_combining_dimensions_into_shorthand
    method test_combining_dimensions_into_shorthand_with_auto (line 113) | def test_combining_dimensions_into_shorthand_with_auto
    method test_combining_font_into_shorthand (line 122) | def test_combining_font_into_shorthand
    method test_combining_background_into_shorthand (line 143) | def test_combining_background_into_shorthand
    method test_combining_background_with_size_into_shorthand (line 158) | def test_combining_background_with_size_into_shorthand
    method test_combining_background_with_size_and_no_position_into_shorthand (line 173) | def test_combining_background_with_size_and_no_position_into_shorthand
    method test_combining_list_style_into_shorthand (line 189) | def test_combining_list_style_into_shorthand
    method test_property_values_in_url (line 203) | def test_property_values_in_url
    method test_a_single_property_is_not_shorted (line 215) | def test_a_single_property_is_not_shorted
    method assert_properties_are_deleted (line 225) | def assert_properties_are_deleted(ruleset, properties)
    method create_shorthand (line 231) | def create_shorthand(properties)

FILE: test/test_rule_set_expanding_shorthand.rb
  class RuleSetExpandingShorthandTests (line 5) | class RuleSetExpandingShorthandTests < Minitest::Test
    method setup (line 8) | def setup
    method test_expanding_border_shorthand (line 13) | def test_expanding_border_shorthand
    method test_getting_dimensions_from_shorthand (line 65) | def test_getting_dimensions_from_shorthand
    method test_getting_font_size_from_shorthand (line 81) | def test_getting_font_size_from_shorthand
    method test_getting_font_families_from_shorthand (line 95) | def test_getting_font_families_from_shorthand
    method test_getting_font_weight_from_shorthand (line 101) | def test_getting_font_weight_from_shorthand
    method test_getting_font_variant_from_shorthand (line 116) | def test_getting_font_variant_from_shorthand
    method test_getting_font_variant_from_shorthand_ensure_normal_is_the_default_state (line 122) | def test_getting_font_variant_from_shorthand_ensure_normal_is_the_defa...
    method test_getting_font_style_from_shorthand (line 132) | def test_getting_font_style_from_shorthand
    method test_getting_line_height_from_shorthand (line 147) | def test_getting_line_height_from_shorthand
    method test_getting_line_height_from_shorthand_with_spaces (line 162) | def test_getting_line_height_from_shorthand_with_spaces
    method test_getting_background_properties_from_shorthand (line 171) | def test_getting_background_properties_from_shorthand
    method test_getting_background_position_from_shorthand (line 182) | def test_getting_background_position_from_shorthand
    method test_getting_background_size_from_shorthand (line 196) | def test_getting_background_size_from_shorthand
    method test_getting_background_colour_from_shorthand (line 210) | def test_getting_background_colour_from_shorthand
    method test_getting_background_attachment_from_shorthand (line 223) | def test_getting_background_attachment_from_shorthand
    method test_getting_background_repeat_from_shorthand (line 231) | def test_getting_background_repeat_from_shorthand
    method test_getting_background_image_from_shorthand (line 239) | def test_getting_background_image_from_shorthand
    method test_getting_background_gradient_from_shorthand (line 249) | def test_getting_background_gradient_from_shorthand
    method test_getting_list_style_properties_from_shorthand (line 260) | def test_getting_list_style_properties_from_shorthand
    method test_getting_list_style_position_from_shorthand (line 271) | def test_getting_list_style_position_from_shorthand
    method test_getting_list_style_type_from_shorthand (line 279) | def test_getting_list_style_type_from_shorthand
    method test_expanding_shorthand_with_replaced_properties_after (line 287) | def test_expanding_shorthand_with_replaced_properties_after
    method test_expanding_important_shorthand_with_replaced_properties (line 301) | def test_expanding_important_shorthand_with_replaced_properties
    method test_functions_with_many_spaces (line 315) | def test_functions_with_many_spaces
    method test_functions_with_no_spaces (line 327) | def test_functions_with_no_spaces
    method test_functions_with_one_space (line 339) | def test_functions_with_one_space
    method test_functions_with_commas (line 351) | def test_functions_with_commas
    method expand_declarations (line 365) | def expand_declarations(declarations)
Condensed preview — 38 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (188K chars).
[
  {
    "path": ".editorconfig",
    "chars": 166,
    "preview": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 98,
    "preview": "Why and what is being done.\n\n## Pre-Merge Checklist\n- [ ] CHANGELOG.md updated with short summary\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 767,
    "preview": "name: Run css_parser CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\njobs:\n  test:\n    name: Test ruby ver"
  },
  {
    "path": ".gitignore",
    "chars": 26,
    "preview": "/pkg/\n/bin/\n.ruby-version\n"
  },
  {
    "path": ".jrubyrc",
    "chars": 18,
    "preview": "cext.enabled=true\n"
  },
  {
    "path": ".rubocop.yml",
    "chars": 1335,
    "preview": "plugins:\n  - rubocop-performance\n  - rubocop-rake\n\nAllCops:\n  TargetRubyVersion: 3.3 # lowest supported version\n  NewCop"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 823,
    "preview": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n   "
  },
  {
    "path": "CHANGELOG.md",
    "chars": 7921,
    "preview": "## Ruby CSS Parser CHANGELOG\n\n### Unreleased\n\n### Version 2.2.0\n* Accept CSS `<number>` values with an omitted integer p"
  },
  {
    "path": "Gemfile",
    "chars": 408,
    "preview": "# frozen_string_literal: true\n\n# Keep Gemfile.lock in repo. Reason: https://grosser.it/2015/08/14/check-in-your-gemfile-"
  },
  {
    "path": "MIT-LICENSE",
    "chars": 1085,
    "preview": "=== Ruby CSS Parser License\n\nCopyright (c) 2007-11 Alex Dunae\n\nPermission is hereby granted, free of charge, to any pers"
  },
  {
    "path": "README.md",
    "chars": 2444,
    "preview": "# Ruby CSS Parser [![Build Status](https://github.com/premailer/css_parser/workflows/Run%20css_parser%20CI/badge.svg)](h"
  },
  {
    "path": "Rakefile",
    "chars": 1423,
    "preview": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'bundler/gem_tasks'\nrequire 'rake/testtask'\nrequire 'rubo"
  },
  {
    "path": "css_parser.gemspec",
    "chars": 797,
    "preview": "# frozen_string_literal: true\n\nname = 'css_parser'\nrequire \"./lib/#{name}/version\"\n\nGem::Specification.new name, CssPars"
  },
  {
    "path": "lib/css_parser/parser.rb",
    "chars": 24087,
    "preview": "# frozen_string_literal: true\n\nrequire 'strscan'\n\nmodule CssParser\n  # Exception class used for any errors encountered w"
  },
  {
    "path": "lib/css_parser/regexps.rb",
    "chars": 7527,
    "preview": "# frozen_string_literal: true\n\nmodule CssParser\n  def self.regex_possible_values(*values)\n    Regexp.new(\"([\\s]*^)?(#{va"
  },
  {
    "path": "lib/css_parser/rule_set.rb",
    "chars": 24876,
    "preview": "# frozen_string_literal: true\n\nrequire 'forwardable'\n\nmodule CssParser\n  class RuleSet\n    # Patterns for specificity ca"
  },
  {
    "path": "lib/css_parser/version.rb",
    "chars": 79,
    "preview": "# frozen_string_literal: true\n\nmodule CssParser\n  VERSION = '2.2.0'.freeze\nend\n"
  },
  {
    "path": "lib/css_parser.rb",
    "chars": 5048,
    "preview": "# frozen_string_literal: true\n\nrequire 'addressable/uri'\nrequire 'uri'\nrequire 'net/https'\nrequire 'digest/md5'\nrequire "
  },
  {
    "path": "test/fixtures/complex.css",
    "chars": 10752,
    "preview": "/*\nFonts:\n\tfont-family:'Caslon 540 LT W01 Italic';\n\tfont-family:'Caslon 540 LT W01 Roman';\n\tfont-family:'Univers LT W01 "
  },
  {
    "path": "test/fixtures/import-circular-reference.css",
    "chars": 103,
    "preview": "@import \"import-circular-reference.css\";\n\nbody { color: black; background: white; }\np { margin: 0px; }\n"
  },
  {
    "path": "test/fixtures/import-malformed.css",
    "chars": 398,
    "preview": ".malformed.one:before {\n  content: \"\\\\\";\n  color: \"red\";\n}\n\n.wellformed.one {\n  color: \"green\";\n}\n\n.malformed.two:before"
  },
  {
    "path": "test/fixtures/import-with-media-types.css",
    "chars": 62,
    "preview": "@import \"simple.css\" print, tv, screen;\n\ndiv { color: lime; }\n"
  },
  {
    "path": "test/fixtures/import1.css",
    "chars": 52,
    "preview": "@import 'subdir/import2.css';\n\ndiv { color: lime; }\n"
  },
  {
    "path": "test/fixtures/simple.css",
    "chars": 64,
    "preview": "body {\n\tcolor: black;\n\tbackground: white;\n}\n\np { margin: 0px; }\n"
  },
  {
    "path": "test/fixtures/subdir/import2.css",
    "chars": 55,
    "preview": "@import \"../simple.css\";\n\na { text-decoration: none; }\n"
  },
  {
    "path": "test/rule_set/declarations/test_value.rb",
    "chars": 5900,
    "preview": "# frozen_string_literal: true\n\nrequire_relative '../../test_helper'\nrequire 'minitest/spec'\nrequire 'ostruct'\n\nclass Rul"
  },
  {
    "path": "test/rule_set/test_declarations.rb",
    "chars": 18569,
    "preview": "# frozen_string_literal: true\n\nrequire_relative '../test_helper'\n\nclass RuleSetDeclarationsTest < Minitest::Test\n  descr"
  },
  {
    "path": "test/test_css_parser_basic.rb",
    "chars": 2707,
    "preview": "# frozen_string_literal: true\n\nrequire_relative \"test_helper\"\n\n# Test cases for reading and generating CSS shorthand pro"
  },
  {
    "path": "test/test_css_parser_loading.rb",
    "chars": 6316,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser's loading functions.\nclass"
  },
  {
    "path": "test/test_css_parser_media_types.rb",
    "chars": 5128,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the handling of media types\nclass CssPar"
  },
  {
    "path": "test/test_css_parser_misc.rb",
    "chars": 9683,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser.\nclass CssParserTests < Mi"
  },
  {
    "path": "test/test_css_parser_offset_capture.rb",
    "chars": 3754,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser's loading functions.\nclass"
  },
  {
    "path": "test/test_css_parser_regexps.rb",
    "chars": 3711,
    "preview": "# coding: iso-8859-1\n# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for CSS regular express"
  },
  {
    "path": "test/test_helper.rb",
    "chars": 183,
    "preview": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'maxitest/autorun'\nrequire 'mocha/minitest'\nrequire 'ostr"
  },
  {
    "path": "test/test_merging.rb",
    "chars": 4655,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\nclass MergingTests < Minitest::Test\n  include CssParser\n\n"
  },
  {
    "path": "test/test_rule_set.rb",
    "chars": 4574,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for parsing CSS blocks\nclass RuleSetTests < "
  },
  {
    "path": "test/test_rule_set_creating_shorthand.rb",
    "chars": 7822,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for reading and generating CSS shorthand pro"
  },
  {
    "path": "test/test_rule_set_expanding_shorthand.rb",
    "chars": 15497,
    "preview": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\nclass RuleSetExpandingShorthandTests < Minitest::Test\n  i"
  }
]

About this extraction

This page contains the full source code of the premailer/css_parser GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 38 files (174.7 KB), approximately 48.8k tokens, and a symbol index with 247 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!