[
  {
    "path": ".editorconfig",
    "content": "# editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\nend_of_line = lf\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "Why and what is being done.\n\n## Pre-Merge Checklist\n- [ ] CHANGELOG.md updated with short summary\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: Run css_parser CI\n\non:\n  pull_request:\n  push:\n    branches:\n      - master\n\njobs:\n  test:\n    name: Test ruby version matrix\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        ruby-version: ['3.3', '3.4', '4.0', 'jruby']\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - run: rm Gemfile.lock\n\n      - uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: ${{ matrix.ruby-version }}\n          bundler-cache: true\n\n      - run: bundle exec rake test\n\n  rubocop:\n    name: Run rubocop\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v4\n\n      - uses: ruby/setup-ruby@v1\n        with:\n          ruby-version: '3.3' # lowest supported Ruby version\n          bundler-cache: true\n\n      - run: bundle exec rake rubocop\n"
  },
  {
    "path": ".gitignore",
    "content": "/pkg/\n/bin/\n.ruby-version\n"
  },
  {
    "path": ".jrubyrc",
    "content": "cext.enabled=true\n"
  },
  {
    "path": ".rubocop.yml",
    "content": "plugins:\n  - rubocop-performance\n  - rubocop-rake\n\nAllCops:\n  TargetRubyVersion: 3.3 # lowest supported version\n  NewCops: enable\n\nLayout/ArgumentAlignment:\n  EnforcedStyle: with_fixed_indentation\n\nLayout/AccessModifierIndentation:\n  EnforcedStyle: outdent\n\nLayout/LineLength:\n  Enabled: false\n\nLayout/SpaceInsideHashLiteralBraces:\n  EnforcedStyle: no_space\n\nMetrics/AbcSize:\n  Enabled: false\n\nMetrics/BlockLength:\n  Enabled: false\n\nMetrics/BlockNesting:\n  Enabled: false\n\nMetrics/ClassLength:\n  Enabled: false\n\nMetrics/CyclomaticComplexity:\n  Enabled: false\n\nMetrics/MethodLength:\n  Enabled: false\n\nMetrics/ModuleLength:\n  Enabled: false\n\nMetrics/PerceivedComplexity:\n  Enabled: false\n\nStyle/AndOr:\n  Enabled: false\n\nStyle/Documentation:\n  Enabled: false\n\nStyle/IfUnlessModifier:\n  Enabled: false\n\nStyle/Not:\n  Enabled: false\n\nStyle/NumericPredicate:\n  Enabled: false\n\nStyle/OpenStructUse:\n  Exclude:\n    - 'test/**/*'\n\nStyle/RedundantFreeze:\n  Enabled: false\n\nStyle/RescueStandardError:\n  EnforcedStyle: implicit\n\nStyle/SafeNavigation:\n  Enabled: false\n\nStyle/StringLiterals:\n  Exclude:\n    - 'test/**/*'\n\nStyle/WordArray:\n  Enabled: false\n\nStyle/SymbolArray:\n  EnforcedStyle: brackets\n\nStyle/HashSyntax:\n  EnforcedShorthandSyntax: never\n\nNaming/BlockForwarding:\n  EnforcedStyle: explicit\n\nStyle/CollectionQuerying:\n  Enabled: false\n"
  },
  {
    "path": ".vscode/tasks.json",
    "content": "{\n    // See https://go.microsoft.com/fwlink/?LinkId=733558\n    // for the documentation about the tasks.json format\n    \"version\": \"2.0.0\",\n    \"tasks\": [\n      {\n        \"label\": \"Bundle dependencies and stubs\",\n        \"type\": \"shell\",\n        \"command\": \"bundle\",\n        \"args\": [\n          \"--binstubs\"\n        ],\n        \"isBackground\": true,\n        \"problemMatcher\": []\n      },\n      {\n        \"label\": \"Test\",\n        \"type\": \"shell\",\n        \"command\": \"${workspaceRoot}/bin/rake\",\n        \"problemMatcher\": [],\n        \"group\": {\n          \"_id\": \"test\",\n          \"isDefault\": false\n        }\n      },\n      {\n        \"label\": \"Benchmark\",\n        \"type\": \"shell\",\n        \"command\": \"${workspaceRoot}/bin/rake\",\n        \"args\": [\n          \"benchmark\"\n        ],\n        \"problemMatcher\": []\n      }\n    ]\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "## Ruby CSS Parser CHANGELOG\n\n### Unreleased\n\n### Version 2.2.0\n* 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:`).\n\n### Version 2.1.0\n* Validate ssl when pulling files via https\n\n### Version 2.0.0\n* Drop ruby <3.2, fix a memory leak\n\n### Version v1.21.1\n\n* Prefer `!important` rules over non-`!important` rules in the same ruleset\n* Minor performance improvements\n\n### Version v1.21.0\n\n* Minor performance improvements\n\n### Version v1.20.0\n\n* Remove `iconv` conditional require\n\n### Version v1.19.1\n\n* Fix error when parsing values consisting of `!important` only\n\n### Version v1.19.0\n\n* Deprecate `load_uri!`, `load_file!` and `load_string!` positional arguments over keyword argument\n* Deprecate `add_rule!` (positional arguments)and `add_rule_with_offsets!` for `add_rule!` (keyword argument)\n* RuleSet initialize now takes keyword argument, positional arguments are still supported but deprecated\n* Removed OffsetAwareRuleSet, it's a RuleSet with optional attributes filename and offset\n* Improved performance of block parsing by using StringScanner\n* Improve `RuleSet#parse_declarations!` performance by using substring search istead of regexps\n* Fix error when parsing values consisting of `!important` only\n\n### Version v1.18.0\n\n * Drop Ruby 2.7 compatibility for parity with Premailer [#149](https://github.com/premailer/css_parser/pull/149)\n\n### Version v1.17.1\n\n * Improve security by using `File.read` instead of `IO.read` [#149](https://github.com/premailer/css_parser/pull/149)\n\n### Version v1.17.0\n\n * Added `user_agent` as an option to Parser [#146](https://github.com/premailer/css_parser/pull/146)\n\n### Version v1.16.0\n\n * Fix parsing space-less media query features like `@media(width:123px)` [#141](https://github.com/premailer/css_parser/pull/141)\n\n### Version v1.15.0\n\n * Fix parsing background shorthands in ruby 3.2 [#140](https://github.com/premailer/css_parser/pull/140)\n\n### Version v1.14.0\n\n * Fix parsing of multiline URL values for rule sets [#97](https://github.com/premailer/css_parser/pull/97)\n\n### Version v1.13.0\n\n * Drop suppor for EOL ruby versions\n * fix regex deprecation\n\n### Version v1.12.0\n\n * Improve exception message for missing value [#131](https://github.com/premailer/css_parser/pull/131)\n * `:rule_set_exceptions` option added [#132](https://github.com/premailer/css_parser/pull/132)\n\n### Version 1.11.0\n\n * Do not combine border styles width/color/style are not all present\n\n### Version 1.10.0\n\n * Allow CSS functions to be used in CssParser::RuleSet#expand_dimensions_shorthand! [#126](https://github.com/premailer/css_parser/pull/126)\n\n### Version 1.9.0\n\n * Misc cleanup [#122](https://github.com/premailer/css_parser/pull/122)\n\n### Version 1.8.0\n\n * Internal refactoring around ruleset [diff](https://github.com/premailer/css_parser/compare/v1.7.1...v1.8.0)\n\n### Version 1.7.1\n\n * Force UTF-8 encoding; do not strip out UTF-8 chars. [#106](https://github.com/premailer/css_parser/pull/106)\n\n### Version 1.7.0\n\n * No longer support ruby versions 1.9 2.0 2.1\n * Memory allocation improvements\n\n### Version 1.6.0\n\n * Handles font-size/ line-height shorthand with spaces\n\n### Version 1.5.0\n\n * Extended color keywords support (https://www.w3.org/TR/css3-color/).\n * `remove_rule_set!` method added.\n * `:capture_offsets` feature added.\n\n### Version 1.4.10\n\n * Include uri in RemoteFileError message.\n * Prevent to convert single declarations to their respective shorthand.\n * Fix Ruby warnings.\n\n### Version 1.4.9\n\n * Support for vrem, vh, vw, vmin, vmax and vm box model units.\n * Replace obsolete calls with actual ones.\n * Fix some Ruby warnings.\n\n### Version 1.4.8\n\n * Allow to get CSS rules as Hash using `to_hash` method.\n * Updates to support Ruby 1.9 and JRuby.\n * utf-8 related update.\n\n### Version 1.4.7\n\n * background-position shorthand fix.\n\n### Version 1.4.6\n\n * Normalize whitespace in selectors and queries.\n * Strip spaces from keys.\n * More checks on ordering.\n\n### Version 1.4.5\n\n * Maintenance release.\n\n### Version 1.4.4\n\n * More robust redirection handling, refs #47.\n\n### Version 1.4.3\n\n * Look for redirects, MAX_REDIRECTS set to 3, refs #36.\n * Fix border style expanding, refs #58.\n * load_string! described, refs #70.\n\n### Version 1.4.2\n\n * Ship license with package, refs #69.\n\n### Version 1.4.1\n\n * Fix background shorthands, refs #66.\n\n### Version 1.4.0\n\n * Add support for background-size in the shorthand property @mitio\n\n### Version 1.3.6\n\n * Fix bug not setting general rules after media query @jievans.\n * We doesn't support Ruby 1.8 anymore.\n * Run tests on Ruby 2.0 and Ruby 2.1.\n * Respect the :import option.\n\n### Version 1.3.5\n\n * Use URI#request_uri instead of URI#path @duckinator.\n * Media_query_support @mzsanford\n * Don't require open-uri @aripollak\n * Symbols not sortable on 1.8.7 @morten\n * Improve create_dimensions_shorthand performance @aaronjensen\n * Fixes hash ordering in tests @morten\n\n### Version 1.3.4\n\n * Enable code highlighting for tests @grosser\n * Fix error in media query parsing @smgt\n * Add test to missing cleaning of media type in parsing @smgt\n\n### Version 1.3.3\n\n * Require version before requiring classes that depend on it @morten\n\n### Version 1.3.2\n\n * Fix them crazy requires and only define version once @grosser\n * Apply ocd @grosser\n\n### Version 1.3.1\n\n * More tests (and fixes) for background gradients @fortnightlabs\n * Support declarations with `;` in them @flavorpill\n * Stricter detection of !important @flavorpill\n\n### Version 1.3.0\n\n * Updates of gem by @grosser\n * Multiple selectors should properly calculate specificity @alexdunae\n * Specificity: The selector with the highest specificity may be in a compound selector statement? @morten\n * Selectors should not be registered with surrounding whitespace. @morten\n * Fix RE_GRADIENT reference @alexdunae\n * Add load_string! method tests @alexdunae\n * Gradient regexp tests @alexdunae\n * Edited rule set @mccuskk\n\n### Version 1.2.6\n\n * JRuby and Ruby 1.9.3-preview1 compat\n\n### Version 1.2.5\n\n * Fix merging of multiple !important rules to match the spec\n\n### Version 1.2.3\n\n * First pass of media query support\n\n### Version 1.2.2\n\n * Fix merging of multiple !important rules to match the spec\n\n### Version 1.2.1\n\n * Better border shorthand handling\n * List shorthand handling\n * Malformed URI handling improvements\n * Use Bundler\n\n### Version 1.2.0\n\n * Specificity improvements\n * RGBA, HSL and HSLA support\n * Bug fixes\n\n### Version 1.1.9\n\n * Add remove_declaration! to RuleSet\n\n### Version 1.1.8\n\n * Fix syntax error\n\n### Version 1.1.7\n\n * Automatically close missing braces at the end of a block\n\n### Version 1.1.6\n\n * Fix media type handling in add_block! and load_uri!\n\n### Version 1.1.5\n\n * Fix merging of !important declarations\n\n### Version 1.1.4\n\n * Ruby 1.9.2 compat\n\n### Version 1.1.3\n\n * allow limiting by media type in add_block!\n\n### Version 1.1.2\n\n * improve parsing of malformed declarations\n * improve support for local files\n * added support for loading over SSL\n * added support for deflate\n\n### Version 1.1.1\n\n * Ruby 1.9 compatibility\n * @import regexp updates\n * various bug fixes\n\n### Version 1.1.0\n\n * Added support for local @import\n * Better remote @import handling\n\n### Version 1.0.1\n\n * Fallback for declarations without sort order\n\n### Version 1.0.0\n\n * Various test fixes and udpate for Ruby 1.9 (thanks to Tyler Cunnion)\n * Allow setting CSS declarations to nil\n\n### Version 0.9\n\n * Initial version forked from Premailer project\n\n### TODO: Future\n\n * re-implement caching on CssParser.merge\n * correctly parse http://www.webstandards.org/files/acid2/test.html\n"
  },
  {
    "path": "Gemfile",
    "content": "# frozen_string_literal: true\n\n# Keep Gemfile.lock in repo. Reason: https://grosser.it/2015/08/14/check-in-your-gemfile-lock/\n\nsource 'https://rubygems.org'\n\ngemspec\n\ngem 'benchmark-ips'\ngem 'bump'\ngem 'maxitest'\ngem 'memory_profiler'\ngem 'mocha'\ngem 'ostruct'\ngem 'rake'\ngem 'rubocop', '~> 1.84.2' # locked down so we don't get accidental new cops\ngem 'rubocop-performance'\ngem 'rubocop-rake'\ngem 'webrick'\n"
  },
  {
    "path": "MIT-LICENSE",
    "content": "=== Ruby CSS Parser License\n\nCopyright (c) 2007-11 Alex Dunae\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# 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)\n\nLoad, parse and cascade CSS rule sets in Ruby.\n\n# Setup\n\n```Bash\ngem install css_parser\n```\n\n# Usage\n\n```Ruby\nrequire 'css_parser'\ninclude CssParser\n\nparser = CssParser::Parser.new\nparser.load_uri!('http://example.com/styles/style.css')\n\nparser = CssParser::Parser.new\nparser.load_uri!('file://home/user/styles/style.css')\n\n# load a remote file, setting the base_uri and media_types\nparser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', media_types: [:screen, :handheld]})\n\n# load a local file, setting the base_dir and media_types\nparser.load_file!('print.css', '~/styles/', :print)\n\n# load a string\nparser = CssParser::Parser.new\nparser.load_string! 'a { color: hotpink; }'\n\n# lookup a rule by a selector\nparser.find_by_selector('#content')\n#=> 'font-size: 13px; line-height: 1.2;'\n\n# lookup a rule by a selector and media type\nparser.find_by_selector('#content', [:screen, :handheld])\n\n# iterate through selectors by media type\nparser.each_selector(:screen) do |selector, declarations, specificity|\n  ...\nend\n\n# add a block of CSS\ncss = <<-EOT\n  body { margin: 0 1em; }\nEOT\n\nparser.add_block!(css)\n\n# output all CSS rules in a single stylesheet\nparser.to_s\n=> #content { font-size: 13px; line-height: 1.2; }\n   body { margin: 0 1em; }\n\n# capturing byte offsets within a file\nparser.load_uri!('../style.css', {base_uri: 'http://example.com/styles/inc/', capture_offsets: true)\ncontent_rule = parser.find_rule_sets(['#content']).first\ncontent_rule.filename\n#=> 'http://example.com/styles/styles.css'\ncontent_rule.offset\n#=> 10703..10752\n\n# capturing byte offsets within a string\nparser.load_string!('a { color: hotpink; }', {filename: 'index.html', capture_offsets: true)\ncontent_rule = parser.find_rule_sets(['a']).first\ncontent_rule.filename\n#=> 'index.html'\ncontent_rule.offset\n#=> 0..21\n```\n\n# Testing\n\n```Bash\nbundle\nbundle exec rake\n```\n\nRuns on Ruby 3.0/JRuby 9.4 or above.\n\n# Credits\n\nBy Alex Dunae (dunae.ca, e-mail 'code' at the same domain), 2007-11.\n\nLicense: MIT\n\nThanks to [all the wonderful contributors](http://github.com/premailer/css_parser/contributors) for their updates.\n\nMade on Vancouver Island.\n"
  },
  {
    "path": "Rakefile",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'bundler/gem_tasks'\nrequire 'rake/testtask'\nrequire 'rubocop/rake_task'\nrequire 'bump/tasks'\n\ntask default: [:rubocop, :test]\n\nRake::TestTask.new do |test|\n  test.pattern = 'test/**/test*.rb'\n  test.verbose = true\nend\n\nRuboCop::RakeTask.new do |t|\n  # allow you to run \"$ rake rubocop -a\" to autofix\n  t.options << '-a' if ARGV.include?('-a')\n  t.options << '-A' if ARGV.include?('-A')\nend\n\ndesc 'Run a performance evaluation.'\ntask :benchmark do\n  require 'css_parser'\n\n  require 'benchmark/ips'\n  require 'memory_profiler'\n\n  fixtures_dir = Pathname.new(__dir__).join('test/fixtures')\n  import_css_path = fixtures_dir.join('import1.css').to_s.freeze\n  complex_css_path = fixtures_dir.join('complex.css').to_s.freeze\n\n  Benchmark.ips do |x|\n    x.report('import1.css loading') { CssParser::Parser.new.load_file!(import_css_path) }\n    x.report('complex.css loading') { CssParser::Parser.new.load_file!(complex_css_path) }\n  end\n\n  puts\n\n  report = MemoryProfiler.report { CssParser::Parser.new.load_file!(import_css_path) }\n  puts \"Loading `import1.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB\"\n\n  report = MemoryProfiler.report { CssParser::Parser.new.load_file!(complex_css_path) }\n  puts \"Loading `complex.css` allocated #{report.total_allocated} objects, #{report.total_allocated_memsize / 1024} KiB\"\nend\n"
  },
  {
    "path": "css_parser.gemspec",
    "content": "# frozen_string_literal: true\n\nname = 'css_parser'\nrequire \"./lib/#{name}/version\"\n\nGem::Specification.new name, CssParser::VERSION do |s|\n  s.summary = 'Ruby CSS parser.'\n  s.description = 'A set of classes for parsing CSS in Ruby.'\n  s.email    = 'code@dunae.ca'\n  s.homepage = \"https://github.com/premailer/#{name}\"\n  s.author = 'Alex Dunae'\n  s.files = Dir.glob('lib/**/*') + ['MIT-LICENSE']\n  s.license = 'MIT'\n  s.required_ruby_version = '>= 3.3'\n\n  s.metadata['changelog_uri'] = 'https://github.com/premailer/css_parser/blob/master/CHANGELOG.md'\n  s.metadata['source_code_uri'] = 'https://github.com/premailer/css_parser'\n  s.metadata['bug_tracker_uri'] = 'https://github.com/premailer/css_parser/issues'\n  s.metadata['rubygems_mfa_required'] = 'true'\n\n  s.add_dependency 'addressable'\nend\n"
  },
  {
    "path": "lib/css_parser/parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'strscan'\n\nmodule CssParser\n  # Exception class used for any errors encountered while downloading remote files.\n  class RemoteFileError < IOError; end\n\n  # Exception class used if a request is made to load a CSS file more than once.\n  class CircularReferenceError < StandardError; end\n\n  # == Parser class\n  #\n  # All CSS is converted to UTF-8.\n  #\n  # When calling Parser#new there are some configuaration options:\n  # [<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>.\n  # [<tt>import</tt>] Follow <tt>@import</tt> rules. Boolean, default is <tt>true</tt>.\n  # [<tt>io_exceptions</tt>] Throw an exception if a link can not be found. Boolean, default is <tt>true</tt>.\n  class Parser\n    USER_AGENT = \"Ruby CSS Parser/#{CssParser::VERSION} (https://github.com/premailer/css_parser)\".freeze\n    RULESET_TOKENIZER_RX = /\\s+|\\\\{2,}|\\\\?[{}\\s\"]|[()]|.[^\\s\"{}()\\\\]*/.freeze\n    STRIP_CSS_COMMENTS_RX = %r{/\\*.*?\\*/}m.freeze\n    STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze\n\n    # Initial parsing\n    RE_AT_IMPORT_RULE = /@import\\s*(?:url\\s*)?(?:\\()?(?:\\s*)[\"']?([^'\"\\s)]*)[\"']?\\)?([\\w\\s,^\\]()]*)\\)?[;\\n]?/.freeze\n\n    MAX_REDIRECTS = 3\n\n    # Array of CSS files that have been loaded.\n    attr_reader   :loaded_uris\n\n    def initialize(options = {})\n      @options = {\n        absolute_paths: false,\n        import: true,\n        io_exceptions: true,\n        rule_set_exceptions: true,\n        capture_offsets: false,\n        user_agent: USER_AGENT\n      }.merge(options)\n\n      # array of RuleSets\n      @rules = []\n\n      @redirect_count = nil\n\n      @loaded_uris = []\n\n      # unprocessed blocks of CSS\n      @blocks = []\n      reset!\n    end\n\n    # Get declarations by selector.\n    #\n    # +media_types+ are optional, and can be a symbol or an array of symbols.\n    # The default value is <tt>:all</tt>.\n    #\n    # ==== Examples\n    #  find_by_selector('#content')\n    #  => 'font-size: 13px; line-height: 1.2;'\n    #\n    #  find_by_selector('#content', [:screen, :handheld])\n    #  => 'font-size: 13px; line-height: 1.2;'\n    #\n    #  find_by_selector('#content', :print)\n    #  => 'font-size: 11pt; line-height: 1.2;'\n    #\n    # Returns an array of declarations.\n    def find_by_selector(selector, media_types = :all)\n      out = []\n      each_selector(media_types) do |sel, dec, _spec|\n        out << dec if sel.strip == selector.strip\n      end\n      out\n    end\n    alias [] find_by_selector\n\n    # Finds the rule sets that match the given selectors\n    def find_rule_sets(selectors, media_types = :all)\n      rule_sets = []\n\n      selectors.each do |selector|\n        selector = selector.gsub(/\\s+/, ' ').strip\n        each_rule_set(media_types) do |rule_set, _media_type|\n          if !rule_sets.member?(rule_set) && rule_set.selectors.member?(selector)\n            rule_sets << rule_set\n          end\n        end\n      end\n\n      rule_sets\n    end\n\n    # Add a raw block of CSS.\n    #\n    # In order to follow +@import+ rules you must supply either a\n    # +:base_dir+ or +:base_uri+ option.\n    #\n    # Use the +:media_types+ option to set the media type(s) for this block.  Takes an array of symbols.\n    #\n    # Use the +:only_media_types+ option to selectively follow +@import+ rules.  Takes an array of symbols.\n    #\n    # ==== Example\n    #   css = <<-EOT\n    #     body { font-size: 10pt }\n    #     p { margin: 0px; }\n    #     @media screen, print {\n    #       body { line-height: 1.2 }\n    #     }\n    #   EOT\n    #\n    #   parser = CssParser::Parser.new\n    #   parser.add_block!(css)\n    def add_block!(block, options = {})\n      options = {base_uri: nil, base_dir: nil, charset: nil, media_types: :all, only_media_types: :all}.merge(options)\n      options[:media_types] = [options[:media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }\n      options[:only_media_types] = [options[:only_media_types]].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }\n\n      block = cleanup_block(block, options)\n\n      if options[:base_uri] and @options[:absolute_paths]\n        block = CssParser.convert_uris(block, options[:base_uri])\n      end\n\n      # Load @imported CSS\n      if @options[:import]\n        block.scan(RE_AT_IMPORT_RULE).each do |import_rule|\n          media_types = []\n          if (media_string = import_rule[-1])\n            media_string.split(',').each do |t|\n              media_types << CssParser.sanitize_media_query(t) unless t.empty?\n            end\n          else\n            media_types = [:all]\n          end\n\n          next unless options[:only_media_types].include?(:all) or media_types.empty? or media_types.intersect?(options[:only_media_types])\n\n          import_path = import_rule[0].to_s.gsub(/['\"]*/, '').strip\n\n          import_options = {media_types: media_types}\n          import_options[:capture_offsets] = true if options[:capture_offsets]\n\n          if options[:base_uri]\n            import_uri = Addressable::URI.parse(options[:base_uri].to_s) + Addressable::URI.parse(import_path)\n            import_options[:base_uri] = options[:base_uri]\n            load_uri!(import_uri, import_options)\n          elsif options[:base_dir]\n            import_options[:base_dir] = options[:base_dir]\n            load_file!(import_path, import_options)\n          end\n        end\n      end\n\n      # Remove @import declarations\n      block = ignore_pattern(block, RE_AT_IMPORT_RULE, options)\n\n      parse_block_into_rule_sets!(block, options)\n    end\n\n    # Add a CSS rule by setting the +selectors+, +declarations+\n    # and +media_types+. Optional pass +filename+ , +offset+ for source\n    # reference too.\n    #\n    # +media_types+ can be a symbol or an array of symbols. default to :all\n    # optional fields for source location for source location\n    # +filename+ can be a string or uri pointing to the file or url location.\n    # +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.\n    def add_rule!(*args, selectors: nil, block: nil, filename: nil, offset: nil, media_types: :all) # rubocop:disable Metrics/ParameterLists\n      if args.any?\n        media_types = nil\n        if selectors || block || filename || offset || media_types\n          raise ArgumentError, \"don't mix positional and keyword arguments arguments\"\n        end\n\n        warn '[DEPRECATION] `add_rule!` with positional arguments is deprecated. ' \\\n             'Please use keyword arguments instead.', uplevel: 1\n\n        case args.length\n        when 2\n          selectors, block = args\n        when 3\n          selectors, block, media_types = args\n        else\n          raise ArgumentError\n        end\n      end\n\n      begin\n        rule_set = RuleSet.new(\n          selectors: selectors, block: block,\n          offset: offset, filename: filename\n        )\n\n        add_rule_set!(rule_set, media_types)\n      rescue ArgumentError => e\n        raise e if @options[:rule_set_exceptions]\n      end\n    end\n\n    # Add a CSS rule by setting the +selectors+, +declarations+, +filename+, +offset+ and +media_types+.\n    #\n    # +filename+ can be a string or uri pointing to the file or url location.\n    # +offset+ should be Range object representing the start and end byte locations where the rule was found in the file.\n    # +media_types+ can be a symbol or an array of symbols.\n    def add_rule_with_offsets!(selectors, declarations, filename, offset, media_types = :all)\n      warn '[DEPRECATION] `add_rule_with_offsets!` is deprecated. Please use `add_rule!` instead.', uplevel: 1\n      add_rule!(\n        selectors: selectors, block: declarations, media_types: media_types,\n        filename: filename, offset: offset\n      )\n    end\n\n    # Add a CssParser RuleSet object.\n    #\n    # +media_types+ can be a symbol or an array of symbols.\n    def add_rule_set!(ruleset, media_types = :all)\n      raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)\n\n      media_types = [media_types] unless media_types.is_a?(Array)\n      media_types = media_types.flat_map { |mt| CssParser.sanitize_media_query(mt) }\n\n      @rules << {media_types: media_types, rules: ruleset}\n    end\n\n    # Remove a CssParser RuleSet object.\n    #\n    # +media_types+ can be a symbol or an array of symbols.\n    def remove_rule_set!(ruleset, media_types = :all)\n      raise ArgumentError unless ruleset.is_a?(CssParser::RuleSet)\n\n      media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }\n\n      @rules.reject! do |rule|\n        rule[:media_types] == media_types && rule[:rules].to_s == ruleset.to_s\n      end\n    end\n\n    # Iterate through RuleSet objects.\n    #\n    # +media_types+ can be a symbol or an array of symbols.\n    def each_rule_set(media_types = :all) # :yields: rule_set, media_types\n      media_types = [:all] if media_types.nil?\n      media_types = [media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }\n\n      @rules.each do |block|\n        if media_types.include?(:all) or block[:media_types].any? { |mt| media_types.include?(mt) }\n          yield(block[:rules], block[:media_types])\n        end\n      end\n    end\n\n    # Output all CSS rules as a Hash\n    def to_h(which_media = :all)\n      out = {}\n      styles_by_media_types = {}\n      each_selector(which_media) do |selectors, declarations, _specificity, media_types|\n        media_types.each do |media_type|\n          styles_by_media_types[media_type] ||= []\n          styles_by_media_types[media_type] << [selectors, declarations]\n        end\n      end\n\n      styles_by_media_types.each_pair do |media_type, media_styles|\n        ms = {}\n        media_styles.each do |media_style|\n          ms = css_node_to_h(ms, media_style[0], media_style[1])\n        end\n        out[media_type.to_s] = ms\n      end\n      out\n    end\n\n    # Iterate through CSS selectors.\n    #\n    # +media_types+ can be a symbol or an array of symbols.\n    # See RuleSet#each_selector for +options+.\n    def each_selector(all_media_types = :all, options = {}) # :yields: selectors, declarations, specificity, media_types\n      return to_enum(__method__, all_media_types, options) unless block_given?\n\n      each_rule_set(all_media_types) do |rule_set, media_types|\n        rule_set.each_selector(options) do |selectors, declarations, specificity|\n          yield selectors, declarations, specificity, media_types\n        end\n      end\n    end\n\n    # Output all CSS rules as a single stylesheet.\n    def to_s(which_media = :all)\n      out = []\n      styles_by_media_types = {}\n\n      each_selector(which_media) do |selectors, declarations, _specificity, media_types|\n        media_types.each do |media_type|\n          styles_by_media_types[media_type] ||= []\n          styles_by_media_types[media_type] << [selectors, declarations]\n        end\n      end\n\n      styles_by_media_types.each_pair do |media_type, media_styles|\n        media_block = (media_type != :all)\n        out << \"@media #{media_type} {\" if media_block\n\n        media_styles.each do |media_style|\n          if media_block\n            out.push(\"  #{media_style[0]} {\\n    #{media_style[1]}\\n  }\")\n          else\n            out.push(\"#{media_style[0]} {\\n#{media_style[1]}\\n}\")\n          end\n        end\n\n        out << '}' if media_block\n      end\n\n      out << ''\n      out.join(\"\\n\")\n    end\n\n    # A hash of { :media_query => rule_sets }\n    def rules_by_media_query\n      rules_by_media = {}\n      @rules.each do |block|\n        block[:media_types].each do |mt|\n          unless rules_by_media.key?(mt)\n            rules_by_media[mt] = []\n          end\n          rules_by_media[mt] << block[:rules]\n        end\n      end\n\n      rules_by_media\n    end\n\n    # Merge declarations with the same selector.\n    def compact! # :nodoc:\n      []\n    end\n\n    def parse_block_into_rule_sets!(block, options = {}) # :nodoc:\n      current_media_queries = [:all]\n      if options[:media_types]\n        current_media_queries = options[:media_types].flatten.collect { |mt| CssParser.sanitize_media_query(mt) }\n      end\n\n      in_declarations = 0\n      block_depth = 0\n\n      in_charset = false # @charset is ignored for now\n      in_string = false\n      in_at_media_rule = false\n      in_media_block = false\n\n      current_selectors = +''\n      current_media_query = +''\n      current_declarations = +''\n\n      # once we are in a rule, we will use this to store where we started if we are capturing offsets\n      rule_start = nil\n      start_offset = nil\n      end_offset = nil\n\n      scanner = StringScanner.new(block)\n      until scanner.eos?\n        # save the regex offset so that we know where in the file we are\n        start_offset = scanner.pos\n        token = scanner.scan(RULESET_TOKENIZER_RX)\n        end_offset = scanner.pos\n\n        if token.start_with?('\"') # found un-escaped double quote\n          in_string = !in_string\n        end\n\n        if in_declarations > 0\n          # too deep, malformed declaration block\n          if in_declarations > 1\n            in_declarations -= 1 if token.include?('}')\n            next\n          end\n\n          if !in_string && token.include?('{')\n            in_declarations += 1\n            next\n          end\n\n          current_declarations << token\n\n          if !in_string && token.include?('}')\n            current_declarations.gsub!(/\\}\\s*$/, '')\n\n            in_declarations -= 1\n            current_declarations.strip!\n\n            unless current_declarations.empty?\n              add_rule_options = {\n                selectors: current_selectors, block: current_declarations,\n                media_types: current_media_queries\n              }\n              if options[:capture_offsets]\n                add_rule_options[:filename] = options[:filename]\n                add_rule_options[:offset] = rule_start..end_offset\n              end\n              add_rule!(**add_rule_options)\n            end\n\n            current_selectors = +''\n            current_declarations = +''\n\n            # restart our search for selectors and declarations\n            rule_start = nil if options[:capture_offsets]\n          end\n        elsif /@media/i.match?(token)\n          # found '@media', reset current media_types\n          in_at_media_rule = true\n          current_media_queries = []\n        elsif in_at_media_rule\n          if token.include?('{')\n            block_depth += 1\n            in_at_media_rule = false\n            in_media_block = true\n            current_media_queries << CssParser.sanitize_media_query(current_media_query)\n            current_media_query = +''\n          elsif token.include?(',')\n            # new media query begins\n            token.tr!(',', ' ')\n            token.strip!\n            current_media_query << token << ' '\n            current_media_queries << CssParser.sanitize_media_query(current_media_query)\n            current_media_query = +''\n          else\n            token.strip!\n            # special-case the ( and ) tokens to remove inner-whitespace\n            # (eg we'd prefer '(width: 500px)' to '( width: 500px )' )\n            case token\n            when '('\n              current_media_query << token\n            when ')'\n              current_media_query.sub!(/ ?$/, token)\n            else\n              current_media_query << token << ' '\n            end\n          end\n        elsif in_charset or /@charset/i.match?(token)\n          # iterate until we are out of the charset declaration\n          in_charset = !token.include?(';')\n        elsif !in_string && token.include?('}')\n          block_depth -= 1\n\n          # reset the current media query scope\n          if in_media_block\n            current_media_queries = [:all]\n            in_media_block = false\n          end\n        elsif !in_string && token.include?('{')\n          current_selectors.strip!\n          in_declarations += 1\n        else\n          # if we are in a selector, add the token to the current selectors\n          current_selectors << token\n\n          # mark this as the beginning of the selector unless we have already marked it\n          rule_start = start_offset if options[:capture_offsets] && rule_start.nil? && /^[^\\s]+$/.match?(token)\n        end\n      end\n\n      # check for unclosed braces\n      return unless in_declarations > 0\n\n      add_rule_options = {\n        selectors: current_selectors, block: current_declarations,\n        media_types: current_media_queries\n      }\n      if options[:capture_offsets]\n        add_rule_options[:filename] = options[:filename]\n        add_rule_options[:offset] = rule_start..end_offset\n      end\n      add_rule!(**add_rule_options)\n    end\n\n    # Load a remote CSS file.\n    #\n    # You can also pass in file://test.css\n    #\n    # See add_block! for options.\n    #\n    # Deprecated: originally accepted three params: `uri`, `base_uri` and `media_types`\n    def load_uri!(uri, options = {}, deprecated = nil)\n      uri = Addressable::URI.parse(uri) unless uri.respond_to? :scheme\n\n      opts = {base_uri: nil, media_types: :all}\n\n      if options.is_a? Hash\n        opts.merge!(options)\n      else\n        warn '[DEPRECATION] `load_uri!` with positional arguments is deprecated. ' \\\n             'Please use keyword arguments instead.', uplevel: 1\n        opts[:base_uri] = options if options.is_a? String\n        opts[:media_types] = deprecated if deprecated\n      end\n\n      if uri.scheme == 'file' or uri.scheme.nil?\n        uri.path = File.expand_path(uri.path)\n        uri.scheme = 'file'\n      end\n\n      opts[:base_uri] = uri if opts[:base_uri].nil?\n\n      # pass on the uri if we are capturing file offsets\n      opts[:filename] = uri.to_s if opts[:capture_offsets]\n\n      src, = read_remote_file(uri) # skip charset\n\n      add_block!(src, opts) if src\n    end\n\n    # Load a local CSS file.\n    def load_file!(file_name, options = {}, deprecated = nil)\n      opts = {base_dir: nil, media_types: :all}\n\n      if options.is_a? Hash\n        opts.merge!(options)\n      else\n        warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \\\n             'Please use keyword arguments instead.', uplevel: 1\n        opts[:base_dir] = options if options.is_a? String\n        opts[:media_types] = deprecated if deprecated\n      end\n\n      file_name = File.expand_path(file_name, opts[:base_dir])\n      return unless File.readable?(file_name)\n      return unless circular_reference_check(file_name)\n\n      src = File.read(file_name)\n\n      opts[:filename] = file_name if opts[:capture_offsets]\n      opts[:base_dir] = File.dirname(file_name)\n\n      add_block!(src, opts)\n    end\n\n    # Load a local CSS string.\n    def load_string!(src, options = {}, deprecated = nil)\n      opts = {base_dir: nil, media_types: :all}\n\n      if options.is_a? Hash\n        opts.merge!(options)\n      else\n        warn '[DEPRECATION] `load_file!` with positional arguments is deprecated. ' \\\n             'Please use keyword arguments instead.', uplevel: 1\n        opts[:base_dir] = options if options.is_a? String\n        opts[:media_types] = deprecated if deprecated\n      end\n\n      add_block!(src, opts)\n    end\n\n  protected\n\n    # Check that a path hasn't been loaded already\n    #\n    # Raises a CircularReferenceError exception if io_exceptions are on,\n    # otherwise returns true/false.\n    # TODO: fix rubocop\n    def circular_reference_check(path) # rubocop:disable Naming/PredicateMethod\n      path = path.to_s\n      if @loaded_uris.include?(path)\n        raise CircularReferenceError, \"can't load #{path} more than once\" if @options[:io_exceptions]\n\n        false\n      else\n        @loaded_uris << path\n        true\n      end\n    end\n\n    # Remove a pattern from a given string\n    #\n    # Returns a string.\n    def ignore_pattern(css, regex, options)\n      # if we are capturing file offsets, replace the characters with spaces to retail the original positions\n      return css.gsub(regex) { |m| ' ' * m.length } if options[:capture_offsets]\n\n      # otherwise just strip it out\n      css.gsub(regex, '')\n    end\n\n    # Strip comments and clean up blank lines from a block of CSS.\n    #\n    # Returns a string.\n    def cleanup_block(block, options = {}) # :nodoc:\n      # Strip CSS comments\n      utf8_block = block.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: ' ')\n      utf8_block = ignore_pattern(utf8_block, STRIP_CSS_COMMENTS_RX, options)\n\n      # Strip HTML comments - they shouldn't really be in here but\n      # some people are just crazy...\n      utf8_block = ignore_pattern(utf8_block, STRIP_HTML_COMMENTS_RX, options)\n\n      # Strip lines containing just whitespace\n      utf8_block.gsub!(/^\\s+$/, '') unless options[:capture_offsets]\n\n      utf8_block\n    end\n\n    # Download a file into a string.\n    #\n    # Returns the file's data and character set in an array.\n    #--\n    # TODO: add option to fail silently or throw and exception on a 404\n    #++\n    def read_remote_file(uri) # :nodoc:\n      if @redirect_count.nil?\n        @redirect_count = 0\n      else\n        @redirect_count += 1\n      end\n\n      unless circular_reference_check(uri.to_s)\n        @redirect_count = nil\n        return nil, nil\n      end\n\n      if @redirect_count > MAX_REDIRECTS\n        @redirect_count = nil\n        return nil, nil\n      end\n\n      src = '', charset = nil\n\n      begin\n        uri = Addressable::URI.parse(uri.to_s)\n\n        if uri.scheme == 'file'\n          # local file\n          path = uri.path\n          path.gsub!(%r{^/}, '') if Gem.win_platform?\n          src = File.read(path, mode: 'rb')\n        else\n          # remote file\n          if uri.scheme == 'https'\n            uri.port = 443 unless uri.port\n            http = Net::HTTP.new(uri.host, uri.port)\n            http.use_ssl = true\n          else\n            http = Net::HTTP.new(uri.host, uri.port)\n          end\n\n          res = http.get(uri.request_uri, {'User-Agent' => @options[:user_agent], 'Accept-Encoding' => 'gzip'})\n          src = res.body\n          charset = res.respond_to?(:charset) ? res.encoding : 'utf-8'\n\n          if res.code.to_i >= 400\n            @redirect_count = nil\n            raise RemoteFileError, uri.to_s if @options[:io_exceptions]\n\n            return '', nil\n          elsif res.code.to_i >= 300 and res.code.to_i < 400\n            unless res['Location'].nil?\n              return read_remote_file Addressable::URI.parse(Addressable::URI.escape(res['Location']))\n            end\n          end\n\n          case res['content-encoding']\n          when 'gzip'\n            io = Zlib::GzipReader.new(StringIO.new(res.body))\n            src = io.read\n          when 'deflate'\n            io = Zlib::Inflate.new\n            src = io.inflate(res.body)\n          end\n        end\n\n        if charset\n          src.encode!('UTF-8', charset)\n        end\n      rescue\n        @redirect_count = nil\n        raise RemoteFileError, uri.to_s if @options[:io_exceptions]\n\n        return nil, nil\n      end\n\n      @redirect_count = nil\n      [src, charset]\n    end\n\n  private\n\n    # Save a folded declaration block to the internal cache.\n    def save_folded_declaration(block_hash, folded_declaration) # :nodoc:\n      @folded_declaration_cache[block_hash] = folded_declaration\n    end\n\n    # Retrieve a folded declaration block from the internal cache.\n    def get_folded_declaration(block_hash) # :nodoc:\n      @folded_declaration_cache[block_hash] ||= nil\n    end\n\n    def reset! # :nodoc:\n      @folded_declaration_cache = {}\n      @css_source = ''\n      @css_rules = []\n      @css_warnings = []\n    end\n\n    # recurse through nested nodes and return them as Hashes nested in\n    # passed hash\n    def css_node_to_h(hash, key, val)\n      hash[key.strip] = '' and return hash if val.nil?\n\n      lines = val.split(';')\n      nodes = {}\n      lines.each do |line|\n        parts = line.split(':', 2)\n        if parts[1].include?(':')\n          nodes[parts[0]] = css_node_to_h(hash, parts[0], parts[1])\n        else\n          nodes[parts[0].to_s.strip] = parts[1].to_s.strip\n        end\n      end\n      hash[key.strip] = nodes\n      hash\n    end\n  end\nend\n"
  },
  {
    "path": "lib/css_parser/regexps.rb",
    "content": "# frozen_string_literal: true\n\nmodule CssParser\n  def self.regex_possible_values(*values)\n    Regexp.new(\"([\\s]*^)?(#{values.join('|')})([\\s]*$)?\", 'i')\n  end\n\n  # :stopdoc:\n  # Base types\n  RE_NL = Regexp.new('(\\n|\\r\\n|\\r|\\f)')\n  RE_NON_ASCII = Regexp.new('([\\x00-\\xFF])', Regexp::IGNORECASE | Regexp::NOENCODING) # [^\\0-\\177]\n  RE_UNICODE = Regexp.new('(\\\\\\\\[0-9a-f]{1,6}(\\r\\n|[ \\n\\r\\t\\f])*)', Regexp::IGNORECASE | Regexp::EXTENDED | Regexp::MULTILINE | Regexp::NOENCODING)\n  RE_ESCAPE = Regexp.union(RE_UNICODE, '|(\\\\\\\\[^\\n\\r\\f0-9a-f])')\n  RE_IDENT = Regexp.new(\"[-]?([_a-z]|#{RE_NON_ASCII}|#{RE_ESCAPE})([_a-z0-9-]|#{RE_NON_ASCII}|#{RE_ESCAPE})*\", Regexp::IGNORECASE | Regexp::NOENCODING)\n\n  # General strings\n  RE_STRING1 = /(\"(.[^\\n\\r\\f\"]*|\\\\#{RE_NL}|#{RE_ESCAPE})*\")/.freeze\n  RE_STRING2 = /('(.[^\\n\\r\\f']*|\\\\#{RE_NL}|#{RE_ESCAPE})*')/.freeze\n  RE_STRING = Regexp.union(RE_STRING1, RE_STRING2)\n\n  RE_INHERIT = regex_possible_values 'inherit'\n\n  RE_URI = /(url\\(\\s*(\\s*#{RE_STRING}\\s*)\\s*\\))|(url\\(\\s*([!#$%&*\\-~]|#{RE_NON_ASCII}|#{RE_ESCAPE})*\\s*)\\)/ixm.freeze\n  URI_RX = /url\\((\"([^\"]*)\"|'([^']*)'|([^)]*))\\)/im.freeze\n  URI_RX_OR_NONE = Regexp.union(URI_RX, /none/i)\n  RE_GRADIENT = /[-a-z]*gradient\\([-a-z0-9 .,#%()]*\\)/im.freeze\n\n  # Initial parsing\n  RE_AT_IMPORT_RULE = /@import\\s+(url\\()?[\"']?(.[^'\"\\s]*)[\"']?\\)?([\\w\\s,^\\])]*)\\)?;?/.freeze\n\n  #--\n  # RE_AT_MEDIA_RULE = Regexp.new('(\\\"(.[^\\n\\r\\f\\\\\"]*|\\\\\\\\' + RE_NL.to_s + '|' + RE_ESCAPE.to_s + ')*\\\")')\n\n  # 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\n  #++\n  IMPORTANT_IN_PROPERTY_RX = /\\s*!important\\b\\s*/i.freeze\n\n  RE_INSIDE_OUTSIDE = regex_possible_values 'inside', 'outside'\n  RE_SCROLL_FIXED = regex_possible_values 'scroll', 'fixed'\n  RE_REPEAT = regex_possible_values 'repeat(\\-x|\\-y)*|no\\-repeat'\n  RE_LIST_STYLE_TYPE = regex_possible_values(\n    'disc', 'circle', 'square', 'decimal-leading-zero', 'decimal', 'lower-roman',\n    'upper-roman', 'lower-greek', 'lower-alpha', 'lower-latin', 'upper-alpha',\n    'upper-latin', 'hebrew', 'armenian', 'georgian', 'cjk-ideographic', 'hiragana',\n    'hira-gana-iroha', 'katakana-iroha', 'katakana', 'none'\n  )\n  RE_IMAGE = Regexp.union(CssParser::URI_RX, CssParser::RE_GRADIENT, /none/i)\n\n  STRIP_CSS_COMMENTS_RX = %r{/\\*.*?\\*/}m.freeze\n  STRIP_HTML_COMMENTS_RX = /<!--|-->/m.freeze\n\n  # Special units\n  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\n  RE_LENGTH_OR_PERCENTAGE = Regexp.new('([\\-]*(([0-9]*\\.[0-9]+)|[0-9]+)(e[mx]+|px|[cm]+m|p[tc+]|in|\\%))', Regexp::IGNORECASE)\n  RE_SINGLE_BACKGROUND_POSITION = /#{RE_LENGTH_OR_PERCENTAGE}|left|center|right|top|bottom/i.freeze\n  RE_SINGLE_BACKGROUND_SIZE = /#{RE_LENGTH_OR_PERCENTAGE}|auto|cover|contain|initial|inherit/i.freeze\n  RE_BACKGROUND_POSITION = /#{RE_SINGLE_BACKGROUND_POSITION}\\s+#{RE_SINGLE_BACKGROUND_POSITION}|#{RE_SINGLE_BACKGROUND_POSITION}/.freeze\n  RE_BACKGROUND_SIZE = %r{\\s*/\\s*(#{RE_SINGLE_BACKGROUND_SIZE}\\s+#{RE_SINGLE_BACKGROUND_SIZE}|#{RE_SINGLE_BACKGROUND_SIZE})}.freeze\n  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\n  RE_BORDER_STYLE = /(\\s*^)?(none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset)(\\s*$)?/imx.freeze\n  RE_BORDER_UNITS = Regexp.union(BOX_MODEL_UNITS_RX, /(thin|medium|thick)/i)\n\n  # Functions like calc, var, clamp, etc.\n  RE_FUNCTIONS = /\n    (\n      [a-z0-9-]+        # function name\n    )\n    (?>\n      \\(                # opening parenthesis\n        (?:\n          ([^()]+)\n          |             # recursion via subexpression\n          \\g<0>\n        )*\n      \\)                # closing parenthesis\n    )\n  /imx.freeze\n\n  # Patterns for specificity calculations\n  NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC = /\n    (?:\\.\\w+)                     # classes\n    |\n    \\[(?:\\w+)                       # attributes\n    |\n    (?::(?:                          # pseudo classes\n      link|visited|active\n      |hover|focus\n      |lang\n      |target\n      |enabled|disabled|checked|indeterminate\n      |root\n      |nth-child|nth-last-child|nth-of-type|nth-last-of-type\n      |first-child|last-child|first-of-type|last-of-type\n      |only-child|only-of-type\n      |empty|contains\n    ))\n  /ix.freeze\n  ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC = /\n    (?:(?:^|[\\s+>~]+)\\w+       # elements\n    |\n    :{1,2}(?:                    # pseudo-elements\n      after|before\n      |first-letter|first-line\n      |selection\n    )\n  )/ix.freeze\n\n  # Colours\n  NAMED_COLOURS = %w[\n    aliceblue\n    antiquewhite\n    aqua\n    aquamarine\n    azure\n    beige\n    bisque\n    black\n    blanchedalmond\n    blue\n    blueviolet\n    brown\n    burlywood\n    cadetblue\n    chartreuse\n    chocolate\n    coral\n    cornflowerblue\n    cornsilk\n    crimson\n    cyan\n    darkblue\n    darkcyan\n    darkgoldenrod\n    darkgray\n    darkgreen\n    darkgrey\n    darkkhaki\n    darkmagenta\n    darkolivegreen\n    darkorange\n    darkorchid\n    darkred\n    darksalmon\n    darkseagreen\n    darkslateblue\n    darkslategray\n    darkslategrey\n    darkturquoise\n    darkviolet\n    deeppink\n    deepskyblue\n    dimgray\n    dimgrey\n    dodgerblue\n    firebrick\n    floralwhite\n    forestgreen\n    fuchsia\n    gainsboro\n    ghostwhite\n    gold\n    goldenrod\n    gray\n    green\n    greenyellow\n    grey\n    honeydew\n    hotpink\n    indianred\n    indigo\n    ivory\n    khaki\n    lavender\n    lavenderblush\n    lawngreen\n    lemonchiffon\n    lightblue\n    lightcoral\n    lightcyan\n    lightgoldenrodyellow\n    lightgray\n    lightgreen\n    lightgrey\n    lightpink\n    lightsalmon\n    lightseagreen\n    lightskyblue\n    lightslategray\n    lightslategrey\n    lightsteelblue\n    lightyellow\n    lime\n    limegreen\n    linen\n    magenta\n    maroon\n    mediumaquamarine\n    mediumblue\n    mediumorchid\n    mediumpurple\n    mediumseagreen\n    mediumslateblue\n    mediumspringgreen\n    mediumturquoise\n    mediumvioletred\n    midnightblue\n    mintcream\n    mistyrose\n    moccasin\n    navajowhite\n    navy\n    oldlace\n    olive\n    olivedrab\n    orange\n    orangered\n    orchid\n    palegoldenrod\n    palegreen\n    paleturquoise\n    palevioletred\n    papayawhip\n    peachpuff\n    peru\n    pink\n    plum\n    powderblue\n    purple\n    red\n    rosybrown\n    royalblue\n    saddlebrown\n    salmon\n    sandybrown\n    seagreen\n    seashell\n    sienna\n    silver\n    skyblue\n    slateblue\n    slategray\n    slategrey\n    snow\n    springgreen\n    steelblue\n    tan\n    teal\n    thistle\n    tomato\n    turquoise\n    violet\n    wheat\n    white\n    whitesmoke\n    yellow\n    yellowgreen\n\n    transparent\n    inherit\n    currentColor\n  ].freeze\n  # CSS <number> allows the integer part to be omitted (e.g. `.1`), per CSS Values & Units.\n  # `(?:\\d*\\.)?\\d+` accepts `1`, `1.5`, and `.5` while still rejecting bare `1.`.\n  RE_COLOUR_NUMERIC = /\\b(hsl|rgb)\\s*\\(-?\\s*-?(?:\\d*\\.)?\\d+%?\\s*%?,-?\\s*-?(?:\\d*\\.)?\\d+%?\\s*%?,-?\\s*-?(?:\\d*\\.)?\\d+%?\\s*%?\\)/i.freeze\n  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\n  RE_COLOUR_HEX = /\\s*#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})\\b/.freeze\n  RE_COLOUR_NAMED = /\\s*\\b(#{NAMED_COLOURS.join('|')})\\b/i.freeze\n  RE_COLOUR = Regexp.union(RE_COLOUR_NUMERIC, RE_COLOUR_NUMERIC_ALPHA, RE_COLOUR_HEX, RE_COLOUR_NAMED)\n  # :startdoc:\nend\n"
  },
  {
    "path": "lib/css_parser/rule_set.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'forwardable'\n\nmodule CssParser\n  class RuleSet\n    # Patterns for specificity calculations\n    RE_ELEMENTS_AND_PSEUDO_ELEMENTS = /((^|[\\s+>]+)\\w+|:(first-line|first-letter|before|after))/i.freeze\n    RE_NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES = /(\\.\\w+)|(\\[\\w+)|(:(link|first-child|lang))/i.freeze\n\n    BACKGROUND_PROPERTIES = ['background-color', 'background-image', 'background-repeat', 'background-position', 'background-size', 'background-attachment'].freeze\n    LIST_STYLE_PROPERTIES = ['list-style-type', 'list-style-position', 'list-style-image'].freeze\n    FONT_STYLE_PROPERTIES = ['font-style', 'font-variant', 'font-weight', 'font-size', 'line-height', 'font-family'].freeze\n    FONT_WEIGHT_PROPERTIES = ['font-style', 'font-weight', 'font-variant'].freeze\n    BORDER_STYLE_PROPERTIES = ['border-width', 'border-style', 'border-color'].freeze\n    BORDER_PROPERTIES = ['border', 'border-left', 'border-right', 'border-top', 'border-bottom'].freeze\n    DIMENSION_DIRECTIONS = [:top, :right, :bottom, :left].freeze\n\n    NUMBER_OF_DIMENSIONS = 4\n\n    DIMENSIONS = [\n      ['margin', %w[margin-top margin-right margin-bottom margin-left]],\n      ['padding', %w[padding-top padding-right padding-bottom padding-left]],\n      ['border-color', %w[border-top-color border-right-color border-bottom-color border-left-color]],\n      ['border-style', %w[border-top-style border-right-style border-bottom-style border-left-style]],\n      ['border-width', %w[border-top-width border-right-width border-bottom-width border-left-width]]\n    ].freeze\n\n    WHITESPACE_REPLACEMENT = '___SPACE___'\n\n    # Tokens for parse_declarations!\n    COLON = ':'.freeze\n    SEMICOLON = ';'.freeze\n    LPAREN = '('.freeze\n    RPAREN = ')'.freeze\n    IMPORTANT = '!important'.freeze\n    class Declarations\n      class Value\n        attr_reader :value\n        attr_accessor :important\n\n        def initialize(value, important: nil)\n          self.value = value\n          @important = important unless important.nil?\n        end\n\n        def value=(value)\n          value = value.to_s.sub(/\\s*;\\s*\\Z/, '')\n          self.important = !value.slice!(CssParser::IMPORTANT_IN_PROPERTY_RX).nil?\n          value.strip!\n          raise ArgumentError, 'value is empty' if value.empty?\n\n          @value = value.freeze\n        end\n\n        def to_s\n          important ? \"#{value} !important\" : value\n        end\n\n        def ==(other)\n          return false unless other.is_a?(self.class)\n\n          value == other.value && important == other.important\n        end\n      end\n\n      extend Forwardable\n\n      def_delegators :declarations, :each, :each_value\n\n      def initialize(declarations = {})\n        self.declarations = {}\n        declarations.each { |property, value| add_declaration!(property, value) }\n      end\n\n      # Add a CSS declaration\n      # @param [#to_s] property that should be added\n      # @param [Value, #to_s] value of the property\n      #\n      # @example\n      #   declarations['color'] = 'blue'\n      #\n      #   puts declarations['color']\n      #   => #<CssParser::RuleSet::Declarations::Value:0x000000000305c730 @important=false, @order=1, @value=\"blue\">\n      #\n      # @example\n      #   declarations['margin'] = '0px auto !important'\n      #\n      #   puts declarations['margin']\n      #   => #<CssParser::RuleSet::Declarations::Value:0x00000000030c1838 @important=true, @order=2, @value=\"0px auto\">\n      #\n      # If the property already exists its value will be over-written unless it was !important and the new value\n      # is not !important.\n      # If the value is empty - property will be deleted\n      def []=(property, value)\n        property = normalize_property(property)\n        currently_important = declarations[property]&.important\n\n        if value.is_a?(Value) && (!currently_important || value.important)\n          declarations[property] = value\n        elsif value.to_s.strip.empty?\n          delete property\n        else\n          value = Value.new(value)\n          declarations[property] = value if !currently_important || value.important\n        end\n      rescue ArgumentError => e\n        raise e.exception, \"#{property} #{e.message}\"\n      end\n      alias add_declaration! []=\n\n      def [](property)\n        declarations[normalize_property(property)]\n      end\n      alias get_value []\n\n      def key?(property)\n        declarations.key?(normalize_property(property))\n      end\n\n      def size\n        declarations.size\n      end\n\n      # Remove CSS declaration\n      # @param [#to_s] property property to be removed\n      #\n      # @example\n      #   declarations.delete('color')\n      def delete(property)\n        declarations.delete(normalize_property(property))\n      end\n      alias remove_declaration! delete\n\n      # Replace CSS property with multiple declarations\n      # @param [#to_s] property property name to be replaces\n      # @param [Hash<String => [String, Value]>] replacements hash with properties to replace with\n      #\n      # @example\n      #  declarations = Declarations.new('line-height' => '0.25px', 'font' => 'small-caps', 'font-size' => '12em')\n      #  declarations.replace_declaration!('font', {'line-height' => '1px', 'font-variant' => 'small-caps', 'font-size' => '24px'})\n      #  declarations\n      #  => #<CssParser::RuleSet::Declarations:0x00000000029c3018\n      #  @declarations=\n      #  {\"line-height\"=>#<CssParser::RuleSet::Declarations::Value:0x00000000038ac458 @important=false, @value=\"1px\">,\n      #   \"font-variant\"=>#<CssParser::RuleSet::Declarations::Value:0x00000000039b3ec8 @important=false, @value=\"small-caps\">,\n      #   \"font-size\"=>#<CssParser::RuleSet::Declarations::Value:0x00000000029c2c80 @important=false, @value=\"12em\">}>\n      def replace_declaration!(property, replacements, preserve_importance: false)\n        property = normalize_property(property)\n        raise ArgumentError, \"property #{property} does not exist\" unless key?(property)\n\n        replacement_declarations = self.class.new(replacements)\n\n        if preserve_importance\n          importance = get_value(property).important\n          replacement_declarations.each_value { |value| value.important = importance }\n        end\n\n        replacement_keys = declarations.keys\n        replacement_values = declarations.values\n        property_index = replacement_keys.index(property)\n\n        # We should preserve subsequent declarations of the same properties\n        # and prior important ones if replacement one is not important\n        replacements = replacement_declarations.each.with_object({}) do |(key, replacement), result|\n          existing = declarations[key]\n\n          # No existing -> set\n          unless existing\n            result[key] = replacement\n            next\n          end\n\n          # Replacement more important than existing -> replace\n          if replacement.important && !existing.important\n            result[key] = replacement\n            replaced_index = replacement_keys.index(key)\n            replacement_keys.delete_at(replaced_index)\n            replacement_values.delete_at(replaced_index)\n            property_index -= 1 if replaced_index < property_index\n            next\n          end\n\n          # Existing is more important than replacement -> keep\n          next if !replacement.important && existing.important\n\n          # Existing and replacement importance are the same,\n          # value which is declared later wins\n          result[key] = replacement if property_index > replacement_keys.index(key)\n        end\n\n        return if replacements.empty?\n\n        replacement_keys.delete_at(property_index)\n        replacement_keys.insert(property_index, *replacements.keys)\n\n        replacement_values.delete_at(property_index)\n        replacement_values.insert(property_index, *replacements.values)\n\n        self.declarations = replacement_keys.zip(replacement_values).to_h\n      end\n\n      def to_s(options = {})\n        str = declarations.reduce(+'') do |memo, (prop, value)|\n          importance = options[:force_important] || value.important ? ' !important' : ''\n          memo << \"#{prop}: #{value.value}#{importance}; \"\n        end\n        # TODO: Clean-up regexp doesn't seem to work\n        str.gsub!(/^[\\s^({)]+|[\\n\\r\\f\\t]*|\\s+$/mx, '')\n        str.strip!\n        str\n      end\n\n      def ==(other)\n        return false unless other.is_a?(self.class)\n\n        declarations == other.declarations && declarations.keys == other.declarations.keys\n      end\n\n    protected\n\n      attr_reader :declarations\n\n    private\n\n      attr_writer :declarations\n\n      def normalize_property(property)\n        property = property.to_s.downcase\n        property.strip!\n        property\n      end\n    end\n\n    extend Forwardable\n\n    # optional field for storing source reference\n    # File offset range\n    attr_reader :offset\n    # the local or remote location\n    attr_accessor :filename\n\n    # Array of selector strings.\n    attr_reader :selectors\n\n    # Integer with the specificity to use for this RuleSet.\n    attr_accessor :specificity\n\n    # @!method add_declaration!\n    #   @see CssParser::RuleSet::Declarations#add_declaration!\n    # @!method delete\n    #   @see CssParser::RuleSet::Declarations#delete\n    def_delegators :declarations, :add_declaration!, :delete\n    alias []= add_declaration!\n    alias remove_declaration! delete\n\n    def initialize(*args, selectors: nil, block: nil, offset: nil, filename: nil, specificity: nil) # rubocop:disable Metrics/ParameterLists\n      if args.any?\n        if selectors || block || offset || filename || specificity\n          raise ArgumentError, \"don't mix positional and keyword arguments\"\n        end\n\n        warn '[DEPRECATION] positional arguments are deprecated use keyword instead.', uplevel: 1\n\n        case args.length\n        when 2\n          selectors, block = args\n        when 3\n          selectors, block, specificity = args\n        when 4\n          filename, offset, selectors, block = args\n        when 5\n          filename, offset, selectors, block, specificity = args\n        else\n          raise ArgumentError\n        end\n      end\n\n      @selectors = []\n      @specificity = specificity\n\n      unless offset.nil? == filename.nil?\n        raise ArgumentError, 'require both offset and filename or no offset and no filename'\n      end\n\n      @offset = offset\n      @filename = filename\n\n      parse_selectors!(selectors) if selectors\n      parse_declarations!(block)\n    end\n\n    # Get the value of a property\n    def get_value(property)\n      return '' unless (value = declarations[property])\n\n      \"#{value};\"\n    end\n    alias [] get_value\n\n    # Iterate through selectors.\n    #\n    # Options\n    # -  +force_important+ -- boolean\n    #\n    # ==== Example\n    #   ruleset.each_selector do |sel, dec, spec|\n    #     ...\n    #   end\n    def each_selector(options = {}) # :yields: selector, declarations, specificity\n      decs = declarations.to_s(options)\n      if @specificity\n        @selectors.each { |sel| yield sel.strip, decs, @specificity }\n      else\n        @selectors.each { |sel| yield sel.strip, decs, CssParser.calculate_specificity(sel) }\n      end\n    end\n\n    # Iterate through declarations.\n    def each_declaration # :yields: property, value, is_important\n      declarations.each do |property_name, value|\n        yield property_name, value.value, value.important\n      end\n    end\n\n    # Return all declarations as a string.\n    def declarations_to_s(options = {})\n      declarations.to_s(options)\n    end\n\n    # Return the CSS rule set as a string.\n    def to_s\n      \"#{@selectors.join(',')} { #{declarations} }\"\n    end\n\n    # Split shorthand declarations (e.g. +margin+ or +font+) into their constituent parts.\n    def expand_shorthand!\n      # border must be expanded before dimensions\n      expand_border_shorthand!\n      expand_dimensions_shorthand!\n      expand_font_shorthand!\n      expand_background_shorthand!\n      expand_list_style_shorthand!\n    end\n\n    # Convert shorthand background declarations (e.g. <tt>background: url(\"chess.png\") gray 50% repeat fixed;</tt>)\n    # into their constituent parts.\n    #\n    # See http://www.w3.org/TR/CSS21/colors.html#propdef-background\n    def expand_background_shorthand! # :nodoc:\n      return unless (declaration = declarations['background'])\n\n      value = declaration.value.dup\n\n      replacement =\n        if value.match(CssParser::RE_INHERIT)\n          BACKGROUND_PROPERTIES.to_h { |key| [key, 'inherit'] }\n        else\n          {\n            'background-image' => value.slice!(CssParser::RE_IMAGE),\n            'background-attachment' => value.slice!(CssParser::RE_SCROLL_FIXED),\n            'background-repeat' => value.slice!(CssParser::RE_REPEAT),\n            'background-color' => value.slice!(CssParser::RE_COLOUR),\n            'background-size' => extract_background_size_from(value),\n            'background-position' => value.slice!(CssParser::RE_BACKGROUND_POSITION)\n          }\n        end\n\n      declarations.replace_declaration!('background', replacement, preserve_importance: true)\n    end\n\n    def extract_background_size_from(value)\n      size = value.slice!(CssParser::RE_BACKGROUND_SIZE)\n\n      size.sub(%r{^\\s*/\\s*}, '') if size\n    end\n\n    # Split shorthand border declarations (e.g. <tt>border: 1px red;</tt>)\n    # Additional splitting happens in expand_dimensions_shorthand!\n    def expand_border_shorthand! # :nodoc:\n      BORDER_PROPERTIES.each do |k|\n        next unless (declaration = declarations[k])\n\n        value = declaration.value.dup\n\n        replacement = {\n          \"#{k}-width\" => value.slice!(CssParser::RE_BORDER_UNITS),\n          \"#{k}-color\" => value.slice!(CssParser::RE_COLOUR),\n          \"#{k}-style\" => value.slice!(CssParser::RE_BORDER_STYLE)\n        }\n\n        declarations.replace_declaration!(k, replacement, preserve_importance: true)\n      end\n    end\n\n    # Split shorthand dimensional declarations (e.g. <tt>margin: 0px auto;</tt>)\n    # into their constituent parts.  Handles margin, padding, border-color, border-style and border-width.\n    def expand_dimensions_shorthand! # :nodoc:\n      DIMENSIONS.each do |property, (top, right, bottom, left)|\n        next unless (declaration = declarations[property])\n\n        value = declaration.value.dup\n\n        # RGB and HSL values in borders are the only units that can have spaces (within params).\n        # We cheat a bit here by stripping spaces after commas in RGB and HSL values so that we\n        # can split easily on spaces.\n        #\n        # TODO: rgba, hsl, hsla\n        value.gsub!(RE_COLOUR) { |c| c.gsub(/(\\s*,\\s*)/, ',') }\n\n        matches = split_value_preserving_function_whitespace(value)\n\n        case matches.length\n        when 1\n          values = matches.to_a * 4\n        when 2\n          values = matches.to_a * 2\n        when 3\n          values = matches.to_a\n          values << matches[1] # left = right\n        when 4\n          values = matches.to_a\n        else\n          raise ArgumentError, \"Cannot parse #{value}\"\n        end\n\n        replacement = [top, right, bottom, left].zip(values).to_h\n\n        declarations.replace_declaration!(property, replacement, preserve_importance: true)\n      end\n    end\n\n    # Convert shorthand font declarations (e.g. <tt>font: 300 italic 11px/14px verdana, helvetica, sans-serif;</tt>)\n    # into their constituent parts.\n    def expand_font_shorthand! # :nodoc:\n      return unless (declaration = declarations['font'])\n\n      # reset properties to 'normal' per http://www.w3.org/TR/CSS21/fonts.html#font-shorthand\n      font_props = {\n        'font-style' => 'normal',\n        'font-variant' => 'normal',\n        'font-weight' => 'normal',\n        'font-size' => 'normal',\n        'line-height' => 'normal'\n      }\n\n      value = declaration.value.dup\n      value.gsub!(%r{/\\s+}, '/') # handle spaces between font size and height shorthand (e.g. 14px/ 16px)\n\n      in_fonts = false\n\n      matches = value.scan(/\"(?:.*[^\"])\"|'(?:.*[^'])'|(?:\\w[^ ,]+)/)\n      matches.each do |m|\n        m.strip!\n        m.gsub!(/;$/, '')\n\n        if in_fonts\n          if font_props.key?('font-family')\n            font_props['font-family'] += \", #{m}\"\n          else\n            font_props['font-family'] = m\n          end\n        elsif /normal|inherit/i.match?(m)\n          FONT_WEIGHT_PROPERTIES.each do |font_prop|\n            font_props[font_prop] ||= m\n          end\n        elsif /italic|oblique/i.match?(m)\n          font_props['font-style'] = m\n        elsif /small-caps/i.match?(m)\n          font_props['font-variant'] = m\n        elsif /[1-9]00$|bold|bolder|lighter/i.match?(m)\n          font_props['font-weight'] = m\n        elsif CssParser::FONT_UNITS_RX.match?(m)\n          if m.include?('/')\n            font_props['font-size'], font_props['line-height'] = m.split('/', 2)\n          else\n            font_props['font-size'] = m\n          end\n          in_fonts = true\n        end\n      end\n\n      declarations.replace_declaration!('font', font_props, preserve_importance: true)\n    end\n\n    # Convert shorthand list-style declarations (e.g. <tt>list-style: lower-alpha outside;</tt>)\n    # into their constituent parts.\n    #\n    # See http://www.w3.org/TR/CSS21/generate.html#lists\n    def expand_list_style_shorthand! # :nodoc:\n      return unless (declaration = declarations['list-style'])\n\n      value = declaration.value.dup\n\n      replacement =\n        if CssParser::RE_INHERIT.match?(value)\n          LIST_STYLE_PROPERTIES.to_h { |key| [key, 'inherit'] }\n        else\n          {\n            'list-style-type' => value.slice!(CssParser::RE_LIST_STYLE_TYPE),\n            'list-style-position' => value.slice!(CssParser::RE_INSIDE_OUTSIDE),\n            'list-style-image' => value.slice!(CssParser::URI_RX_OR_NONE)\n          }\n        end\n\n      declarations.replace_declaration!('list-style', replacement, preserve_importance: true)\n    end\n\n    # Create shorthand declarations (e.g. +margin+ or +font+) whenever possible.\n    def create_shorthand!\n      create_background_shorthand!\n      create_dimensions_shorthand!\n      # border must be shortened after dimensions\n      create_border_shorthand!\n      create_font_shorthand!\n      create_list_style_shorthand!\n    end\n\n    # Combine several properties into a shorthand one\n    def create_shorthand_properties!(properties, shorthand_property) # :nodoc:\n      values = []\n      properties_to_delete = []\n      properties.each do |property|\n        next unless (declaration = declarations[property])\n        next if declaration.important\n\n        values << declaration.value\n        properties_to_delete << property\n      end\n\n      return if values.length <= 1\n\n      properties_to_delete.each do |property|\n        declarations.delete(property)\n      end\n\n      declarations[shorthand_property] = values.join(' ')\n    end\n\n    # Looks for long format CSS background properties (e.g. <tt>background-color</tt>) and\n    # converts them into a shorthand CSS <tt>background</tt> property.\n    #\n    # Leaves properties declared !important alone.\n    def create_background_shorthand! # :nodoc:\n      # When we have a background-size property we must separate it and distinguish it from\n      # background-position by preceding it with a backslash. In this case we also need to\n      # have a background-position property, so we set it if it's missing.\n      # http://www.w3schools.com/cssref/css3_pr_background.asp\n      if (declaration = declarations['background-size']) && !declaration.important\n        declarations['background-position'] ||= '0% 0%'\n        declaration.value = \"/ #{declaration.value}\"\n      end\n\n      create_shorthand_properties! BACKGROUND_PROPERTIES, 'background'\n    end\n\n    # Combine border-color, border-style and border-width into border\n    # Should be run after create_dimensions_shorthand!\n    #\n    # TODO: this is extremely similar to create_background_shorthand! and should be combined\n    def create_border_shorthand! # :nodoc:\n      values = BORDER_STYLE_PROPERTIES.filter_map do |property|\n        next unless (declaration = declarations[property])\n        next if declaration.important\n        # can't merge if any value contains a space (i.e. has multiple values)\n        # we temporarily remove any spaces after commas for the check (inside rgba, etc...)\n        next if /\\s/.match?(declaration.value.gsub(/,\\s/, ',').strip)\n\n        declaration.value\n      end\n\n      return if values.size != BORDER_STYLE_PROPERTIES.size\n\n      BORDER_STYLE_PROPERTIES.each do |property|\n        declarations.delete(property)\n      end\n\n      declarations['border'] = values.join(' ')\n    end\n\n    # Looks for long format CSS dimensional properties (margin, padding, border-color, border-style and border-width)\n    # and converts them into shorthand CSS properties.\n    def create_dimensions_shorthand! # :nodoc:\n      return if declarations.size < NUMBER_OF_DIMENSIONS\n\n      DIMENSIONS.each do |property, dimensions|\n        values = DIMENSION_DIRECTIONS.each_with_index.with_object({}) do |(side, index), result|\n          next unless (declaration = declarations[dimensions[index]])\n\n          result[side] = declaration.value\n        end\n\n        # All four dimensions must be present\n        next if values.size != dimensions.size\n\n        new_value = values.values_at(*compute_dimensions_shorthand(values)).join(' ').strip\n        declarations[property] = new_value unless new_value.empty?\n\n        # Delete the longhand values\n        dimensions.each { |d| declarations.delete(d) }\n      end\n    end\n\n    # Looks for long format CSS font properties (e.g. <tt>font-weight</tt>) and\n    # tries to convert them into a shorthand CSS <tt>font</tt> property.  All\n    # font properties must be present in order to create a shorthand declaration.\n    def create_font_shorthand! # :nodoc:\n      return unless FONT_STYLE_PROPERTIES.all? { |prop| declarations.key?(prop) }\n\n      new_value = +''\n      ['font-style', 'font-variant', 'font-weight'].each do |property|\n        unless declarations[property].value == 'normal'\n          new_value << declarations[property].value << ' '\n        end\n      end\n\n      new_value << declarations['font-size'].value\n\n      unless declarations['line-height'].value == 'normal'\n        new_value << '/' << declarations['line-height'].value\n      end\n\n      new_value << ' ' << declarations['font-family'].value\n\n      declarations['font'] = new_value.gsub(/\\s+/, ' ')\n\n      FONT_STYLE_PROPERTIES.each { |prop| declarations.delete(prop) }\n    end\n\n    # Looks for long format CSS list-style properties (e.g. <tt>list-style-type</tt>) and\n    # converts them into a shorthand CSS <tt>list-style</tt> property.\n    #\n    # Leaves properties declared !important alone.\n    def create_list_style_shorthand! # :nodoc:\n      create_shorthand_properties! LIST_STYLE_PROPERTIES, 'list-style'\n    end\n\n  private\n\n    attr_accessor :declarations\n\n    def compute_dimensions_shorthand(values)\n      # All four sides are equal, returning single value\n      return [:top] if values.values.uniq.count == 1\n\n      # `/* top | right | bottom | left */`\n      return DIMENSION_DIRECTIONS if values[:left] != values[:right]\n\n      # Vertical are the same & horizontal are the same, `/* vertical | horizontal */`\n      return [:top, :left] if values[:top] == values[:bottom]\n\n      [:top, :left, :bottom]\n    end\n\n    def parse_declarations!(block) # :nodoc:\n      self.declarations = Declarations.new\n\n      return unless block\n\n      continuation = nil\n      block.split(SEMICOLON) do |decs|\n        decs = (continuation ? \"#{continuation};#{decs}\" : decs)\n        if unmatched_open_parenthesis?(decs)\n          # Semicolon happened within parenthesis, so it is a part of the value\n          # the rest of the value is in the next segment\n          continuation = decs\n          next\n        end\n\n        next unless (colon = decs.index(COLON))\n\n        property = decs[0, colon]\n        value = decs[(colon + 1)..]\n        property.strip!\n        value.strip!\n        next if property.empty? || value.empty? || value.casecmp?(IMPORTANT)\n\n        add_declaration!(property, value)\n        continuation = nil\n      end\n    end\n\n    def unmatched_open_parenthesis?(declarations)\n      (lparen_index = declarations.index(LPAREN)) && !declarations.index(RPAREN, lparen_index)\n    end\n\n    #--\n    # TODO: way too simplistic\n    #++\n    def parse_selectors!(selectors) # :nodoc:\n      @selectors = selectors.split(',').map do |s|\n        s.gsub!(/\\s+/, ' ')\n        s.strip!\n        s\n      end\n    end\n\n    def split_value_preserving_function_whitespace(value)\n      split_value = value.gsub(RE_FUNCTIONS) do |c|\n        c.gsub!(/\\s+/, WHITESPACE_REPLACEMENT)\n        c\n      end\n\n      matches = split_value.strip.split(/\\s+/)\n\n      matches.each do |c|\n        c.gsub!(WHITESPACE_REPLACEMENT, ' ')\n      end\n    end\n  end\nend\n"
  },
  {
    "path": "lib/css_parser/version.rb",
    "content": "# frozen_string_literal: true\n\nmodule CssParser\n  VERSION = '2.2.0'.freeze\nend\n"
  },
  {
    "path": "lib/css_parser.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'addressable/uri'\nrequire 'uri'\nrequire 'net/https'\nrequire 'digest/md5'\nrequire 'zlib'\nrequire 'stringio'\n\nrequire 'css_parser/version'\nrequire 'css_parser/rule_set'\nrequire 'css_parser/regexps'\nrequire 'css_parser/parser'\n\nmodule CssParser\n  # Merge multiple CSS RuleSets by cascading according to the CSS 2.1 cascading rules\n  # (http://www.w3.org/TR/REC-CSS2/cascade.html#cascading-order).\n  #\n  # Takes one or more RuleSet objects.\n  #\n  # Returns a RuleSet.\n  #\n  # ==== Cascading\n  # If a RuleSet object has its +specificity+ defined, that specificity is\n  # used in the cascade calculations.\n  #\n  # If no specificity is explicitly set and the RuleSet has *one* selector,\n  # the specificity is calculated using that selector.\n  #\n  # If no selectors the specificity is treated as 0.\n  #\n  # If multiple selectors are present then the greatest specificity is used.\n  #\n  # ==== Example #1\n  #   rs1 = RuleSet.new(nil, 'color: black;')\n  #   rs2 = RuleSet.new(nil, 'margin: 0px;')\n  #\n  #   merged = CssParser.merge(rs1, rs2)\n  #\n  #   puts merged\n  #   => \"{ margin: 0px; color: black; }\"\n  #\n  # ==== Example #2\n  #   rs1 = RuleSet.new(nil, 'background-color: black;')\n  #   rs2 = RuleSet.new(nil, 'background-image: none;')\n  #\n  #   merged = CssParser.merge(rs1, rs2)\n  #\n  #   puts merged\n  #   => \"{ background: none black; }\"\n  #--\n  # TODO: declaration_hashes should be able to contain a RuleSet\n  #       this should be a Class method\n  def self.merge(*rule_sets)\n    # in case called like CssParser.merge([rule_set, rule_set])\n    rule_sets.flatten! if rule_sets[0].is_a?(Array)\n\n    unless rule_sets.all?(CssParser::RuleSet)\n      raise ArgumentError, 'all parameters must be CssParser::RuleSets.'\n    end\n\n    return rule_sets[0] if rule_sets.length == 1\n\n    # Internal storage of CSS properties that we will keep\n    properties = {}\n\n    rule_sets.each do |rule_set|\n      rule_set.expand_shorthand!\n\n      specificity = rule_set.specificity\n      specificity ||= rule_set.selectors.filter_map { |s| calculate_specificity(s) }.max || 0\n\n      rule_set.each_declaration do |property, value, is_important|\n        # Add the property to the list to be folded per http://www.w3.org/TR/CSS21/cascade.html#cascading-order\n        if !properties.key?(property)\n          properties[property] = {value: value, specificity: specificity, is_important: is_important}\n        elsif is_important\n          if !properties[property][:is_important] || properties[property][:specificity] <= specificity\n            properties[property] = {value: value, specificity: specificity, is_important: is_important}\n          end\n        elsif properties[property][:specificity] < specificity || properties[property][:specificity] == specificity\n          unless properties[property][:is_important]\n            properties[property] = {value: value, specificity: specificity, is_important: is_important}\n          end\n        end\n      end\n    end\n\n    merged = properties.each_with_object(RuleSet.new(nil, nil)) do |(property, details), rule_set|\n      value = details[:value].strip\n      rule_set[property.strip] = details[:is_important] ? \"#{value.gsub(/;\\Z/, '')}!important\" : value\n    end\n\n    merged.create_shorthand!\n    merged\n  end\n\n  # Calculates the specificity of a CSS selector\n  # per http://www.w3.org/TR/CSS21/cascade.html#specificity\n  #\n  # Returns an integer.\n  #\n  # ==== Example\n  #  CssParser.calculate_specificity('#content div p:first-line a:link')\n  #  => 114\n  #--\n  # Thanks to Rafael Salazar and Nick Fitzsimons on the css-discuss list for their help.\n  #++\n  def self.calculate_specificity(selector)\n    a = 0\n    b = selector.scan('#').length\n    c = selector.scan(NON_ID_ATTRIBUTES_AND_PSEUDO_CLASSES_RX_NC).length\n    d = selector.scan(ELEMENTS_AND_PSEUDO_ELEMENTS_RX_NC).length\n\n    \"#{a}#{b}#{c}#{d}\".to_i\n  rescue\n    0\n  end\n\n  # Make <tt>url()</tt> links absolute.\n  #\n  # Takes a block of CSS and returns it with all relative URIs converted to absolute URIs.\n  #\n  # \"For CSS style sheets, the base URI is that of the style sheet, not that of the source document.\"\n  # per http://www.w3.org/TR/CSS21/syndata.html#uri\n  #\n  # Returns a string.\n  #\n  # ==== Example\n  #  CssParser.convert_uris(\"body { background: url('../style/yellow.png?abc=123') };\",\n  #               \"http://example.org/style/basic.css\").inspect\n  #  => \"body { background: url('http://example.org/style/yellow.png?abc=123') };\"\n  def self.convert_uris(css, base_uri)\n    base_uri = Addressable::URI.parse(base_uri) unless base_uri.is_a?(Addressable::URI)\n\n    css.gsub(URI_RX) do\n      uri = Regexp.last_match(1).to_s.gsub(/[\"']+/, '')\n      # Don't process URLs that are already absolute\n      unless uri.match?(%r{^[a-z]+://}i)\n        begin\n          uri = base_uri.join(uri)\n        rescue\n          nil\n        end\n      end\n      \"url('#{uri}')\"\n    end\n  end\n\n  def self.sanitize_media_query(raw)\n    mq = raw.to_s.gsub(/\\s+/, ' ')\n    mq.strip!\n    mq = 'all' if mq.empty?\n    mq.to_sym\n  end\nend\n"
  },
  {
    "path": "test/fixtures/complex.css",
    "content": "/*\nFonts:\n\tfont-family:'Caslon 540 LT W01 Italic';\n\tfont-family:'Caslon 540 LT W01 Roman';\n\tfont-family:'Univers LT W01 53 Extended';\n\tfont-family:'Univers LT W01 57 Condensed';\n\tfont-family:'Univers LT W01 65 Bold';\n*/\n\n\n/*!\n   http://meyerweb.com/eric/tools/css/reset/ \n   v2.0 | 20110126\n   License: none (public domain)\n*/\nhtml,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;}\n/* HTML5 display-role reset for older browsers */\narticle,aside,details,figcaption,figure,footer,header,hgroup,menu,nav,section{display:block;}\nbody{line-height:1;}\nol,ul{list-style:none;}\nblockquote,q{quotes:none;}\nblockquote:before,blockquote:after,\nq:before,q:after{content:'';content:none;}\ntable{border-collapse:collapse;border-spacing:0;}\n\n/*!\n * Copyright (c) 2008, Yahoo! Inc. All rights reserved.\n * Code licensed under the BSD License:\n * http://developer.yahoo.net/yui/license.txt\n * version: 2.6.0\n*/\nbody{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%;}\n\nbody {\n\tfont: normal 11px/20px sans-serif;\n\tcolor: #3C3C46;\n\tbackground: #FFF none;\n\n}\n\n\n.debug #page {background-image:url(\"grid.png?1\");}\n\n.debug img { opacity: .5;}\n.debug .cyclenav, .debug .cycleprev, .debug .services li { background-color: rgba(100,100,100,0.5);}\n\n/** Frame **/\nbody: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\";}\n\n#container {\n\tmargin: 0 auto;\n\tbackground: #FFF none;\n}\n\n#page {\n\twidth: 940px;\n\tmargin: 0 auto;\n}\n\n\n\n\nheader{width:940px;margin:60px auto;background:#fff url(\"header_bg.png\") 0 50% repeat-x;}\nheader li{display:block;float:left;width:180px;height:10px;padding:50px 0;line-height: 10px;}\n#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;}\n#logo,#logo a{display:block;width:110px;height:110px;margin:0 auto;padding:0;}\n#logo{padding:0 55px;}\n#logo a{background:transparent url(\"sprites.png\") -380px 0;text-indent:-9999em;}\n#logo a:hover,#logo a:focus{background-position:-380px -109px;}\n#logo a:active{background-position:-380px -220px;}\n#nav_work{text-align:right;}\n\n\n\nheader, footer, nav, section{clear:both;zoom:1;}\nem, i { font-style: italic;}\nb, strong {font-weight: bold;}\na { color: #000; text-decoration: none;}\nimg {display: block;}\npre {\n\tmargin: 20px;\n\tline-height: 1.5;\n}\n\nh1 {\n\tmargin-top: 3px;\n\tmargin-bottom: 17px;\n\tfont: normal 70px/80px \"Caslon 540 LT W01 Roman\", Georgia, serif;\n\ttext-align: center;\n\n\ttext-shadow: -2px 2px 1px #FFFFFF, -3px 3px 1px #DCD7D2;\n\n\t/*filter: dropshadow(color=#cccccc, offx=3, offy=2);*/\n\t/*filter: Shadow(Color=#cccccc,Direction=135,Strenth=1);*/\n}\n\nh2 {\n\theight: 20px;\n\tmargin: 50px 0 23px;\n\tfont: normal 9px/20px \"Univers LT W01 53 Extended\", sans-serif;\n\tbackground: #FFF url(\"dashdots.png\") 0 50% repeat-x;\n\ttext-align: center;\n\ttext-transform: uppercase;\n\tletter-spacing: 5px;\n}\n\nh2 span { padding: 0 30px;background: #FFF none;}\n\nh3 {\n\tmargin-bottom: 12px;\n\tfont: normal 16px/20px \"Univers LT W01 57 Condensed\", sans-serif;\n}\n\n\nol, ul { margin-bottom: 20px; list-style-position: inside; font: italic 11px/20px Arial, sans-serif;}\n\nblockquote {\n\tmargin: 20px;\n\tfont: italic 11px/20px Arial, sans-serif;\n}\n\nul {\n\tlist-style-type: disc;\n}\n\nli {\n\tmargin: 0 0 20px;\n\t\n}\n\np {\n\tmargin-bottom: 20px;\n}\n\n\n.lede {\n\twidth: 820px;\n\tmargin: 0 auto 100px;\n\tfont: normal 18px/30px \"Caslon 540 LT W01 Roman\", Georgia, serif;\n\ttext-align: center;\n}\n\n.lede em, .lede i, .lede a {\n\t\tfont-family: \"Garamond W01 Italic\", serif;\n}\n\na {\n\toutline: none;\n\tfont-style: italic;\n}\n\na:hover, a:focus {\n\ttext-decoration: underline;\n}\n\n#nav a, .features a, footer a { font-style: normal;}\n\n\n/* Feature boxes */\n.features {\n\tclear: both;\n\tdisplay: block;\n\twidth: 900px;\n\tmargin-bottom: 50px;\n\tpadding: 0 20px;\n\tlist-style: none;\n\tfont-style: normal;\n\tbackground-color: transparent !important;\n}\n\n.features a:hover, .features a:focus {\n\ttext-decoration: none;\n}\n\n.features li {\n\tfloat: left;\n\twidth: 180px;\n\tmargin: 0 60px 50px 0;\n}\n\n#contact .features h3 {font-size: 16px;}\n\n#contact .features li {text-align: center;}\n\n#studies .features, #contact .features {\n\twidth: 940px;\n\tpadding: 0 60px;\n}\n#studies .features li, #contact .features li {\n\twidth: 220px;\n\tmargin-right: 80px;\n}\n#studies .lede {margin: 0 auto 50px;\n}\n\n#studies nav {\n\twidth: 940px;\n\tmargin: 0;\n}\n\n#studies .fade_l {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 40px;\n\theight: 100%;\n\tz-index: 1000;\n\tbackground: transparent url(\"fade_l.png\") repeat-y;\n}\n\n\n\n#studies .fade_r {\n\tposition: absolute;\n\tright: 0;\n\ttop: 0;\n\twidth: 40px;\n\theight: 100%;\n\tz-index: 1000;\n\tbackground: transparent url(\"fade_r.png\") repeat-y;\n}\n\n.features h3 { margin-bottom: 3px;}\n\n.services h3 {\n\tmargin-left: -5px;\n\tmargin-bottom: 12px;\n\tpadding-left: 40px;\n\tbackground: #FFF url(\"sprites.png\")  0 -250px no-repeat;\n}\n\n\n.services .strategy h3  {\n\tbackground-position: 0 -297px;\n}\n\n.services .id h3  {\n\tpadding-left: 43px;\n\tbackground-position: 0 -347px;\n}\n\n.services .review h3  {\n\tpadding-left: 45px;\n\tbackground-position: 0 -397px;\n}\n\n.services p {\n\tpadding: 0 7px;\n}\n\n.features p { margin-bottom:0;}\n.features .img {\n\tdisplay: block;\n\twidth: 228px;\n\theight: 168px;\n\tmargin: 0 -5px 15px;\n\tborder: 1px solid #e4e4e4;\n}\n.features .img:active {\nbackground-color: #e4e4e4;\t\n}\n\n/*.features .img:hover img {opacity: 1;}*/\n\n.features img { margin: 4px; }\n.js .features img {opacity: 1;}\n.features .first { clear: both;}\n.features .last { margin-right: 0 !important;}\n\n.features .meta {\n\tmargin-bottom: 0;\n\tfont: italic 12px/30px \"Caslon 540 LT W01 Italic\", Georgia, sans-serif;\n}\n\n\n\n#contact .email { font-style: italic;}\n#content .tel a { font-style: normal;}\n#contact a:hover, #contact a:focus { text-decoration: underline;}\n\n\nfooter {\n\twidth: 940px;\n\tmargin: 0 auto 0;\n\tpadding-bottom: 100px;\n}\n\n\nfooter nav {\n\twidth: 100%;\n\theight: 10px;\n\tmargin: 0 0 50px;\n\tpadding: 25px 0;\n\ttext-align: center;\n\tfont: normal 8px/10px \"Univers LT W01 53 Extended\", sans-serif;\n\tletter-spacing: 3px;\n\ttext-transform: uppercase;\n\tbackground: #fff url(\"header_bg.png\") 0 50% repeat-x;\n}\n\nfooter nav a {\n\tdisplay: inline;\n\tpadding: 0 32px;\n\ttext-align: center;\n\t\n}\n\nfooter div {\n\tmargin: 0 auto;\n\tfont-size: 10px;\n\ttext-align: center;\n\n\tcolor: #B3B3B3;\n}\n\nfooter a { font-style: normal;}\n\nfooter img {\n\tdisplay: block;\n\tmargin: 20px auto;\n}\n\n\n\n\n#study h1, .code h1, .inside h1 {\n\tfont-size: 50px;\n\tline-height: 60px;\n\ttext-shadow: none;\n}\n\n.focus h2 {\n\theight: auto;\n\tmargin: 0 0 12px;\n\tborder: 0;\n\tfont: normal 15px/20px \"Univers LT W01 57 Condensed\", sans-serif;\n\ttext-transform: none;\n\ttext-align: left;\n\tletter-spacing: 0;\n\tbackground: none;\n}\n\n.focus ul { list-style: none;}\n\n.focus li {\n\tmargin: 5px 0 10px;\n\tfont: normal 12px/15px \"Univers LT W01 65 Bold\", sans-serif;\n}\n\n#study { \tmargin-bottom: 80px;}\n#study .prose, #study .focus {\n\tfloat: left;\n}\n\n#study .prose {\n\twidth: 460px;\n\tmargin: 0 40px 0 120px;\n\tfont: normal 15px/20px \"Caslon 540 LT W01 Roman\", Georgia, serif;\n}\n\n#study .prose em, #study .prose i {\n\tfont-family: \"Garamond W01 Italic\", serif;\n}\n\n#study .focus {\n\twidth: 200px;\n}\n\n#study .visit {\n\tmargin: 20px 0;\n\tfont: normal 15px/20px \"Univers LT W01 57 Condensed\", sans-serif;\n\ttext-transform: lowercase;\n}\n\n#study .visit a { text-decoration:none;font-style: normal;}\n\n\n.prose a { text-decoration: underline;}\n\n\n\n.gallerywrap .gallery,.cycleprev,.cyclenext{float:left;}\n.cycleprev,.cyclenext{display:block;width:26px;height:26px;text-indent:-9999em;cursor:pointer;background:#FFF url(\"sprites.png\") no-repeat;}\n.cycleprev{margin-left:14px;margin-right:80px;background-position:-40px -60px;}\n.cycleprev:hover,.cycleprev:focus{background-position:-70px -60px;}\n.cyclenext{margin-right:14px;margin-left:80px;background-position:-100px -60px;}\n.cyclenext:hover,.cyclenext:focus{background-position:-130px -60px;}\n\n\n.cyclenav{width:700px;height:12px;margin:50px auto 50px;text-align:center;}\n.cyclenav a{display:inline-block;width:12px;height:12px;margin:0 5px;text-indent:-9999em;background:#fff url(\"sprites.png\") 0 -60px no-repeat;}\n.cyclenav a:hover,.cyclenav a:focus,.cyclenav a.activeSlide{background-position:-15px -60px;}\n#study .cyclenav { margin-top: 40px;}\n#studies .studywrap .cycleprev { margin: 25px 40px 35px 34px;}\n#studies .studywrap .cyclenext { margin: 25px 34px 35px 40px;}\n#studies .studywrap .cyclenav { float: left; width: 740px;height: 26px;margin:30px auto 45px;}\n#studies nav ul.features { background-color: #FFF;}\n#studies nav { margin-bottom: 20px; background-color: #FFF;}\n\n#study .cycleprev,#study .cyclenext{margin-top:235px;}\n\n\n\n.gallerywrap {\n\twidth: 100%;\n}\n\n.gallery {\n\twidth: 700px;\n\tmargin: 0 auto 50px;\n}\n\n.js .gallery img {\n\tdisplay: none;\n}\n\n.js .gallery img:first-child {\n\tdisplay: block;\n}\n\n#study .gallery img {\n\twidth: 700px;\n\theight: 500px;\n\tmargin: 0 auto;\n}\n\n\n\n\n/* Forms and code */\nform {\n\twidth: 700px;\n\tmargin: 45px auto;\n}\n\nform .field {\n\tclear: left;\n\tmargin: 0 0 50px;\n}\n\nlabel {\n\tdisplay: block;\n\tfloat: left;\n\twidth: 280px;\n\tmargin: 0 20px 5px 0;\n\tfont: normal 20px/20px \"Univers LT W01 57 Condensed\", sans-serif;\n}\n\nlabel .pull {\n\tdisplay: inline-block;\n\twidth: 25px;\n\tmargin-left: -30px;\n}\n\nform .sublabel {\n\tdisplay: block;\n\tmargin: 10px 0 5px;\n\tfont: normal 12px/15px sans-serif;\n}\n\nform .hint {\n\tmargin: 5px 0;\n\tfont: italic 11px/15px sans-serif;\n}\n\n\ninput.text, textarea {\n\twidth: 378px;\n\tpadding: 0 10px;\n\tborder: 1px solid #E4E4E4;\n}\n\ninput.text {\n\t\theight: 38px;\n}\n\ntextarea { padding: 10px; line-height: 20px; height: 218px;}\n\nform div.button {\n\tpadding-left: 300px;\n}\n\nbutton.submit {\n\tdisplay: block;\n\twidth: 85px;\n\theight: 85px;\n\tborder: 0;\n\ttext-align: left;\n\ttext-indent: -9999em;\n\tbackground: #FFF url(\"sprites.png\") 0 -150px;\n\tcursor: pointer;\n}\n\nbutton.submit:hover, button.submit:focus {\n\tbackground-position: -100px -150px;\n}\n\nbutton.submit:active {\n\tbackground-position: -200px -150px;\n}\n\n\n#name_case_converter textarea {\n\theight: 400px;\n}"
  },
  {
    "path": "test/fixtures/import-circular-reference.css",
    "content": "@import \"import-circular-reference.css\";\n\nbody { color: black; background: white; }\np { margin: 0px; }\n"
  },
  {
    "path": "test/fixtures/import-malformed.css",
    "content": ".malformed.one:before {\n  content: \"\\\\\";\n  color: \"red\";\n}\n\n.wellformed.one {\n  color: \"green\";\n}\n\n.malformed.two:before {\n  content: \"\\\"\";\n  color: \"red\";\n}\n\n.wellformed.two {\n  color: \"green\";\n}\n\n.malformed.three:before {\n  content: \"{\";\n  color: \"red\";\n}\n\n.wellformed.three {\n  color: \"green\";\n}\n\n.malformed.four:before {\n  content: \"}\";\n  color: \"red\";\n}\n\n.wellformed.four {\n  color: \"green\";\n}"
  },
  {
    "path": "test/fixtures/import-with-media-types.css",
    "content": "@import \"simple.css\" print, tv, screen;\n\ndiv { color: lime; }\n"
  },
  {
    "path": "test/fixtures/import1.css",
    "content": "@import 'subdir/import2.css';\n\ndiv { color: lime; }\n"
  },
  {
    "path": "test/fixtures/simple.css",
    "content": "body {\n\tcolor: black;\n\tbackground: white;\n}\n\np { margin: 0px; }\n"
  },
  {
    "path": "test/fixtures/subdir/import2.css",
    "content": "@import \"../simple.css\";\n\na { text-decoration: none; }\n"
  },
  {
    "path": "test/rule_set/declarations/test_value.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative '../../test_helper'\nrequire 'minitest/spec'\nrequire 'ostruct'\n\nclass RuleSetProperyTest < Minitest::Test\n  describe '.new' do\n    describe 'with invalid value' do\n      it 'raises an error when empty' do\n        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new('  ') }\n        assert_equal 'value is empty', exception.message\n      end\n\n      it 'raises an error when nil' do\n        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new(nil) }\n        assert_equal 'value is empty', exception.message\n      end\n\n      it 'raises an error when contains only important declaration' do\n        exception = assert_raises(ArgumentError) { CssParser::RuleSet::Declarations::Value.new(' !important; ') }\n        assert_equal 'value is empty', exception.message\n      end\n    end\n\n    describe 'with valid value' do\n      it 'remove semicolon at the end' do\n        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value;').value\n      end\n\n      it 'removes important declarations' do\n        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value !important').value\n      end\n\n      it 'strips value' do\n        assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('  value ').value\n      end\n\n      it 'does everything above' do\n        assert_equal \"value\\t another one\",\n          CssParser::RuleSet::Declarations::Value.new(\"  \\tvalue\\t another one  \\t!important  \\t  ;  \").value\n      end\n\n      it 'freezes the string' do\n        assert_equal true, CssParser::RuleSet::Declarations::Value.new('value').value.frozen?\n      end\n    end\n\n    describe 'important' do\n      describe 'when not set' do\n        it 'is not important if value is not important' do\n          assert_equal false, CssParser::RuleSet::Declarations::Value.new('value').important\n        end\n\n        it 'is important if value is not important' do\n          assert_equal true, CssParser::RuleSet::Declarations::Value.new('value !important;').important\n        end\n      end\n\n      describe 'when set' do\n        it 'overrides value importance' do\n          assert_equal false, CssParser::RuleSet::Declarations::Value.new('value !important;', important: false).important\n          assert_equal true, CssParser::RuleSet::Declarations::Value.new('value', important: true).important\n        end\n      end\n    end\n  end\n\n  describe 'important=' do\n    it 'sets importance' do\n      property = CssParser::RuleSet::Declarations::Value.new('value')\n      assert_equal false, property.important\n\n      property.important = true\n      assert_equal true, property.important\n    end\n  end\n\n  describe 'value=' do\n    it 'sets normalized value' do\n      property = CssParser::RuleSet::Declarations::Value.new('foo')\n      assert_equal 'foo', property.value\n\n      property.value = \"  \\tvalue\\t another one  \\t!important  \\t  ;  \"\n      assert_equal \"value\\t another one\", property.value\n    end\n\n    it 'sets importance' do\n      property = CssParser::RuleSet::Declarations::Value.new('foo')\n      assert_equal 'foo', property.value\n      assert_equal false, property.important\n\n      property.value = 'bar !important'\n\n      assert_equal 'bar', property.value\n      assert_equal true, property.important\n    end\n\n    it 'freezes the string' do\n      property = CssParser::RuleSet::Declarations::Value.new('foo')\n      property.value = 'bar'\n\n      assert_equal true, CssParser::RuleSet::Declarations::Value.new('value').value.frozen?\n    end\n\n    it 'raises an exception when the value is empty' do\n      assert_raises ArgumentError do\n        CssParser::RuleSet::Declarations::Value.new\n      end\n    end\n  end\n\n  describe '#to_s' do\n    it 'returns value if not important' do\n      assert_equal 'value', CssParser::RuleSet::Declarations::Value.new('value').to_s\n    end\n\n    it 'returns value with important annotation if important' do\n      assert_equal 'value !important', CssParser::RuleSet::Declarations::Value.new('value', important: true).to_s\n    end\n  end\n\n  describe '#==' do\n    it 'returns true if value & importance are the same' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n\n      assert_equal property, other\n    end\n\n    it 'returns false if value is not a Declarations::Value' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other = OpenStruct.new(value: 'value', important: true)\n\n      refute_equal other, property\n    end\n\n    it 'returns true if value is a Declarations::Value subclass and value are equal' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other_class = Class.new(CssParser::RuleSet::Declarations::Value)\n      other = other_class.new('value', important: true)\n\n      assert_equal property, other\n    end\n\n    it 'returns false if value is a Declarations::Value subclass and value are not equal' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other_class = Class.new(CssParser::RuleSet::Declarations::Value)\n      other = other_class.new('other value', important: true)\n\n      refute_equal other, property\n    end\n\n    it 'returns false if value is different' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other = CssParser::RuleSet::Declarations::Value.new('other value', important: true)\n\n      refute_equal property, other\n    end\n\n    it 'returns false if importance is different' do\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n      other = CssParser::RuleSet::Declarations::Value.new('value', important: false)\n\n      refute_equal property, other\n    end\n  end\nend\n"
  },
  {
    "path": "test/rule_set/test_declarations.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative '../test_helper'\n\nclass RuleSetDeclarationsTest < Minitest::Test\n  describe '.new' do\n    describe 'when initial declarations is not given' do\n      it 'initialized empty' do\n        assert_equal 0, CssParser::RuleSet::Declarations.new.size\n      end\n    end\n\n    describe 'when initial declarations is given' do\n      it 'initialized with given declarations' do\n        baz_property = CssParser::RuleSet::Declarations::Value.new('baz value', important: true)\n        declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value', bar: 'bar value', baz: baz_property})\n\n        assert_equal 3, declarations.size\n\n        assert_equal CssParser::RuleSet::Declarations::Value.new('foo value'), declarations['foo']\n        assert_equal CssParser::RuleSet::Declarations::Value.new('bar value'), declarations[:bar]\n        assert_equal baz_property, declarations['baz']\n      end\n    end\n  end\n\n  describe '#[]=' do\n    it 'normalizes property name' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      declarations[:'  fOo'] = 'foo value'\n\n      assert_equal true, declarations.key?('foo')\n      assert_equal 1, declarations.size\n      assert_equal 'foo value', declarations['foo'].value\n    end\n\n    it 'assigns proper value if Declarations::Value is given' do\n      declarations = CssParser::RuleSet::Declarations.new\n      property = CssParser::RuleSet::Declarations::Value.new('value', important: true)\n\n      declarations['foo'] = property\n\n      assert_equal property, declarations['foo']\n    end\n\n    it 'deletes property if given value is empty' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value', bar: 'bar value'})\n      assert_equal 2, declarations.size\n\n      declarations['foo'] = nil\n\n      assert_equal 1, declarations.size\n      assert_equal true, declarations.key?('bar')\n    end\n\n    it 'creates Declarations::Value with proper value if string is given' do\n      declarations = CssParser::RuleSet::Declarations.new\n      declarations['foo'] = 'foo value'\n\n      assert_instance_of CssParser::RuleSet::Declarations::Value, declarations['foo']\n      assert_equal 'foo value', declarations['foo'].value\n    end\n\n    it 'has alias #add_declaration!' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      assert_equal declarations.method(:[]=), declarations.method(:add_declaration!)\n    end\n\n    it 'raises an exception including the property when the value is empty' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      assert_raises ArgumentError, 'foo value is empty' do\n        declarations['foo'] = '!important'\n      end\n    end\n  end\n\n  describe '#[]' do\n    it 'returns property if exists' do\n      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)\n      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})\n\n      assert_equal foo_value, declarations['foo']\n    end\n\n    it 'returns nil if not exists' do\n      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)\n      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})\n\n      assert_nil declarations['bar']\n    end\n\n    it 'normalizes property name' do\n      foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)\n      declarations = CssParser::RuleSet::Declarations.new({foo: foo_value})\n\n      assert_equal foo_value, declarations[:'Foo ']\n    end\n\n    it 'has alias #get_value' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      assert_equal declarations.method(:[]), declarations.method(:get_value)\n    end\n  end\n\n  describe '#key?' do\n    it 'return true if key exists' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n\n      assert_equal true, declarations.key?('foo')\n    end\n\n    it 'return false if key does not exists' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n\n      assert_equal false, declarations.key?('bar')\n    end\n\n    it 'normalizes property name' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n\n      assert_equal true, declarations.key?(:'foO ')\n    end\n  end\n\n  describe '#size' do\n    it 'returns declarations size' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n\n      assert_equal 1, declarations.size\n\n      declarations['bar'] = 'bar value'\n\n      assert_equal 2, declarations.size\n\n      declarations['foo'] = nil\n\n      assert_equal 1, declarations.size\n    end\n  end\n\n  describe '#delete' do\n    it 'removes declaration if exists' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n      assert_equal 1, declarations.size\n\n      declarations.remove_declaration!('foo')\n\n      assert_equal 0, declarations.size\n    end\n\n    it 'does nothing if declarations does not exist' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n\n      assert_equal 1, declarations.size\n\n      declarations.remove_declaration!('bar')\n\n      assert_equal 1, declarations.size\n    end\n\n    it 'normalizes property name' do\n      declarations = CssParser::RuleSet::Declarations.new({foo: 'foo value'})\n      assert_equal 1, declarations.size\n\n      declarations.remove_declaration!(:fOo)\n\n      assert_equal 0, declarations.size\n    end\n\n    it 'has alias #remove_declaration!' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      assert_equal declarations.method(:delete), declarations.method(:remove_declaration!)\n    end\n  end\n\n  describe '#replace_declaration!' do\n    it 'raises an error when replaced property does not exist' do\n      declarations = CssParser::RuleSet::Declarations.new\n\n      exception = assert_raises(ArgumentError) { declarations.replace_declaration!('property_name', {}) }\n      assert_equal 'property property_name does not exist', exception.message\n    end\n\n    it 'replaces declaration with normalized property name in place' do\n      declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value')\n\n      declarations.replace_declaration!(\"   bAr\\t\\n\", {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})\n\n      expected = CssParser::RuleSet::Declarations.new(\n        'foo' => 'foo_value', 'bar1' => 'bar1_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n      )\n      assert_equal expected, declarations\n    end\n\n    describe 'when `preserve_importance: false`' do\n      it 'does not set importance when replaced property is important' do\n        declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value !important')\n\n        declarations.replace_declaration!('foo', {'bar' => 'bar_value', 'baz' => 'baz_value !important'})\n        expected = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'baz' => 'baz_value !important'})\n\n        assert_equal expected, declarations\n      end\n\n      it 'does not unset importance when replaced property is not important' do\n        declarations = CssParser::RuleSet::Declarations.new('foo' => 'foo_value')\n\n        declarations.replace_declaration!('foo', {'bar' => 'bar_value', 'baz' => 'baz_value !important'})\n        expected = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'baz' => 'baz_value !important'})\n\n        assert_equal expected, declarations\n      end\n    end\n\n    describe 'when `preserve_importance: true`' do\n      it 'sets importance when replaced property is important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value !important', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'}, preserve_importance: true)\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar2' => 'bar2_value !important', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'unsets importance when replaced property is not important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'\n        )\n\n        declarations.replace_declaration!(\n          'bar',\n          {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value !important'},\n          preserve_importance: true\n        )\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'\n        )\n\n        assert_equal expected, declarations\n      end\n    end\n\n    describe 'when subsequent declarations for the replacement declarations exist' do\n      it 'does not replace declarations when both are not important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'does not replace declarations when both are important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'does not replace declaration when only replaced is important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value !important'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'replaces declarations when only replacement is important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value', 'bar1' => 'old_bar1_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n        )\n\n        assert_equal expected, declarations\n      end\n    end\n\n    describe 'when prior declarations for the replacement declarations exist' do\n      it 'replaces declarations when both are not important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'old_bar1_value', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'bar1_value', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'replaces declarations when both are important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'bar1_value !important', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'does not replace declaration when only replaced is important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'old_bar1_value !important', 'foo' => 'foo_value', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n        )\n\n        assert_equal expected, declarations\n      end\n\n      it 'replaces declarations when only replacement is important' do\n        declarations = CssParser::RuleSet::Declarations.new(\n          'bar1' => 'old_bar1_value', 'foo' => 'foo_value', 'bar' => 'bar_value', 'baz' => 'baz_value'\n        )\n\n        declarations.replace_declaration!('bar', {'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value'})\n        expected = CssParser::RuleSet::Declarations.new(\n          'foo' => 'foo_value', 'bar1' => 'bar1_value !important', 'bar2' => 'bar2_value', 'baz' => 'baz_value'\n        )\n\n        assert_equal expected, declarations\n      end\n    end\n  end\n\n  describe '#each' do\n    describe 'when block is not given' do\n      it 'returns enumerator with properties in order' do\n        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value')\n        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value')\n        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value')\n\n        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})\n\n        assert_instance_of Enumerator, declarations.each\n        assert_equal 3, declarations.each.size\n        assert_equal [['foo', foo_value], ['bar', bar_value], ['baz', baz_value]], declarations.each.to_a\n      end\n    end\n\n    describe 'when block is given' do\n      it 'yields properties in order' do\n        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value')\n        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value')\n        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value')\n\n        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})\n\n        mock = stub(\"Fake\")\n        mock.expects(:call).with('foo', foo_value).returns(true)\n        mock.expects(:call).with('bar', bar_value).returns(true)\n        mock.expects(:call).with('baz', baz_value).returns(true)\n\n        declarations.each { |name, value| mock.call(name, value) }\n      end\n    end\n  end\n\n  describe '#to_s' do\n    context 'when `force_important` is not passed' do\n      it 'returns declarations with declared importance' do\n        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: true)\n        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value', important: false)\n        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value', important: true)\n\n        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})\n\n        assert_equal 'foo: foo value !important; bar: bar value; baz: baz value !important;', declarations.to_s\n      end\n    end\n\n    context 'when `force_important` is passed' do\n      it 'returns declarations with important annotations' do\n        foo_value = CssParser::RuleSet::Declarations::Value.new('foo value', important: false)\n        bar_value = CssParser::RuleSet::Declarations::Value.new('bar value', important: false)\n        baz_value = CssParser::RuleSet::Declarations::Value.new('baz value', important: false)\n\n        declarations = CssParser::RuleSet::Declarations.new({foo: foo_value, bar: bar_value, baz: baz_value})\n\n        assert_equal 'foo: foo value !important; bar: bar value !important; baz: baz value !important;',\n          declarations.to_s({force_important: true})\n      end\n    end\n  end\n\n  describe '#==' do\n    it 'returns true if declarations & their order are the same' do\n      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}\n      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)\n      other = CssParser::RuleSet::Declarations.new(declarations_hash)\n\n      assert_equal declarations, other\n    end\n\n    it 'returns false if other is not a Declarations' do\n      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}\n      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)\n      other = OpenStruct.new(declarations: declarations_hash)\n\n      refute_equal declarations, other\n    end\n\n    it 'returns true if value is a Declarations subclass and declarations are equal' do\n      declarations_hash = {'foo' => 'foo_value', 'bar' => 'bar_value'}\n      declarations = CssParser::RuleSet::Declarations.new(declarations_hash)\n      other_class = Class.new(CssParser::RuleSet::Declarations)\n      other = other_class.new(declarations_hash)\n\n      assert_equal declarations, other\n    end\n\n    it 'returns false if value is a Declarations subclass and value are not equal' do\n      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})\n      other_class = Class.new(CssParser::RuleSet::Declarations)\n      other = other_class.new({'bar' => 'bar_value', 'foo' => 'foo_value'})\n\n      refute_equal declarations, other\n    end\n\n    it 'returns false if declarations values are different' do\n      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})\n      other = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'foo' => 'other_foo_value'})\n\n      refute_equal declarations, other\n    end\n\n    it 'returns false if declarations are the same and their order is different' do\n      declarations = CssParser::RuleSet::Declarations.new({'foo' => 'foo_value', 'bar' => 'bar_value'})\n      other = CssParser::RuleSet::Declarations.new({'bar' => 'bar_value', 'foo' => 'foo_value'})\n\n      refute_equal declarations, other\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_basic.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative \"test_helper\"\n\n# Test cases for reading and generating CSS shorthand properties\nclass CssParserBasicTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = CssParser::Parser.new\n    @css = <<-CSS\n      html, body, p { margin: 0px; }\n      p { padding: 0px; }\n      #content { font: 12px/normal sans-serif; }\n      .content { color: red; }\n    CSS\n  end\n\n  def test_finding_by_selector\n    @cp.add_block!(@css)\n    assert_equal 'margin: 0px;', @cp.find_by_selector('body').join(' ')\n    assert_equal 'margin: 0px; padding: 0px;', @cp.find_by_selector('p').join(' ')\n    assert_equal 'font: 12px/normal sans-serif;', @cp.find_by_selector('#content').join(' ')\n    assert_equal 'color: red;', @cp.find_by_selector('.content').join(' ')\n  end\n\n  def test_adding_block\n    @cp.add_block!(@css)\n    assert_equal 'margin: 0px;', @cp.find_by_selector('body').join\n  end\n\n  def test_adding_block_without_closing_brace\n    @cp.add_block!('p { color: red;')\n    assert_equal 'color: red;', @cp.find_by_selector('p').join\n  end\n\n  def test_adding_a_rule\n    @cp.add_rule!(selectors: 'div', block: 'color: blue;')\n    assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')\n  end\n\n  def test_adding_a_rule_set\n    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')\n    @cp.add_rule_set!(rs)\n    assert_equal 'color: blue;', @cp.find_by_selector('div').join(' ')\n  end\n\n  def test_removing_a_rule_set\n    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')\n    @cp.add_rule_set!(rs)\n    rs2 = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')\n    @cp.remove_rule_set!(rs2)\n    assert_equal '', @cp.find_by_selector('div').join(' ')\n  end\n\n  def test_toggling_uri_conversion\n    # with conversion\n    cp_with_conversion = Parser.new(absolute_paths: true)\n    cp_with_conversion.add_block!(\"body { background: url('../style/yellow.png?abc=123') };\",\n      base_uri: 'http://example.org/style/basic.css')\n\n    assert_equal \"background: url('http://example.org/style/yellow.png?abc=123');\",\n      cp_with_conversion['body'].join(' ')\n\n    # without conversion\n    cp_without_conversion = Parser.new(absolute_paths: false)\n    cp_without_conversion.add_block!(\"body { background: url('../style/yellow.png?abc=123') };\",\n      base_uri: 'http://example.org/style/basic.css')\n\n    assert_equal \"background: url('../style/yellow.png?abc=123');\",\n      cp_without_conversion['body'].join(' ')\n  end\n\n  def test_converting_to_hash\n    rs = CssParser::RuleSet.new(selectors: 'div', block: 'color: blue;')\n    @cp.add_rule_set!(rs)\n    hash = @cp.to_h\n    assert_equal 'blue', hash['all']['div']['color']\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_loading.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser's loading functions.\nclass CssParserLoadingTests < Minitest::Test\n  include CssParser\n  include WEBrick\n\n  def setup\n    # from http://nullref.se/blog/2006/5/17/testing-with-webrick\n    @cp = Parser.new\n\n    @uri_base = 'http://localhost:12000'\n\n    @www_root = File.expand_path('fixtures', __dir__)\n\n    @server_thread = Thread.new do\n      s = WEBrick::HTTPServer.new(Port: 12_000, DocumentRoot: @www_root, Logger: Log.new(nil, BasicLog::FATAL), AccessLog: [])\n      s.mount_proc('/redirect301') do |_request, response|\n        response['Location'] = '/simple.css'\n        raise WEBrick::HTTPStatus::MovedPermanently\n      end\n      s.mount_proc('/redirect302') do |_request, response|\n        response['Location'] = '/simple.css'\n        raise WEBrick::HTTPStatus::TemporaryRedirect\n      end\n      @port = s.config[:Port]\n      begin\n        s.start\n      ensure\n        s.shutdown\n      end\n    end\n\n    sleep 1 # ensure the server has time to load\n  end\n\n  def teardown\n    @server_thread.kill\n    @server_thread.join(5)\n    @server_thread = nil\n  end\n\n  def test_loading_301_redirect\n    @cp.load_uri!(\"#{@uri_base}/redirect301\")\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_loading_302_redirect\n    @cp.load_uri!(\"#{@uri_base}/redirect302\")\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_loading_a_local_file\n    file_name = File.expand_path('fixtures/simple.css', __dir__)\n    @cp.load_file!(file_name)\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_loading_a_local_file_with_scheme\n    file_name = \"file://#{__dir__}/fixtures/simple.css\"\n    @cp.load_uri!(file_name)\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_loading_a_remote_file\n    @cp.load_uri!(\"#{@uri_base}/simple.css\")\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  # http://github.com/premailer/css_parser/issues#issue/4\n  def test_loading_a_remote_file_over_ssl\n    @cp.load_uri!(\"https://dialect.ca/inc/screen.css\")\n    assert_includes(@cp.find_by_selector('body').join(' '), \"margin: 0;\")\n  end\n\n  def test_loading_a_string\n    @cp.load_string!(\"p{margin:0px}\")\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_following_at_import_rules_local\n    base_dir = File.expand_path('fixtures', __dir__)\n    @cp.load_file!('import1.css', base_dir: base_dir)\n\n    # from '/import1.css'\n    assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ')\n\n    # from '/subdir/import2.css'\n    assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ')\n\n    # from '/subdir/../simple.css'\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_following_at_import_rules_remote\n    @cp.load_uri!(\"#{@uri_base}/import1.css\")\n\n    # from '/import1.css'\n    assert_equal 'color: lime;', @cp.find_by_selector('div').join(' ')\n\n    # from '/subdir/import2.css'\n    assert_equal 'text-decoration: none;', @cp.find_by_selector('a').join(' ')\n\n    # from '/subdir/../simple.css'\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_imports_disabled\n    cp = Parser.new(import: false)\n    cp.load_uri!(\"#{@uri_base}/import1.css\")\n\n    # from '/import1.css'\n    assert_equal 'color: lime;', cp.find_by_selector('div').join(' ')\n\n    # from '/subdir/import2.css'\n    assert_equal '', cp.find_by_selector('a').join(' ')\n\n    # from '/subdir/../simple.css'\n    assert_equal '', cp.find_by_selector('p').join(' ')\n  end\n\n  def test_following_remote_import_rules\n    css_block = '@import \"http://example.com/css\";'\n\n    assert_raises CssParser::RemoteFileError do\n      @cp.add_block!(css_block, base_uri: \"#{@uri_base}/subdir/\")\n    end\n  end\n\n  def test_following_badly_escaped_import_rules\n    css_block = '@import \"http://example.com/css?family=Droid+Sans:regular,bold|Droid+Serif:regular,italic,bold,bolditalic&subset=latin\";'\n\n    assert_raises CssParser::RemoteFileError do\n      @cp.add_block!(css_block, base_uri: \"#{@uri_base}/subdir/\")\n    end\n  end\n\n  def test_loading_malformed_content_strings\n    file_name = File.expand_path('fixtures/import-malformed.css', __dir__)\n    @cp.load_file!(file_name)\n    @cp.each_selector do |_sel, dec, _spec|\n      assert_equal false, dec.include?('wellformed')\n    end\n  end\n\n  def test_loading_malformed_css_brackets\n    file_name = File.expand_path('fixtures/import-malformed.css', __dir__)\n    @cp.load_file!(file_name)\n    selector_count = 0\n    @cp.each_selector do |_sel, _dec, _spec|\n      selector_count += 1\n    end\n\n    assert_equal 8, selector_count\n  end\n\n  def test_following_at_import_rules_from_add_block\n    css_block = '@import \"../simple.css\";'\n\n    @cp.add_block!(css_block, base_uri: \"#{@uri_base}/subdir/\")\n\n    # from 'simple.css'\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p').join(' ')\n  end\n\n  def test_importing_with_media_types\n    @cp.load_uri!(\"#{@uri_base}/import-with-media-types.css\")\n\n    # from simple.css with :screen media type\n    assert_equal 'margin: 0px;', @cp.find_by_selector('p', :screen).join(' ')\n    assert_equal '', @cp.find_by_selector('p', :tty).join(' ')\n  end\n\n  def test_local_circular_reference_exception\n    assert_raises CircularReferenceError do\n      @cp.load_file!(File.expand_path('fixtures/import-circular-reference.css', __dir__))\n    end\n  end\n\n  def test_remote_circular_reference_exception\n    assert_raises CircularReferenceError do\n      @cp.load_uri!(\"#{@uri_base}/import-circular-reference.css\")\n    end\n  end\n\n  def test_suppressing_circular_reference_exceptions\n    cp_without_exceptions = Parser.new(io_exceptions: false)\n\n    cp_without_exceptions.load_uri!(\"#{@uri_base}/import-circular-reference.css\")\n  end\n\n  def test_toggling_not_found_exceptions\n    cp_with_exceptions = Parser.new(io_exceptions: true)\n\n    err = assert_raises RemoteFileError do\n      cp_with_exceptions.load_uri!(\"#{@uri_base}/no-exist.xyz\")\n    end\n\n    assert_includes err.message, \"#{@uri_base}/no-exist.xyz\"\n\n    cp_without_exceptions = Parser.new(io_exceptions: false)\n\n    cp_without_exceptions.load_uri!(\"#{@uri_base}/no-exist.xyz\")\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_media_types.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the handling of media types\nclass CssParserMediaTypesTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = Parser.new\n  end\n\n  def test_that_media_types_dont_include_all\n    @cp.add_block!(<<-CSS)\n      @media handheld {\n        body { color: blue; }\n        p { color: grey; }\n      }\n      @media screen {\n        body { color: red; }\n      }\n    CSS\n    rules = @cp.rules_by_media_query\n    assert_equal [\"handheld\", \"screen\"], rules.keys.map(&:to_s).sort\n  end\n\n  def test_finding_by_media_type\n    # from http://www.w3.org/TR/CSS21/media.html#at-media-rule\n    @cp.add_block!(<<-CSS)\n      @media print {\n        body { font-size: 10pt }\n      }\n      @media screen {\n        body { font-size: 13px }\n      }\n      @media screen, print {\n        body { line-height: 1.2 }\n      }\n      @media screen, 3d-glasses, print and resolution > 90dpi {\n        body { color: blue; }\n      }\n    CSS\n\n    assert_equal 'font-size: 10pt; line-height: 1.2;', @cp.find_by_selector('body', :print).join(' ')\n    assert_equal 'font-size: 13px; line-height: 1.2; color: blue;', @cp.find_by_selector('body', :screen).join(' ')\n    assert_equal 'color: blue;', @cp.find_by_selector('body', :'print and resolution > 90dpi').join(' ')\n  end\n\n  def test_with_parenthesized_media_features\n    @cp.add_block!(<<-CSS)\n      body { color: black }\n      @media(prefers-color-scheme: dark) {\n        body { color: white }\n      }\n      @media(min-width: 500px) {\n        body { color: blue }\n      }\n      @media screen and (width > 500px) {\n        body { color: red }\n      }\n    CSS\n    assert_equal [:all, :'(prefers-color-scheme: dark)', :'(min-width: 500px)', :'screen and (width > 500px)'], @cp.rules_by_media_query.keys\n    assert_equal 'color: white;', @cp.find_by_selector('body', :'(prefers-color-scheme: dark)').join(' ')\n    assert_equal 'color: blue;', @cp.find_by_selector('body', :'(min-width: 500px)').join(' ')\n    assert_equal 'color: red;', @cp.find_by_selector('body', :'screen and (width > 500px)').join(' ')\n  end\n\n  def test_finding_by_multiple_media_types\n    @cp.add_block!(<<-CSS)\n      @media print {\n        body { font-size: 10pt }\n      }\n      @media handheld {\n        body { font-size: 13px }\n      }\n      @media screen, print {\n        body { line-height: 1.2 }\n      }\n    CSS\n\n    assert_equal 'font-size: 13px; line-height: 1.2;', @cp.find_by_selector('body', [:screen, :handheld]).join(' ')\n  end\n\n  def test_adding_block_with_media_types\n    @cp.add_block!(<<-CSS, media_types: [:screen])\n      body { font-size: 10pt }\n    CSS\n\n    assert_equal 'font-size: 10pt;', @cp.find_by_selector('body', :screen).join(' ')\n    assert @cp.find_by_selector('body', :handheld).empty?\n  end\n\n  def test_adding_block_with_media_types_followed_by_general_rule\n    @cp.add_block!(<<-CSS)\n      @media print {\n        body { font-size: 10pt }\n      }\n\n      body { color: black; }\n    CSS\n\n    assert_includes @cp.to_s, 'color: black;'\n  end\n\n  def test_adding_block_and_limiting_media_types1\n    css = <<-CSS\n      @import \"import1.css\", print\n    CSS\n\n    base_dir = Pathname.new(__dir__).join('fixtures')\n\n    @cp.add_block!(css, only_media_types: :screen, base_dir: base_dir)\n    assert @cp.find_by_selector('div').empty?\n  end\n\n  def test_adding_block_and_limiting_media_types2\n    css = <<-CSS\n      @import \"import1.css\", print and (color)\n    CSS\n\n    base_dir = Pathname.new(__dir__).join('fixtures')\n\n    @cp.add_block!(css, only_media_types: 'print and (color)', base_dir: base_dir)\n    assert_includes @cp.find_by_selector('div').join(' '), 'color: lime'\n  end\n\n  def test_adding_block_and_limiting_media_types\n    css = <<-CSS\n      @import \"import1.css\"\n    CSS\n\n    base_dir = Pathname.new(__dir__).join('fixtures')\n    @cp.add_block!(css, only_media_types: :print, base_dir: base_dir)\n    assert_equal '', @cp.find_by_selector('div').join(' ')\n  end\n\n  def test_adding_rule_set_with_media_type\n    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: [:handheld, :tty])\n    @cp.add_rule!(selectors: 'body', block: 'color: blue;', media_types: :screen)\n    assert_equal 'color: black;', @cp.find_by_selector('body', :handheld).join(' ')\n  end\n\n  def test_adding_rule_set_with_media_query\n    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: 'aural and (device-aspect-ratio: 16/9)')\n    assert_equal 'color: black;', @cp.find_by_selector('body', 'aural and (device-aspect-ratio: 16/9)').join(' ')\n    assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ')\n  end\n\n  def test_selecting_with_all_media_types\n    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: [:handheld, :tty])\n    assert_equal 'color: black;', @cp.find_by_selector('body', :all).join(' ')\n  end\n\n  def test_to_s_includes_media_queries\n    @cp.add_rule!(selectors: 'body', block: 'color: black;', media_types: 'aural and (device-aspect-ratio: 16/9)')\n    assert_equal \"@media aural and (device-aspect-ratio: 16/9) {\\n  body {\\n    color: black;\\n  }\\n}\\n\", @cp.to_s\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_misc.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser.\nclass CssParserTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = Parser.new\n  end\n\n  def test_utf8\n    css = <<-CSS\n      .chinese { font-family: \"Microsoft YaHei\",\"微软雅黑\"; }\n    CSS\n\n    @cp.add_block!(css)\n\n    assert_equal 'font-family: \"Microsoft YaHei\",\"微软雅黑\";', @cp.find_by_selector('.chinese').join(' ')\n  end\n\n  def test_at_page_rule\n    # from http://www.w3.org/TR/CSS21/page.html#page-selectors\n    css = <<-CSS\n      @page { margin: 2cm }\n\n      @page :first {\n        margin-top: 10cm\n      }\n    CSS\n\n    @cp.add_block!(css)\n\n    assert_equal 'margin: 2cm;', @cp.find_by_selector('@page').join(' ')\n    assert_equal 'margin-top: 10cm;', @cp.find_by_selector('@page :first').join(' ')\n  end\n\n  def test_should_ignore_comments\n    # see http://www.w3.org/Style/CSS/Test/CSS2.1/current/html4/t040109-c17-comments-00-b.htm\n    css = <<-CSS\n      /* This is a CSS comment. */\n      .one {color: green;} /* Another comment */\n      /* The following should not be used:\n      .one {color: red;} */\n      .two {color: green; /* color: yellow; */}\n      /**\n      .three {color: red;} */\n      .three {color: green;}\n      /**/\n      .four {color: green;}\n      /*********/\n      .five {color: green;}\n      /* a comment **/\n      .six {color: green;}\n    CSS\n\n    @cp.add_block!(css)\n    @cp.each_selector do |_sel, decs, _spec|\n      assert_equal 'color: green;', decs\n    end\n  end\n\n  def test_parsing_blocks\n    # dervived from http://www.w3.org/TR/CSS21/syndata.html#rule-sets\n    css = <<-CSS\n      div[name='test'] {\n\n      color:\n\n      red;\n\n      }div:hover{coloR:red;\n         }div:first-letter{color:red;/*color:blue;}\"commented out\"*/}\n\n      p[example=\"public class foo\\\n      {\\\n          private string x;\\\n      \\\n          foo(int x) {\\\n              this.x = 'test';\\\n              this.x = \"test\";\\\n          }\\\n      \\\n      }\"] { color: red }\n\n      p { color:red}\n    CSS\n\n    @cp.add_block!(css)\n\n    @cp.each_selector do |_sel, decs, _spec|\n      assert_equal 'color: red;', decs\n    end\n  end\n\n  def test_ignoring_malformed_declarations\n    # dervived from http://www.w3.org/TR/CSS21/syndata.html#parsing-errors\n    css = <<-CSS\n      p { color:green }\n      p { color:green; color }  /* malformed declaration missing ':', value */\n      p { color:red;   color; color:green }  /* same with expected recovery */\n      p { color:green; color: } /* malformed declaration missing value */\n      p { color:red;   color:; color:green } /* same with expected recovery */\n      p { color:green; color{;color:maroon} } /* unexpected tokens { } */\n      p { color:red;   color{;color:maroon}; color:green } /* same with recovery */\n    CSS\n\n    @cp.add_block!(css)\n\n    @cp.each_selector do |_sel, decs, _spec|\n      assert_equal 'color: green;', decs\n    end\n  end\n\n  def test_multiline_declarations\n    css = <<-CSS\n      @font-face {\n        font-family: 'some_font';\n        src: url(https://example.com/font.woff2) format('woff2'),\n             url(https://example.com/font.woff) format('woff');\n        font-style: normal;\n      }\n    CSS\n\n    @cp.add_block!(css)\n    @cp.each_selector do |selector, declarations, _spec|\n      assert_equal '@font-face', selector\n      assert_equal \"font-family: 'some_font'; \" \\\n                   \"src: url(https://example.com/font.woff2) format('woff2'),url(https://example.com/font.woff) format('woff'); \" \\\n                   \"font-style: normal;\", declarations\n    end\n  end\n\n  def test_find_rule_sets\n    css = <<-CSS\n      h1, h2 { color: blue; }\n      h1 { font-size: 10px; }\n      h2 { font-size: 5px; }\n      article  h3  { color: black; }\n      article\n      h3 { background-color: white; }\n    CSS\n\n    @cp.add_block!(css)\n    assert_equal 2, @cp.find_rule_sets([\"h2\"]).size\n    assert_equal 3, @cp.find_rule_sets([\"h1\", \"h2\"]).size\n    assert_equal 2, @cp.find_rule_sets([\"article h3\"]).size\n    assert_equal 2, @cp.find_rule_sets([\"  article \\t  \\n  h3 \\n \"]).size\n  end\n\n  def test_calculating_specificity\n    # from http://www.w3.org/TR/CSS21/cascade.html#specificity\n    assert_equal 0,   CssParser.calculate_specificity('*')\n    assert_equal 1,   CssParser.calculate_specificity('li')\n    assert_equal 2,   CssParser.calculate_specificity('li:first-line')\n    assert_equal 2,   CssParser.calculate_specificity('ul li')\n    assert_equal 3,   CssParser.calculate_specificity('ul ol+li')\n    assert_equal 11,  CssParser.calculate_specificity('h1 + *[rel=up]')\n    assert_equal 13,  CssParser.calculate_specificity('ul ol li.red')\n    assert_equal 21,  CssParser.calculate_specificity('li.red.level')\n    assert_equal 100, CssParser.calculate_specificity('#x34y')\n\n    # from http://www.hixie.ch/tests/adhoc/css/cascade/specificity/003.html\n    assert_equal CssParser.calculate_specificity('div *'), CssParser.calculate_specificity('p')\n    assert CssParser.calculate_specificity('body div *') > CssParser.calculate_specificity('div *')\n\n    # other tests\n    assert_equal 11, CssParser.calculate_specificity('h1[id|=123]')\n  end\n\n  def test_converting_uris\n    base_uri = 'http://www.example.org/style/basic.css'\n    [\"body { background: url(yellow) };\", \"body { background: url('yellow') };\",\n     \"body { background: url('/style/yellow') };\",\n     \"body { background: url(\\\"../style/yellow\\\") };\",\n     \"body { background: url(\\\"lib/../../style/yellow\\\") };\"].each do |css|\n      converted_css = CssParser.convert_uris(css, base_uri)\n      assert_equal \"body { background: url('http://www.example.org/style/yellow') };\", converted_css\n    end\n\n    converted_css = CssParser.convert_uris(\"body { background: url(../style/yellow-dot_symbol$.png?abc=123&amp;def=456&ghi=789#1011) };\", base_uri)\n    assert_equal \"body { background: url('http://www.example.org/style/yellow-dot_symbol$.png?abc=123&amp;def=456&ghi=789#1011') };\", converted_css\n\n    # taken from error log: 2007-10-23 04:37:41#2399\n    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')\n    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\n  end\n\n  def test_ruleset_with_braces\n    # parser = Parser.new\n    # parser.add_block!(\"div { background-color: black !important; }\")\n    # parser.add_block!(\"div { background-color: red; }\")\n    #\n    # rulesets = []\n    #\n    # parser['div'].each do |declaration|\n    #   rulesets << RuleSet.new(selectors: 'div', block: declaration)\n    # end\n    #\n    # merged = CssParser.merge(rulesets)\n    #\n    # result: # merged.to_s => \"{ background-color: black !important; }\"\n\n    new_rule = RuleSet.new(selectors: 'div', block: \"{ background-color: black !important; }\")\n\n    assert_equal 'div { background-color: black !important; }', new_rule.to_s\n  end\n\n  def test_content_with_data\n    rule = RuleSet.new(selectors: 'div', block: '{content: url(data:image/png;base64,LOTSOFSTUFF)}')\n    assert_includes rule.to_s, \"image/png;base64,LOTSOFSTUFF\"\n  end\n\n  def test_enumerator_empty\n    assert_kind_of Enumerator, @cp.each_selector\n  end\n\n  def test_enumerator_nonempty\n    @cp.add_block! 'body {color: black;}'\n\n    assert_kind_of Enumerator, @cp.each_selector\n\n    @cp.each_selector.each do |sel, desc, _spec|\n      assert_equal 'body', sel\n      assert_equal 'color: black;', desc\n    end\n  end\n\n  def with_value_exception\n    # Raise synthetic exception to test error handling because there is no known way to cause it naturally\n    CssParser::RuleSet::Declarations::Value.stubs(:new).raises(ArgumentError.new('stub'))\n    yield # TODO: do not pass a block instead\n  end\n\n  def test_catching_argument_exceptions_for_add_rule\n    with_value_exception do\n      cp_with_exceptions = Parser.new(rule_set_exceptions: true)\n      assert_raises ArgumentError, 'stub' do\n        cp_with_exceptions.add_rule!(selectors: 'body', block: 'background-color: blue')\n      end\n\n      cp_without_exceptions = Parser.new(rule_set_exceptions: false)\n      cp_without_exceptions.add_rule!(selectors: 'body', block: 'background-color: blue')\n    end\n  end\n\n  def test_catching_argument_exceptions_for_add_rule_positional\n    with_value_exception do\n      cp_with_exceptions = Parser.new(rule_set_exceptions: true)\n\n      assert_raises ArgumentError, 'stub' do\n        _, err = capture_io do\n          cp_with_exceptions.add_rule!('body', 'background-color: blue')\n        end\n        assert_includes err, \"DEPRECATION\"\n      end\n\n      cp_without_exceptions = Parser.new(rule_set_exceptions: false)\n      _, err = capture_io do\n        cp_without_exceptions.add_rule!('body', 'background-color: blue')\n      end\n      assert_includes err, \"DEPRECATION\"\n    end\n  end\n\n  def test_catching_argument_exceptions_for_add_rule_with_offsets\n    with_value_exception do\n      cp_with_exceptions = Parser.new(capture_offsets: true, rule_set_exceptions: true)\n\n      assert_raises ArgumentError, 'stub' do\n        _, err = capture_io do\n          cp_with_exceptions.add_rule_with_offsets!('body', 'background-color: blue', 'inline', 1)\n        end\n        assert_includes err, \"DEPRECATION\"\n      end\n\n      cp_without_exceptions = Parser.new(capture_offsets: true, rule_set_exceptions: false)\n      _, err = capture_io do\n        cp_without_exceptions.add_rule_with_offsets!('body', 'background-color: blue', 'inline', 1)\n      end\n      assert_includes err, \"DEPRECATION\"\n    end\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_offset_capture.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for the CssParser's loading functions.\nclass CssParserOffsetCaptureTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = Parser.new\n  end\n\n  def test_capturing_offsets_for_local_file\n    file_name = File.expand_path('fixtures/simple.css', __dir__)\n    @cp.load_file!(file_name, capture_offsets: true)\n\n    rules = @cp.find_rule_sets(['body', 'p'])\n\n    # check that we found the body rule where we expected\n    assert_equal 0, rules[0].offset.first\n    assert_equal 43, rules[0].offset.last\n    assert_equal file_name, rules[0].filename\n\n    # and the p rule\n    assert_equal 45, rules[1].offset.first\n    assert_equal 63, rules[1].offset.last\n    assert_equal file_name, rules[1].filename\n  end\n\n  # http://github.com/premailer/css_parser/issues#issue/4\n  def test_capturing_offsets_from_remote_file\n    # TODO: test SSL locally\n    @cp.load_uri!(\"https://dialect.ca/inc/screen.css\", capture_offsets: true)\n\n    # there are a lot of rules in this file, but check some rule offsets\n    rules = @cp.find_rule_sets(['#container', '#name_case_converter textarea'])\n    assert_equal 2, rules.count\n\n    assert_equal 2172, rules.first.offset.first\n    assert_equal 2227, rules.first.offset.last\n    assert_equal 'https://dialect.ca/inc/screen.css', rules.first.filename\n\n    assert_equal 10_703, rules.last.offset.first\n    assert_equal 10_752, rules.last.offset.last\n    assert_equal 'https://dialect.ca/inc/screen.css', rules.last.filename\n  end\n\n  def test_capturing_offsets_from_string\n    css = <<-CSS\n      body { margin: 0px; }\n      p { padding: 0px; }\n      #content { font: 12px/normal sans-serif; }\n      .content { color: red; }\n    CSS\n    @cp.load_string!(css, capture_offsets: true, filename: 'index.html')\n\n    rules = @cp.find_rule_sets(['body', 'p', '#content', '.content'])\n    assert_equal 4, rules.count\n\n    assert_equal 6, rules[0].offset.first\n    assert_equal 27, rules[0].offset.last\n    assert_equal 'index.html', rules[0].filename\n\n    assert_equal 34, rules[1].offset.first\n    assert_equal 53, rules[1].offset.last\n    assert_equal 'index.html', rules[1].filename\n\n    assert_equal 60, rules[2].offset.first\n    assert_equal 102, rules[2].offset.last\n    assert_equal 'index.html', rules[2].filename\n\n    assert_equal 109, rules[3].offset.first\n    assert_equal 133, rules[3].offset.last\n    assert_equal 'index.html', rules[3].filename\n  end\n\n  def test_capturing_offsets_with_imports\n    base_dir = Pathname.new(__dir__).join('fixtures')\n    @cp.load_file!('import1.css', base_dir: base_dir, capture_offsets: true)\n\n    rules = @cp.find_rule_sets(['div', 'a', 'body', 'p'])\n\n    # check that we found the div rule where we expected in the primary file\n    assert_equal 'div', rules[0].selectors.join\n    assert_equal 31, rules[0].offset.first\n    assert_equal 51, rules[0].offset.last\n    assert_equal base_dir.join('import1.css').to_s, rules[0].filename\n\n    # check that the a rule in the first import is where we expect\n    assert_equal 'a', rules[1].selectors.join\n    assert_equal 26, rules[1].offset.first\n    assert_equal 54, rules[1].offset.last\n    assert_equal base_dir.join('subdir/import2.css').to_s, rules[1].filename\n\n    # and the body rule in the second import\n    assert_equal 'body', rules[2].selectors.join\n    assert_equal 0, rules[2].offset.first\n    assert_equal 43, rules[2].offset.last\n    assert_equal base_dir.join('simple.css').to_s, rules[2].filename\n\n    # as well as the p rule in the second import\n    assert_equal 'p', rules[3].selectors.join\n    assert_equal 45, rules[3].offset.first\n    assert_equal 63, rules[3].offset.last\n    assert_equal base_dir.join('simple.css').to_s, rules[3].filename\n  end\nend\n"
  },
  {
    "path": "test/test_css_parser_regexps.rb",
    "content": "# coding: iso-8859-1\n# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for CSS regular expressions\n#\n# see http://www.w3.org/TR/CSS21/syndata.html and\n# http://www.w3.org/TR/CSS21/grammar.html\nclass CssParserRegexpTests < Minitest::Test\n  def test_strings\n    # complete matches\n    [\n      '\"abcd\"', '\" A sd sédrcv \\'dsf\\' asd rfg asd\"', '\"A\\ d??ef 123!\"',\n      \"\\\"this is\\\\\\n a test\\\"\", '\"back\\67round\"', '\"r\\000065 ed\"',\n      \"'abcd'\", \"' A sd sedrcv \\\"dsf\\\" asd rf—&23$%#%$g asd'\", \"'A\\\\\\n def 123!'\",\n      \"'this is\\\\\\n a test'\", \"'back\\\\67round'\", \"'r\\\\000065 ed'\"\n    ].each do |str|\n      assert_equal str, str.match(CssParser::RE_STRING).to_s\n    end\n\n    test_string = \"p { background: red url(\\\"url\\\\.'p'ng\\\"); }\"\n    assert_equal \"\\\"url\\\\.'p'ng\\\"\", test_string.match(CssParser::RE_STRING).to_s\n  end\n\n  def test_box_model_units\n    %w[auto inherit 80px 90pt 80pc 80rem 80vh 70vm 60vw 1vmin 2vmax 0 2em 3ex 1cm 100mm 2in 120%].each do |str|\n      assert_match(CssParser::BOX_MODEL_UNITS_RX, str)\n    end\n  end\n\n  def test_unicode\n    ['back\\67round', 'r\\000065 ed', '\\00006C'].each do |str|\n      assert_match(Regexp.new(CssParser::RE_UNICODE), str)\n    end\n  end\n\n  def test_colour\n    [\n      'color: #fff', 'color:#f0a09c;', 'color: #04A', 'color: #04a9CE',\n      'color: rgb(100, -10%, 300);', 'color: rgb(10,10,10)', 'color:rgb(12.7253%, -12%,0)',\n      'color: hsla(-15, -77%, 19%, 5%);',\n      'color: rgba(0,0,0,.1)', 'color: rgba(0, 0, 0, .5)', 'color: hsla(0, 0%, 0%, .05)',\n      'color: black', 'color:Red;', 'color: AqUa;', 'color: blue   ', 'color: transparent',\n      'color: darkslategray'\n    ].each do |colour|\n      assert_match(CssParser::RE_COLOUR, colour)\n    end\n\n    [\n      'color: #fa', 'color:#f009c;', 'color: #04G', 'color: #04a9Cq',\n      'color: rgb 100, -10%, 300;', 'color: rgb 10,10,10', 'color:rgb(12px, -12%,0)',\n      'color:fuscia;', 'color: thick',\n      'color:  alice_blue'\n    ].each do |colour|\n      refute_match(CssParser::RE_COLOUR, colour)\n    end\n  end\n\n  def test_gradients\n    [\n      'linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',\n      'linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)',\n      '-o-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',\n      '-moz-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',\n      '-webkit-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)',\n      '-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)))',\n      '-ms-linear-gradient(bottom, rgb(197,112,191) 7%, rgb(237,146,230) 54%, rgb(255,176,255) 77%)'\n    ].each do |grad|\n      assert_match(CssParser::RE_GRADIENT, grad)\n    end\n  end\n\n  def test_uris\n    crazy_uri = 'http://www.example.com:80/~/redb%20all.png?test=test&test;test+test#test!'\n\n    assert_equal \"url('#{crazy_uri}')\",\n      \"li { list-style: url('#{crazy_uri}') disc }\".match(CssParser::RE_URI).to_s\n\n    assert_equal \"url(#{crazy_uri})\",\n      \"li { list-style: url(#{crazy_uri}) disc }\".match(CssParser::RE_URI).to_s\n\n    assert_equal \"url(\\\"#{crazy_uri}\\\")\",\n      \"li { list-style: url(\\\"#{crazy_uri}\\\") disc }\".match(CssParser::RE_URI).to_s\n  end\n\n  def test_important\n    assert_match(CssParser::IMPORTANT_IN_PROPERTY_RX, \"color: #f00 !important   ;\")\n    refute_match(CssParser::IMPORTANT_IN_PROPERTY_RX, \"color: #f00 !importantish;\")\n  end\n\nprotected\n\n  def load_test_file(filename)\n    fh = File.new(\"fixtures/#{filename}\", 'r')\n    test_file = fh.read\n    fh.close\n\n    test_file\n  end\nend\n"
  },
  {
    "path": "test/test_helper.rb",
    "content": "# frozen_string_literal: true\n\nrequire 'bundler/setup'\nrequire 'maxitest/autorun'\nrequire 'mocha/minitest'\nrequire 'ostruct'\nrequire 'net/http'\nrequire 'webrick'\nrequire 'css_parser'\n"
  },
  {
    "path": "test/test_merging.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\nclass MergingTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = CssParser::Parser.new\n  end\n\n  def test_simple_merge\n    rs1 = RuleSet.new(block: 'color: black;')\n    rs2 = RuleSet.new(block: 'margin: 0px;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal '0px;', merged['margin']\n    assert_equal 'black;', merged['color']\n  end\n\n  def test_merging_array\n    rs1 = RuleSet.new(block: 'color: black;')\n    rs2 = RuleSet.new(block: 'margin: 0px;')\n    merged = CssParser.merge([rs1, rs2])\n    assert_equal '0px;', merged['margin']\n    assert_equal 'black;', merged['color']\n  end\n\n  def test_merging_with_compound_selectors\n    @cp.add_block! \"body { margin: 0; }\"\n    @cp.add_block! \"h2   { margin: 5px; }\"\n\n    rules = @cp.find_rule_sets([\"body\", \"h2\"])\n    assert_equal \"margin: 5px;\", CssParser.merge(rules).declarations_to_s\n\n    @cp = CssParser::Parser.new\n    @cp.add_block! \"body { margin: 0; }\"\n    @cp.add_block! \"h2,h1 { margin: 5px; }\"\n\n    rules = @cp.find_rule_sets([\"body\", \"h2\"])\n    assert_equal \"margin: 5px;\", CssParser.merge(rules).declarations_to_s\n  end\n\n  def test_merging_multiple\n    rs1 = RuleSet.new(block: 'color: black;')\n    rs2 = RuleSet.new(block: 'margin: 0px;')\n    rs3 = RuleSet.new(block: 'margin: 5px;')\n    merged = CssParser.merge(rs1, rs2, rs3)\n    assert_equal '5px;', merged['margin']\n  end\n\n  def test_multiple_selectors_should_have_proper_specificity\n    rs1 = RuleSet.new(selectors: 'p, a[rel=\"external\"]', block: 'color: black;')\n    rs2 = RuleSet.new(selectors: 'a', block: 'color: blue;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'black;', merged['color']\n  end\n\n  def test_setting_specificity\n    rs1 = RuleSet.new(block: 'color: red;', specificity: 20)\n    rs2 = RuleSet.new(block: 'color: blue;', specificity: 10)\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'red;', merged['color']\n  end\n\n  def test_properties_should_be_case_insensitive\n    rs1 = RuleSet.new(block: ' CoLor   : red  ;', specificity: 20)\n    rs2 = RuleSet.new(block: 'color: blue;', specificity: 10)\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'red;', merged['color']\n  end\n\n  def test_merging_backgrounds\n    rs1 = RuleSet.new(block: 'background-color: black;')\n    rs2 = RuleSet.new(block: 'background-image: none;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'black none;', merged['background']\n  end\n\n  def test_merging_dimensions\n    rs1 = RuleSet.new(block: 'margin: 3em;')\n    rs2 = RuleSet.new(block: 'margin-left: 1em;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal '3em 3em 3em 1em;', merged['margin']\n  end\n\n  def test_merging_fonts\n    rs1 = RuleSet.new(block: 'font: 11px Arial;')\n    rs2 = RuleSet.new(block: 'font-weight: bold;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'bold 11px Arial;', merged['font']\n  end\n\n  def test_raising_error_on_bad_type\n    assert_raises ArgumentError do\n      CssParser.merge([1, 2, 3])\n    end\n  end\n\n  def test_returning_early_with_only_one_params\n    rs = RuleSet.new(block: 'font-weight: bold;')\n    merged = CssParser.merge(rs)\n    assert_equal rs.object_id, merged.object_id\n  end\n\n  def test_merging_important\n    rs1 = RuleSet.new(block: 'color: black !important;')\n    rs2 = RuleSet.new(block: 'color: red;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'black !important;', merged['color']\n  end\n\n  def test_prioritising_important_over_non_important_in_the_same_block\n    rs1 = RuleSet.new(block: 'color: black !important; color: red;')\n    merged = CssParser.merge(rs1)\n    assert_equal 'black !important;', merged['color']\n  end\n\n  def test_prioritising_two_important_declarations_in_the_same_block\n    rs1 = RuleSet.new(block: 'color: black !important; color: red !important;')\n    merged = CssParser.merge(rs1)\n    assert_equal 'red !important;', merged['color']\n  end\n\n  def test_merging_multiple_important\n    rs1 = RuleSet.new(block: 'color: black !important;', specificity: 1000)\n    rs2 = RuleSet.new(block: 'color: red !important;', specificity: 1)\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'black !important;', merged['color']\n\n    rs3 = RuleSet.new(block: 'color: blue !important;', specificity: 1000)\n    merged = CssParser.merge(rs1, rs2, rs3)\n    assert_equal 'blue !important;', merged['color']\n  end\n\n  def test_merging_shorthand_important\n    rs1 = RuleSet.new(block: 'background: black none !important;')\n    rs2 = RuleSet.new(block: 'background-color: red;')\n    merged = CssParser.merge(rs1, rs2)\n    assert_equal 'black !important;', merged['background-color']\n  end\nend\n"
  },
  {
    "path": "test/test_rule_set.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for parsing CSS blocks\nclass RuleSetTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = Parser.new\n  end\n\n  def test_setting_property_values\n    rs = RuleSet.new\n\n    rs['background-color'] = 'red'\n    assert_equal('red;', rs['background-color'])\n\n    rs['background-color'] = 'blue !important;'\n    assert_equal('blue !important;', rs['background-color'])\n  end\n\n  def test_getting_property_values\n    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')\n    assert_equal('#fff;', rs['color'])\n  end\n\n  def test_getting_property_value_ignoring_case\n    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')\n    assert_equal('#fff;', rs['  ColoR '])\n  end\n\n  def test_each_selector\n    expected = [\n      {selector: \"#content p\", declarations: \"color: #fff;\", specificity: 101},\n      {selector: \"a\", declarations: \"color: #fff;\", specificity: 1}\n    ]\n\n    actual = []\n    rs = RuleSet.new(selectors: '#content p, a', block: 'color: #fff;')\n    rs.each_selector do |sel, decs, spec|\n      actual << {selector: sel, declarations: decs, specificity: spec}\n    end\n\n    assert_equal(expected, actual)\n  end\n\n  def test_each_declaration\n    expected = Set[\n      {property: 'margin', value: '1px -0.25em', is_important: false},\n      {property: 'background', value: 'white none no-repeat', is_important: true},\n      {property: 'color', value: '#fff', is_important: false}\n    ]\n\n    actual = Set.new\n    rs = RuleSet.new(block: 'color: #fff; Background: white none no-repeat !important; margin: 1px -0.25em;')\n    rs.each_declaration do |prop, val, imp|\n      actual << {property: prop, value: val, is_important: imp}\n    end\n\n    assert_equal(expected, actual)\n  end\n\n  def test_each_declaration_respects_order\n    css_fragment = \"margin: 0; padding: 20px; margin-bottom: 28px;\"\n    rs           = RuleSet.new(block: css_fragment)\n    expected     = %w[margin padding margin-bottom]\n    actual       = []\n    rs.each_declaration { |prop, _val, _imp| actual << prop }\n    assert_equal(expected, actual)\n  end\n\n  def test_each_declaration_containing_semicolons\n    rs = RuleSet.new(block: \"background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);\" \\\n                            \"background-repeat: no-repeat\")\n    assert_equal('url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAiCAMAAAB7);', rs['background-image'])\n    assert_equal('no-repeat;', rs['background-repeat'])\n  end\n\n  def test_each_declaration_with_newlines\n    expected = Set[\n      {property: 'background-image', value: 'url(foo;bar)', is_important: false},\n      {property: 'font-weight', value: 'bold', is_important: true}\n    ]\n    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\")\n    actual = Set.new\n    rs.each_declaration do |prop, val, imp|\n      actual << {property: prop, value: val, is_important: imp}\n    end\n    assert_equal(expected, actual)\n  end\n\n  def test_selector_sanitization\n    selectors = \"h1, h2,\\nh3 \"\n    rs = RuleSet.new(selectors: selectors, block: \"color: #fff;\")\n    assert rs.selectors.member?(\"h3\")\n  end\n\n  def test_multiple_selectors_to_s\n    selectors = \"#content p, a\"\n    rs = RuleSet.new(selectors: selectors, block: \"color: #fff;\")\n    assert_match(/^\\s*#content p,\\s*a\\s*\\{/, rs.to_s)\n  end\n\n  def test_declarations_to_s\n    declarations = 'color: #fff; font-weight: bold;'\n    rs = RuleSet.new(selectors: '#content p, a', block: declarations)\n    assert_equal(declarations.split.sort, rs.declarations_to_s.split.sort)\n  end\n\n  def test_important_declarations_to_s\n    declarations = 'color: #fff; font-weight: bold !important;'\n    rs = RuleSet.new(selectors: '#content p, a', block: declarations)\n    assert_equal(declarations.split.sort, rs.declarations_to_s.split.sort)\n  end\n\n  def test_overriding_specificity\n    rs = RuleSet.new(selectors: '#content p, a', block: 'color: white', specificity: 1000)\n    rs.each_selector do |_sel, _decs, spec|\n      assert_equal 1000, spec\n    end\n  end\n\n  def test_important_without_value\n    declarations = 'color: !important; background-color: #fff'\n    rs = RuleSet.new(selectors: '#content p, a', block: declarations)\n    assert_equal('background-color: #fff;', rs.declarations_to_s)\n  end\n\n  def test_not_raised_issue68\n    ok = true\n    begin\n      RuleSet.new(selectors: 'td', block: 'border-top: 5px solid; border-color: #fffff0;')\n    rescue\n      ok = false\n    end\n    assert_equal true, ok\n  end\nend\n"
  },
  {
    "path": "test/test_rule_set_creating_shorthand.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\n# Test cases for reading and generating CSS shorthand properties\nclass RuleSetCreatingShorthandTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = CssParser::Parser.new\n  end\n\n  def test_border_width\n    combined = create_shorthand('border-width': '1px')\n\n    assert_equal '', combined['border']\n    assert_equal '1px;', combined['border-width']\n  end\n\n  def test_border_width_with_border_color_with_spaces\n    combined = create_shorthand(\n      'border-width': '1px',\n      'border-color': 'rgb(0 0 0 / 1)',\n      'border-style': 'solid'\n    )\n\n    assert_equal '', combined['border']\n    assert_equal '1px;', combined['border-width']\n    assert_equal 'rgb(0 0 0 / 1);', combined['border-color']\n  end\n\n  # Border shorthand\n  def test_combining_borders_into_shorthand\n    properties = {\n      'border-top-width' => 'auto',\n      'border-right-width' => 'thin',\n      'border-bottom-width' => 'auto',\n      'border-left-width' => '0px'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('', combined['border'])\n    assert_equal('auto thin auto 0px;', combined['border-width'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n\n    # should not combine if any properties are missing\n    properties.delete('border-top-width')\n\n    combined = create_shorthand(properties)\n\n    assert_equal '', combined['border-width']\n\n    properties = {\n      'border-width' => '22%',\n      'border-color' => 'rgba(255, 0, 0)',\n      'border-style' => 'solid'\n    }\n    combined = create_shorthand(properties)\n    assert_equal '22% solid rgba(255, 0, 0);', combined['border']\n    assert_equal '', combined['border-width']\n    assert_equal '', combined['border-color']\n    assert_equal '', combined['border-style']\n\n    properties = {\n      'border-top-style' => 'none',\n      'border-right-style' => 'none',\n      'border-bottom-style' => 'none',\n      'border-left-style' => 'none'\n    }\n    combined = create_shorthand(properties)\n    assert_equal '', combined['border']\n    assert_equal 'none;', combined['border-style']\n\n    properties = {\n      'border-top-color' => '#bada55',\n      'border-right-color' => '#000000',\n      'border-bottom-color' => '#ffffff',\n      'border-left-color' => '#ff0000'\n    }\n    combined = create_shorthand(properties)\n    assert_equal '#bada55 #000000 #ffffff #ff0000;', combined['border-color']\n  end\n\n  # Dimensions shorthand\n  def test_combining_dimensions_into_shorthand\n    properties = {\n      'margin-right' => 'auto', 'margin-bottom' => '0px', 'margin-left' => 'auto', 'margin-top' => '0px',\n      'padding-right' => '1.25em', 'padding-bottom' => '11%', 'padding-left' => '3pc', 'padding-top' => '11.25ex'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('0px auto;', combined['margin'])\n    assert_equal('11.25ex 1.25em 11% 3pc;', combined['padding'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n\n    # should not combine if any properties are missing\n    properties.delete('margin-right')\n    properties.delete('padding-right')\n\n    combined = create_shorthand(properties)\n\n    assert_equal '', combined['margin']\n    assert_equal '', combined['padding']\n  end\n\n  # Dimensions shorthand, auto property\n  def test_combining_dimensions_into_shorthand_with_auto\n    rs = RuleSet.new(selectors: '#page', block: \"margin: 0; margin-left: auto; margin-right: auto;\")\n    rs.expand_shorthand!\n    assert_equal('auto;', rs['margin-left'])\n    rs.create_shorthand!\n    assert_equal('0 auto;', rs['margin'])\n  end\n\n  # Font shorthand\n  def test_combining_font_into_shorthand\n    # should combine if all font properties are present\n    properties = {\n      \"font-weight\" => \"300\", \"font-size\" => \"12pt\",\n      \"font-family\" => \"sans-serif\", \"line-height\" => \"18px\",\n      \"font-style\" => \"oblique\", \"font-variant\" => \"small-caps\"\n    }\n\n    combined = create_shorthand(properties)\n    assert_equal('oblique small-caps 300 12pt/18px sans-serif;', combined['font'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n\n    # should not combine if any properties are missing\n    properties.delete('font-weight')\n    combined = create_shorthand(properties)\n    assert_equal '', combined['font']\n  end\n\n  # Background shorthand\n  def test_combining_background_into_shorthand\n    properties = {\n      'background-image' => 'url(\\'chess.png\\')', 'background-color' => 'gray',\n      'background-position' => 'center -10.2%', 'background-attachment' => 'fixed',\n      'background-repeat' => 'no-repeat'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('gray url(\\'chess.png\\') no-repeat center -10.2% fixed;', combined['background'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n  end\n\n  def test_combining_background_with_size_into_shorthand\n    properties = {\n      'background-image' => 'url(\\'chess.png\\')', 'background-color' => 'gray',\n      'background-position' => 'center -10.2%', 'background-attachment' => 'fixed',\n      'background-repeat' => 'no-repeat', 'background-size' => '50% 100%'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('gray url(\\'chess.png\\') no-repeat center -10.2% / 50% 100% fixed;', combined['background'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n  end\n\n  def test_combining_background_with_size_and_no_position_into_shorthand\n    properties = {\n      'background-image' => 'url(\\'chess.png\\')', 'background-color' => 'gray',\n      'background-attachment' => 'fixed', 'background-repeat' => 'no-repeat',\n      'background-size' => '50% 100%'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('gray url(\\'chess.png\\') no-repeat 0% 0% / 50% 100% fixed;', combined['background'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n  end\n\n  # List-style shorthand\n  def test_combining_list_style_into_shorthand\n    properties = {\n      'list-style-image' => 'url(\\'chess.png\\')', 'list-style-type' => 'katakana',\n      'list-style-position' => 'inside'\n    }\n\n    combined = create_shorthand(properties)\n\n    assert_equal('katakana inside url(\\'chess.png\\');', combined['list-style'])\n\n    # after creating shorthand, all long-hand properties should be deleted\n    assert_properties_are_deleted(combined, properties)\n  end\n\n  def test_property_values_in_url\n    rs = RuleSet.new(\n      selectors: '#header',\n      block: \"background:url(http://example.com/1528/www/top-logo.jpg) no-repeat top right; \" \\\n             \"padding: 79px 0 10px 0;  text-align:left;\"\n    )\n    rs.expand_shorthand!\n    assert_equal('top right;', rs['background-position'])\n    rs.create_shorthand!\n    assert_equal('url(http://example.com/1528/www/top-logo.jpg) no-repeat top right;', rs['background'])\n  end\n\n  def test_a_single_property_is_not_shorted\n    properties = {'background-color' => 'gray'}\n    combined = create_shorthand(properties)\n\n    assert_equal('gray;', combined['background-color'])\n    assert_equal('', combined['background'])\n  end\n\nprotected\n\n  def assert_properties_are_deleted(ruleset, properties)\n    properties.each_key do |property|\n      assert_equal '', ruleset[property]\n    end\n  end\n\n  def create_shorthand(properties)\n    ruleset = RuleSet.new(nil, nil)\n    properties.each do |property, value|\n      ruleset[property] = value\n    end\n    ruleset.create_shorthand!\n    ruleset\n  end\nend\n"
  },
  {
    "path": "test/test_rule_set_expanding_shorthand.rb",
    "content": "# frozen_string_literal: true\n\nrequire_relative 'test_helper'\n\nclass RuleSetExpandingShorthandTests < Minitest::Test\n  include CssParser\n\n  def setup\n    @cp = CssParser::Parser.new\n  end\n\n  # Dimensions shorthand\n  def test_expanding_border_shorthand\n    declarations = expand_declarations('border: none')\n    assert_equal 'none', declarations['border-right-style']\n\n    declarations = expand_declarations('border: 1px solid red')\n    assert_equal '1px', declarations['border-top-width']\n    assert_equal 'solid', declarations['border-bottom-style']\n\n    # Regression: rgba/hsla with no leading zero on the alpha (e.g. `.1`) used\n    # to fail the colour regex, causing border-*-color to be silently dropped\n    # during shorthand expansion.\n    declarations = expand_declarations('border: 1px solid rgba(0,0,0,.1)')\n    assert_equal '1px', declarations['border-top-width']\n    assert_equal 'solid', declarations['border-top-style']\n    assert_equal 'rgba(0,0,0,.1)', declarations['border-top-color']\n    assert_equal 'rgba(0,0,0,.1)', declarations['border-right-color']\n    assert_equal 'rgba(0,0,0,.1)', declarations['border-bottom-color']\n    assert_equal 'rgba(0,0,0,.1)', declarations['border-left-color']\n\n    declarations = expand_declarations('border-color: red hsla(255, 0, 0, 5) rgb(2% ,2%,2%)')\n    assert_equal 'red', declarations['border-top-color']\n    assert_equal 'rgb(2%,2%,2%)', declarations['border-bottom-color']\n    assert_equal 'hsla(255,0,0,5)', declarations['border-left-color']\n\n    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff #ff0000')\n\n    assert_equal '#000000', declarations['border-top-color']\n    assert_equal '#bada55', declarations['border-right-color']\n    assert_equal '#ffffff', declarations['border-bottom-color']\n    assert_equal '#ff0000', declarations['border-left-color']\n\n    declarations = expand_declarations('border-color: #000000 #bada55 #ffffff')\n\n    assert_equal '#000000', declarations['border-top-color']\n    assert_equal '#bada55', declarations['border-right-color']\n    assert_equal '#ffffff', declarations['border-bottom-color']\n    assert_equal '#bada55', declarations['border-left-color']\n\n    declarations = expand_declarations('border-color: #000000 #bada55')\n\n    assert_equal '#000000', declarations['border-top-color']\n    assert_equal '#bada55', declarations['border-right-color']\n    assert_equal '#000000', declarations['border-bottom-color']\n    assert_equal '#bada55', declarations['border-left-color']\n\n    declarations = expand_declarations('border: thin dot-dot-dash')\n    assert_equal 'dot-dot-dash', declarations['border-left-style']\n    assert_equal 'thin', declarations['border-left-width']\n    assert_nil declarations['border-left-color']\n  end\n\n  # Dimensions shorthand\n  def test_getting_dimensions_from_shorthand\n    # test various shorthand forms\n    ['margin: 0px auto', 'margin: 0px auto 0px', 'margin: 0px auto 0px'].each do |shorthand|\n      declarations = expand_declarations(shorthand)\n      assert_equal({\"margin-right\" => \"auto\", \"margin-bottom\" => \"0px\", \"margin-left\" => \"auto\", \"margin-top\" => \"0px\"}, declarations)\n    end\n\n    # test various units\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"margin: 0% -0.123#{unit} 9px -.9pc\"\n      declarations = expand_declarations(shorthand)\n      assert_equal({\"margin-right\" => \"-0.123#{unit}\", \"margin-bottom\" => \"9px\", \"margin-left\" => \"-.9pc\", \"margin-top\" => \"0%\"}, declarations)\n    end\n  end\n\n  # Font shorthand\n  def test_getting_font_size_from_shorthand\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"font: 300 italic 11.25#{unit}/14px verdana, helvetica, sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(\"11.25#{unit}\", declarations['font-size'])\n    end\n\n    ['smaller', 'small', 'medium', 'large', 'x-large', 'auto'].each do |unit|\n      shorthand = \"font: 300 italic #{unit}/14px verdana, helvetica, sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(unit, declarations['font-size'])\n    end\n  end\n\n  def test_getting_font_families_from_shorthand\n    shorthand = \"font: 300 italic 12px/14px \\\"Helvetica-Neue-Light 45\\\", 'verdana', helvetica, sans-serif;\"\n    declarations = expand_declarations(shorthand)\n    assert_equal(\"\\\"Helvetica-Neue-Light 45\\\", 'verdana', helvetica, sans-serif\", declarations['font-family'])\n  end\n\n  def test_getting_font_weight_from_shorthand\n    ['300', 'bold', 'bolder', 'lighter', 'normal'].each do |unit|\n      shorthand = \"font: #{unit} italic 12px sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(unit, declarations['font-weight'])\n    end\n\n    # ensure normal is the default state\n    ['font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',\n     'font: small-caps normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand|\n      declarations = expand_declarations(shorthand)\n      assert_equal('normal', declarations['font-weight'], shorthand)\n    end\n  end\n\n  def test_getting_font_variant_from_shorthand\n    shorthand = \"font: small-caps italic 12px sans-serif;\"\n    declarations = expand_declarations(shorthand)\n    assert_equal('small-caps', declarations['font-variant'])\n  end\n\n  def test_getting_font_variant_from_shorthand_ensure_normal_is_the_default_state\n    [\n      'font: normal italic 12px sans-serif;', 'font: italic 12px sans-serif;',\n      'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'\n    ].each do |shorthand|\n      declarations = expand_declarations(shorthand)\n      assert_equal('normal', declarations['font-variant'], shorthand)\n    end\n  end\n\n  def test_getting_font_style_from_shorthand\n    ['italic', 'oblique'].each do |unit|\n      shorthand = \"font: normal #{unit} bold 12px sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(unit, declarations['font-style'])\n    end\n\n    # ensure normal is the default state\n    ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;',\n     'font: normal 12px sans-serif;', 'font: 12px/16px sans-serif;'].each do |shorthand|\n      declarations = expand_declarations(shorthand)\n      assert_equal('normal', declarations['font-style'], shorthand)\n    end\n  end\n\n  def test_getting_line_height_from_shorthand\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"font: 300 italic 12px/0.25#{unit} verdana, helvetica, sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(\"0.25#{unit}\", declarations['line-height'])\n    end\n\n    # ensure normal is the default state\n    ['font: normal bold 12px sans-serif;', 'font: small-caps 12px sans-serif;',\n     'font: normal 12px sans-serif;', 'font: 12px sans-serif;'].each do |shorthand|\n      declarations = expand_declarations(shorthand)\n      assert_equal('normal', declarations['line-height'], shorthand)\n    end\n  end\n\n  def test_getting_line_height_from_shorthand_with_spaces\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"font: 300 italic 12px/ 0.25#{unit} verdana, helvetica, sans-serif;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(\"0.25#{unit}\", declarations['line-height'])\n    end\n  end\n\n  # Background shorthand\n  def test_getting_background_properties_from_shorthand\n    expected = {\n      \"background-image\" => \"url('chess.png')\", \"background-color\" => \"gray\", \"background-repeat\" => \"repeat\",\n      \"background-attachment\" => \"fixed\", \"background-position\" => \"50%\"\n    }\n\n    shorthand = \"background: url('chess.png') gray 50% repeat fixed;\"\n    declarations = expand_declarations(shorthand)\n    assert_equal expected, declarations\n  end\n\n  def test_getting_background_position_from_shorthand\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"background: url('chess.png') gray 30% -0.15#{unit} repeat fixed;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(\"30% -0.15#{unit}\", declarations['background-position'])\n    end\n\n    ['left', 'center', 'right', 'top', 'bottom', 'inherit'].each do |position|\n      shorthand = \"background: url('chess.png') #000fff #{position} no-repeat fixed;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(position, declarations['background-position'])\n    end\n  end\n\n  def test_getting_background_size_from_shorthand\n    ['em', 'ex', 'in', 'px', 'pt', 'pc', '%'].each do |unit|\n      shorthand = \"background: url('chess.png') gray 30% -0.20/-0.15#{unit} auto repeat fixed;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(\"-0.15#{unit} auto\", declarations['background-size'])\n    end\n\n    ['cover', 'contain', 'auto', 'initial', 'inherit'].each do |size|\n      shorthand = \"background: url('chess.png') #000fff 0% 50% / #{size} no-repeat fixed;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(size, declarations['background-size'])\n    end\n  end\n\n  def test_getting_background_colour_from_shorthand\n    [\n      'blue', 'lime', 'rgb(10,10,10)', 'rgb (  -10%, 99, 300)', '#ffa0a0', '#03c', 'trAnsparEnt', 'inherit',\n      # Regression: alpha without a leading zero (e.g. `.1`) used to fail the\n      # colour regex and silently drop background-color.\n      'rgba(0,0,0,.1)'\n    ].each do |colour|\n      shorthand = \"background:#{colour} url('chess.png') center repeat fixed ;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(colour, declarations['background-color'])\n    end\n  end\n\n  def test_getting_background_attachment_from_shorthand\n    ['scroll', 'fixed', 'inherit'].each do |attachment|\n      shorthand = \"background:#0f0f0f url('chess.png') center repeat #{attachment};\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(attachment, declarations['background-attachment'])\n    end\n  end\n\n  def test_getting_background_repeat_from_shorthand\n    ['repeat-x', 'repeat-y', 'no-repeat', 'inherit'].each do |repeat|\n      shorthand = \"background:#0f0f0f none #{repeat};\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(repeat, declarations['background-repeat'])\n    end\n  end\n\n  def test_getting_background_image_from_shorthand\n    ['url(\"chess.png\")', 'url(\"https://example.org:80/~files/chess.png?123=abc&test#5\")',\n     'url(https://example.org:80/~files/chess.png?123=abc&test#5)',\n     \"url('https://example.org:80/~files/chess.png?123=abc&test#5')\", 'none', 'inherit'].each do |image|\n      shorthand = \"background: #0f0f0f #{image} ;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(image, declarations['background-image'])\n    end\n  end\n\n  def test_getting_background_gradient_from_shorthand\n    ['linear-gradient(top, hsla(0, 0%, 0%, 0.00) 0%, hsla(0, 0%, 0%, 0.20) 100%)',\n     '-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)))',\n     '-moz-linear-gradient(bottom, blue, red)'].each do |image|\n      shorthand = \"background: #0f0f0f #{image} repeat ;\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(image, declarations['background-image'])\n    end\n  end\n\n  # List-style shorthand\n  def test_getting_list_style_properties_from_shorthand\n    expected = {\n      'list-style-image' => 'url(\\'chess.png\\')', 'list-style-type' => 'katakana',\n      'list-style-position' => 'inside'\n    }\n\n    shorthand = \"list-style: katakana inside url('chess.png');\"\n    declarations = expand_declarations(shorthand)\n    assert_equal expected, declarations\n  end\n\n  def test_getting_list_style_position_from_shorthand\n    ['inside', 'outside'].each do |position|\n      shorthand = \"list-style: katakana #{position} url('chess.png');\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(position, declarations['list-style-position'])\n    end\n  end\n\n  def test_getting_list_style_type_from_shorthand\n    ['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|\n      shorthand = \"list-style: #{type} inside url('chess.png');\"\n      declarations = expand_declarations(shorthand)\n      assert_equal(type, declarations['list-style-type'])\n    end\n  end\n\n  def test_expanding_shorthand_with_replaced_properties_after\n    shorthand = 'line-height: 0.25px !important; font-style: normal; font: small-caps italic 12px sans-serif; font-size: 12em;'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'line-height' => '0.25px',\n      'font-style' => 'italic',\n      'font-variant' => 'small-caps',\n      'font-weight' => 'normal',\n      'font-family' => 'sans-serif',\n      'font-size' => '12em'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\n  def test_expanding_important_shorthand_with_replaced_properties\n    shorthand = 'line-height: 0.25px !important; font-style: normal; font: small-caps italic 12px sans-serif !important; font-size: 12em; font-family: emoji !important;'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'font-style' => 'italic',\n      'font-variant' => 'small-caps',\n      'font-weight' => 'normal',\n      'line-height' => 'normal',\n      'font-family' => 'emoji',\n      'font-size' => '12px'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\n  def test_functions_with_many_spaces\n    shorthand = 'margin: calc(1em / 4 * var(--foo));'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'margin-top' => 'calc(1em / 4 * var(--foo))',\n      'margin-bottom' => 'calc(1em / 4 * var(--foo))',\n      'margin-left' => 'calc(1em / 4 * var(--foo))',\n      'margin-right' => 'calc(1em / 4 * var(--foo))'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\n  def test_functions_with_no_spaces\n    shorthand = 'margin: calc(1em/4*4);'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'margin-top' => 'calc(1em/4*4)',\n      'margin-bottom' => 'calc(1em/4*4)',\n      'margin-left' => 'calc(1em/4*4)',\n      'margin-right' => 'calc(1em/4*4)'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\n  def test_functions_with_one_space\n    shorthand = 'margin: calc(1em /4);'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'margin-top' => 'calc(1em /4)',\n      'margin-bottom' => 'calc(1em /4)',\n      'margin-left' => 'calc(1em /4)',\n      'margin-right' => 'calc(1em /4)'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\n  def test_functions_with_commas\n    shorthand = 'margin: clamp(1rem, 2.5vw, 2rem)'\n    declarations = expand_declarations(shorthand)\n    expected_declarations = {\n      'margin-top' => 'clamp(1rem, 2.5vw, 2rem)',\n      'margin-bottom' => 'clamp(1rem, 2.5vw, 2rem)',\n      'margin-left' => 'clamp(1rem, 2.5vw, 2rem)',\n      'margin-right' => 'clamp(1rem, 2.5vw, 2rem)'\n    }\n    assert_equal expected_declarations, declarations\n  end\n\nprotected\n\n  def expand_declarations(declarations)\n    ruleset = RuleSet.new(block: declarations)\n    ruleset.expand_shorthand!\n\n    collected = {}\n    ruleset.each_declaration do |prop, val, _imp|\n      collected[prop.to_s] = val.to_s\n    end\n    collected\n  end\nend\n"
  }
]