├── .gitignore ├── .ruby-version ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── CONTRIBUTORS.md ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin └── csscss ├── csscss.gemspec ├── docker-ruby ├── lib ├── csscss.rb └── csscss │ ├── cli.rb │ ├── json_reporter.rb │ ├── parser │ ├── background.rb │ ├── base.rb │ ├── border.rb │ ├── border_color.rb │ ├── border_side.rb │ ├── border_style.rb │ ├── border_width.rb │ ├── color.rb │ ├── common.rb │ ├── css.rb │ ├── font.rb │ ├── list_style.rb │ ├── margin.rb │ ├── multi_side_transformer.rb │ ├── outline.rb │ └── padding.rb │ ├── redundancy_analyzer.rb │ ├── reporter.rb │ ├── sass_include_extensions.rb │ ├── types.rb │ └── version.rb └── test ├── csscss ├── json_reporter_test.rb ├── parser │ ├── background_test.rb │ ├── border_color_test.rb │ ├── border_side_test.rb │ ├── border_style_test.rb │ ├── border_test.rb │ ├── border_width_test.rb │ ├── color_test.rb │ ├── common_test.rb │ ├── css_test.rb │ ├── font_test.rb │ ├── list_style_test.rb │ ├── margin_test.rb │ ├── outline_test.rb │ └── padding_test.rb ├── redundancy_analyzer_test.rb ├── reporter_test.rb ├── sass_include_extensions_test.rb └── types_test.rb ├── just_parse.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | .rspec 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | _site 19 | .vscode 20 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.5 4 | - 2.4 5 | - 2.3 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.3 - 5/30/2014 ## 2 | 3 | * Upgrades parslet dependency to v1.6.1 and drops optimization monkeypatch 4 | * Use correct terminology "declartions" instead of "rules" in the output 5 | 6 | ## 1.3.2 - 6/22/2013 ## 7 | 8 | * Fixes attribute parsing bug that includes comments with braces 9 | * Fixes parsing bug with empty media selectors #67 10 | * Fixes parsing bug with quoted brackets #72 11 | * Fixes parsing bug with nested media queries #73 12 | * Removes --compass-with-config deprecation 13 | * Adds a CONTRIBUTING.md file with instructions 14 | 15 | ## 1.3.1 - 4/20/2013 ## 16 | 17 | * Fixes --ignore-sass-mixins bug with @importing 18 | 19 | ## 1.3.0 - 4/20/2013 ## 20 | 21 | * Adds --require switch for user configuration 22 | * Deprecates --compass-with-config config.rb in favor of --compass --require config.rb 23 | * Ignores @import statements. Users will need to run csscss on those directly 24 | * Adds --ignore-sass-mixins which won't match declarations coming from sass mixins 25 | 26 | ## 1.2.0 - 4/14/2013 ## 27 | 28 | * 0 and 0px are now reconciled as redundancies 29 | * Disables color support by default for windows & ruby < 2.0 30 | * Fixes bug where unquoted url(data...) isn't parsed correctly 31 | * Adds support for LESS files 32 | 33 | ## 1.1.0 - 4/12/2013 ## 34 | 35 | * Fixes bug where CLI --no-color wasn't respected 36 | * Added ruby version requirement for >= 1.9 37 | * Added CONTRIBUTORS.md 38 | * Fixes bugs with urls that have dashes in them 39 | * Fixes bugs with urls containing encoded data (usually images) 40 | * Deprecates CSSCSS_DEBUG in favor of --show-parser-errors 41 | * Fixes line/column output during parser errors 42 | * --compass now grabs config.rb by default if it exists 43 | * Adds --compass-with-config that lets users specify a config 44 | * Fixes parser error bug when trying to parse blank files 45 | * Fixes bug where rules from multiple files aren't consolidated 46 | * Adds --no-match-shorthand to allow users to opt out of shorthand matching 47 | 48 | ## 1.0.0 - 4/7/2013 ## 49 | 50 | * Allows the user to specify ignored properties and selectors 51 | * Better parse error messages 52 | 53 | ## 0.2.1 - 3/28/2013 ## 54 | 55 | * Changes coloring to the selectors and declarations 56 | * Fixes bug where some duplicates weren't being combined #1 57 | 58 | ## 0.2.0 - 3/24/2013 ## 59 | 60 | * Colorizes text output. 61 | * Supports scss/sass files. 62 | * Fixes newline output bug when there are no redundancies 63 | * Downloads remote css files if passed a URL 64 | * Fixes bug with double semicolons (blank attributes) 65 | 66 | ## 0.1.0 - 3/21/2013 ## 67 | 68 | * Initial project release. 69 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to csscss 2 | 3 | First of all: Thanks! 4 | 5 | ## Bugs & Feature Requests 6 | 7 | You can file bugs on the [issues 8 | tracker](https://github.com/zmoazeni/csscss/issues), and tag them with 9 | 'bug'. Feel free to discuss features there, too. 10 | 11 | ## Good report structure 12 | 13 | Please include the following four things in your report: 14 | 15 | 1. The smallest CSS snippet to explain the problem. 16 | 2. What you did. 17 | 3. What you expected to happen. 18 | 4. What happened instead. 19 | 20 | The more information the better. 21 | 22 | ## Contributing Code 23 | 24 | It's easy to contribute code to csscss: 25 | 26 | 1. Fork csscss. 27 | 2. Create a topic branch - `git checkout -b my_branch` 28 | 3. Push to your branch - `git push origin my_branch` 29 | 4. Make sure the code follows the contributing guidelines below. 30 | 5. Create a [Pull Request](http://help.github.com/pull-requests/) from your 31 | branch. 32 | 6. That's it! 33 | 34 | ## First Time OSS Contributors 35 | 36 | Submitting your first pull request can be a little daunting. If this is 37 | your first Open Source contribution, please mention it in your pull 38 | request and I'll help guide you through the process. 39 | 40 | ## Contributing Guidelines 41 | 42 | * Make sure your code follows typical [ruby 43 | conventions](https://github.com/bbatsov/ruby-style-guide). 44 | * Make sure the test suite is green. A simple `bundle exec rake test` 45 | will run all the tests. 46 | * Include a CHANGELOG entry with your change. Add an `(Unreleased)` 47 | section at the top if one doesn't exist. 48 | * Try to keep the git commits squashed and concise. Keep tests and code 49 | changes together in the same commit. Keep only logical changes together 50 | in a single commit. 51 | * I strongly encourage [well written git commit 52 | messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 53 | * Make sure all whitespace is trimmed from the end of lines. 54 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | * Zach Moazeni @zmoazeni 2 | * Carson McDonald @carsonmcdonald 3 | * Martin Kuckert @MKuckert 4 | * Ivan Lazarevic @kopipejst 5 | * Matt DuVall @mduvall twitter:@mduvall_ 6 | * Mekka Okereke @mekka @mekkaokereke 7 | * JoseLuis Torres @joseluistorres twitter:@joseluis_torres 8 | * Paul Simpson @prsimp 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in csscss.gemspec 4 | gemspec 5 | 6 | # optional runtime dependencies 7 | gem 'sass' 8 | gem 'compass' 9 | gem 'less' 10 | gem 'therubyracer', :platform => :mri 11 | 12 | gem 'rake', :require => false 13 | gem 'byebug' 14 | 15 | gem 'minitest' 16 | gem 'm' 17 | gem 'minitest-rg' 18 | 19 | gem 'ruby-prof' 20 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | csscss (1.3.3) 5 | colorize 6 | parslet (>= 1.6.1, < 2.0) 7 | 8 | GEM 9 | remote: https://rubygems.org/ 10 | specs: 11 | byebug (10.0.2) 12 | chunky_png (1.3.11) 13 | colorize (0.8.1) 14 | commonjs (0.2.7) 15 | compass (0.12.2) 16 | chunky_png (~> 1.2) 17 | fssm (>= 0.2.7) 18 | sass (~> 3.1) 19 | ffi (1.9.25) 20 | fssm (0.2.10) 21 | less (2.6.0) 22 | commonjs (~> 0.2.7) 23 | libv8 (3.16.14.19) 24 | m (1.5.1) 25 | method_source (>= 0.6.7) 26 | rake (>= 0.9.2.2) 27 | method_source (0.9.2) 28 | minitest (5.11.3) 29 | minitest-rg (5.2.0) 30 | minitest (~> 5.0) 31 | parslet (1.8.2) 32 | rake (12.3.1) 33 | rb-fsevent (0.10.3) 34 | rb-inotify (0.9.10) 35 | ffi (>= 0.5.0, < 2) 36 | ref (2.0.0) 37 | ruby-prof (0.17.0) 38 | sass (3.7.2) 39 | sass-listen (~> 4.0.0) 40 | sass-listen (4.0.0) 41 | rb-fsevent (~> 0.9, >= 0.9.4) 42 | rb-inotify (~> 0.9, >= 0.9.7) 43 | therubyracer (0.12.3) 44 | libv8 (~> 3.16.14.15) 45 | ref 46 | 47 | PLATFORMS 48 | ruby 49 | 50 | DEPENDENCIES 51 | byebug 52 | compass 53 | csscss! 54 | less 55 | m 56 | minitest 57 | minitest-rg 58 | rake 59 | ruby-prof 60 | sass 61 | therubyracer 62 | 63 | BUNDLED WITH 64 | 1.16.4 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Zach Moazeni 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/zmoazeni/csscss.png?branch=master)](https://travis-ci.org/zmoazeni/csscss) 2 | [![Code Climate](https://codeclimate.com/github/zmoazeni/csscss.png)](https://codeclimate.com/github/zmoazeni/csscss) 3 | 4 | ## What is it? ## 5 | 6 | csscss will parse any CSS files you give it and let you know which 7 | rulesets have duplicated declarations. 8 | 9 | ## What is it for? ## 10 | 11 | One of the best strategies for me to maintain CSS is to reduce 12 | duplication as much as possible. It's not a silver bullet, but it sure 13 | helps. 14 | 15 | To do that, you need to have all the rulesets in your head at all times. 16 | That's hard, csscss is easy. Let it tell you what is redundant. 17 | 18 | ## How do I use it? ## 19 | 20 | First you need to install it. It is currently packaged as a ruby gem: 21 | 22 | $ gem install csscss 23 | 24 | Note: csscss only works on ruby 1.9.x and up. It will have trouble with ruby 1.8.x. 25 | 26 | Then you can run it in at the command line against CSS files. 27 | 28 | $ csscss path/to/styles.css path/to/other-styles.css 29 | 30 | {.contact .content .primary} and {article, #comments} share 5 rules 31 | {.profile-picture}, {.screenshot img} and {a.blurb img} share 4 rules 32 | {.work h2:first-child, .contact h2} and {body.home h2} share 4 rules 33 | {article.blurb:hover} and {article:hover} share 3 rules 34 | 35 | Run it in a verbose mode to see all the duplicated styles. 36 | 37 | $ csscss -v path/to/styles.css 38 | 39 | Run it against remote files by passing a valid URL. 40 | 41 | $ csscss -v http://example.com/css/main.css 42 | 43 | You can also choose a minimum number of matches, which will ignore any 44 | rulesets that have fewer matches. 45 | 46 | $ csscss -n 10 -v path/to/style.css # ignores rulesets with < 10 matches 47 | 48 | If you prefer writing in [Sass](http://sass-lang.com/), you can also parse your sass/scss files. 49 | 50 | $ gem install sass 51 | $ csscss path/to/style.scss 52 | 53 | Sass users may be interested in the `--ignore-sass-mixins` 54 | experimental flag that won't match duplicate declarations from including mixins. 55 | 56 | If you prefer writing in [LESS](http://lesscss.org/), you can also parse your LESS files. 57 | 58 | $ gem install less 59 | $ csscss path/to/style.less 60 | 61 | LESS requires an additional javascript runtime. 62 | [v8/therubyracer](https://rubygems.org/gems/therubyracer) on most 63 | rubies, and [therubyrhino](https://rubygems.org/gems/therubyrhino) on 64 | jruby. 65 | 66 | ## Are there any community extensions? ## 67 | 68 | * [compass-csscss](https://github.com/Comcast/compass-csscss) integrates csscss with [compass](http://compass-style.org/) projects. 69 | * [grunt-csscss](https://github.com/peterkeating/grunt-csscss) a [grunt](http://gruntjs.com/) task to automatically run csscss. 70 | * [gulp-csscss](https://www.npmjs.org/package/gulp-csscss/) a [gulp](http://gulpjs.com/) task to automatically run csscss. 71 | 72 | _Please submit [an issue](https://github.com/zmoazeni/csscss/issues/new) if you know of any others._ 73 | 74 | ## Why doesn't csscss automatically remove duplications for me? ## 75 | 76 | I have been asked this a lot, but csscss is intentionally designed this 77 | way. Check out [this 78 | post](https://connectionrequired.com/blog/2013/04/why-csscss-doesnt-remove-duplication-for-you) 79 | for my reasoning. 80 | 81 | ## I found bugs ## 82 | 83 | This is still a new and evolving project. I heartily welcome feedback. 84 | If you find any issues, please report them on 85 | [github](https://github.com/zmoazeni/csscss/issues). 86 | 87 | Please include the smallest CSS snippet to describe the issue and the 88 | output you expect to see. 89 | 90 | ## I'm a dev, I can help ## 91 | 92 | Awesome! Thanks! Here are the steps I ask: 93 | 94 | 1. Fork it 95 | 2. Create your feature branch (`git checkout -b my-new-feature`) 96 | 3. Commit your changes (`git commit -am 'Add some feature'`) 97 | 4. Make sure the tests pass (`bundle exec rake test`) 98 | 5. Push to the branch (`git push origin my-new-feature`) 99 | 6. Create new Pull Request 100 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.pattern = "test/**/*_test.rb" 7 | t.verbose = true 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/csscss: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require "csscss" 4 | Csscss::CLI.run(ARGV) 5 | -------------------------------------------------------------------------------- /csscss.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'csscss/version' 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "csscss" 8 | gem.version = Csscss::VERSION 9 | gem.authors = ["Zach Moazeni"] 10 | gem.email = ["zach.moazeni@gmail.com"] 11 | gem.summary = %q{A CSS redundancy analyzer that analyzes redundancy.} 12 | gem.description = %q{csscss will parse any CSS files you give it and let you know which rulesets have duplicated declarations.} 13 | gem.homepage = "http://zmoazeni.github.io/csscss/" 14 | 15 | gem.files = `git ls-files`.split($/) 16 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 17 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 18 | gem.require_paths = ["lib"] 19 | 20 | gem.required_ruby_version = ">= 1.9" 21 | 22 | gem.add_dependency "parslet", ">= 1.6.1", "< 2.0" 23 | gem.add_dependency "colorize" 24 | end 25 | -------------------------------------------------------------------------------- /docker-ruby: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | VERSION=${VERSION:-latest} 3 | docker run -it --rm -v $(pwd):/app -v csscss_gem_data:/usr/local/bundle -w /app ruby:$VERSION $@ 4 | -------------------------------------------------------------------------------- /lib/csscss.rb: -------------------------------------------------------------------------------- 1 | require "stringio" 2 | require "optparse" 3 | require "json" 4 | require "open-uri" 5 | 6 | require "colorize" 7 | require "parslet" 8 | 9 | require "csscss/version" 10 | require "csscss/cli" 11 | require "csscss/types" 12 | require "csscss/redundancy_analyzer" 13 | require "csscss/reporter" 14 | require "csscss/json_reporter" 15 | 16 | require "csscss/parser/common" 17 | require "csscss/parser/color" 18 | require "csscss/parser/base" 19 | require "csscss/parser/multi_side_transformer" 20 | 21 | require "csscss/parser/css" 22 | require "csscss/parser/background" 23 | require "csscss/parser/list_style" 24 | require "csscss/parser/margin" 25 | require "csscss/parser/padding" 26 | require "csscss/parser/border_width" 27 | require "csscss/parser/border_color" 28 | require "csscss/parser/border_style" 29 | require "csscss/parser/border_side" 30 | require "csscss/parser/border" 31 | require "csscss/parser/outline" 32 | require "csscss/parser/font" 33 | 34 | -------------------------------------------------------------------------------- /lib/csscss/cli.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | class CLI 3 | def initialize(argv) 4 | @argv = argv 5 | @verbose = false 6 | @color = !windows_1_9 7 | @minimum = 3 8 | @compass = false 9 | @ignored_properties = [] 10 | @ignored_selectors = [] 11 | @match_shorthand = true 12 | @ignore_sass_mixins = false 13 | end 14 | 15 | def run 16 | parse(@argv) 17 | execute 18 | end 19 | 20 | private 21 | def execute 22 | deprecate("Use --show-parser-errors instead of CSSCSS_DEBUG") if ENV["CSSCSS_DEBUG"] 23 | 24 | all_contents= @argv.map do |filename| 25 | if filename =~ URI.regexp 26 | load_css_file(filename) 27 | else 28 | case File.extname(filename).downcase 29 | when ".scss", ".sass" 30 | load_sass_file(filename) 31 | when ".less" 32 | load_less_file(filename) 33 | else 34 | load_css_file(filename) 35 | end 36 | end 37 | end.join("\n") 38 | 39 | unless all_contents.strip.empty? 40 | redundancies = RedundancyAnalyzer.new(all_contents).redundancies( 41 | minimum: @minimum, 42 | ignored_properties: @ignored_properties, 43 | ignored_selectors: @ignored_selectors, 44 | match_shorthand: @match_shorthand 45 | ) 46 | 47 | if @json 48 | puts JSONReporter.new(redundancies).report 49 | else 50 | report = Reporter.new(redundancies).report(verbose:@verbose, color:@color) 51 | puts report unless report.empty? 52 | end 53 | end 54 | 55 | rescue Parslet::ParseFailed => e 56 | line, column = e.cause.source.line_and_column(e.cause.pos) 57 | puts "Had a problem parsing the css at line: #{line}, column: #{column}".red 58 | if @show_parser_errors || ENV['CSSCSS_DEBUG'] == 'true' 59 | puts e.cause.ascii_tree.red 60 | else 61 | puts "Run with --show-parser-errors for verbose parser errors".red 62 | end 63 | exit 1 64 | end 65 | 66 | def parse(argv) 67 | options = OptionParser.new do |opts| 68 | opts.banner = "Usage: csscss [files..]" 69 | opts.version = Csscss::VERSION 70 | 71 | opts.on("-v", "--[no-]verbose", "Display each rule") do |v| 72 | @verbose = v 73 | end 74 | 75 | opts.on("--[no-]color", "Colorize output", "(default is #{@color})") do |c| 76 | @color = c 77 | end 78 | 79 | opts.on("-n", "--num N", Integer, "Print matches with at least this many rules.", "(default is 3)") do |n| 80 | @minimum = n 81 | end 82 | 83 | opts.on("--[no-]match-shorthand", "Expand shorthand rules and matches on explicit rules", "(default is true)") do |match_shorthand| 84 | @match_shorthand = match_shorthand 85 | end 86 | 87 | opts.on("-j", "--[no-]json", "Output results in JSON") do |j| 88 | @json = j 89 | end 90 | 91 | opts.on("--ignore-sass-mixins", "EXPERIMENTAL: Ignore matches that come from including sass/scss mixins", 92 | "This is an experimental feature and may not be included in future releases", 93 | "(default is false)") do |ignore| 94 | @ignore_sass_mixins = ignore 95 | end 96 | 97 | opts.on("--[no-]compass", "Enable compass extensions when parsing sass/scss (default is false)") do |compass| 98 | enable_compass if @compass = compass 99 | end 100 | 101 | opts.on("--compass-with-config config", "Enable compass extensions when parsing sass/scss and pass config file") do |config| 102 | @compass = true 103 | enable_compass(config) 104 | end 105 | 106 | opts.on("--require file.rb", "Load ruby file before running csscss.", "Great for bootstrapping requires/configurations") do |file| 107 | load file 108 | end 109 | 110 | opts.on("--ignore-properties property1,property2,...", Array, "Ignore these properties when finding matches") do |ignored_properties| 111 | @ignored_properties = ignored_properties 112 | end 113 | 114 | opts.on('--ignore-selectors "selector1","selector2",...', Array, "Ignore these selectors when finding matches") do |ignored_selectors| 115 | @ignored_selectors = ignored_selectors 116 | end 117 | 118 | opts.on("--show-parser-errors", "Print verbose parser errors") do |show_parser_errors| 119 | @show_parser_errors = show_parser_errors 120 | end 121 | 122 | opts.on("-V", "--version", "Show version") do |v| 123 | puts opts.ver 124 | exit 125 | end 126 | 127 | opts.on_tail("-h", "--help", "Show this message") do 128 | print_help(opts) 129 | end 130 | end 131 | options.parse!(argv) 132 | 133 | print_help(options) if argv.empty? 134 | rescue OptionParser::ParseError 135 | print_help(options) 136 | end 137 | 138 | def print_help(opts) 139 | puts opts 140 | exit 141 | end 142 | 143 | def deprecate(message) 144 | $stderr.puts("DEPRECATED: #{message}".yellow) 145 | end 146 | 147 | def enable_compass(config = nil) 148 | abort 'Must install the "compass" gem before enabling its extensions' unless gem_installed?("compass") 149 | 150 | if config 151 | Compass.add_configuration(config) 152 | else 153 | Compass.add_configuration("config.rb") if File.exist?("config.rb") 154 | end 155 | end 156 | 157 | def windows_1_9 158 | RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ && RUBY_VERSION =~ /^1\.9/ 159 | end 160 | 161 | def gem_installed?(gem_name) 162 | begin 163 | require gem_name 164 | true 165 | rescue LoadError 166 | false 167 | end 168 | end 169 | 170 | def load_sass_file(filename) 171 | abort 'Must install the "sass" gem before parsing sass/scss files' unless gem_installed?("sass") 172 | require "csscss/sass_include_extensions" if @ignore_sass_mixins 173 | 174 | sass_options = {cache:false} 175 | sass_options[:load_paths] = Compass.configuration.sass_load_paths if @compass 176 | begin 177 | Sass::Engine.for_file(filename, sass_options).render 178 | rescue Sass::SyntaxError => e 179 | if e.message =~ /compass/ && !@compass 180 | puts "Enable --compass option to use compass's extensions" 181 | exit 1 182 | else 183 | raise e 184 | end 185 | end 186 | end 187 | 188 | def load_less_file(filename) 189 | abort 'Must install the "less" gem before parsing less files' unless gem_installed?("less") 190 | contents = load_css_file(filename) 191 | Less::Parser.new.parse(contents).to_css 192 | end 193 | 194 | def load_css_file(filename) 195 | open(filename) {|f| f.read } 196 | end 197 | 198 | class << self 199 | def run(argv) 200 | new(argv).run 201 | end 202 | end 203 | end 204 | end 205 | -------------------------------------------------------------------------------- /lib/csscss/json_reporter.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | class JSONReporter 3 | def initialize(redundancies) 4 | @redundancies = redundancies 5 | end 6 | 7 | def report 8 | JSON.dump(@redundancies.map {|selector_groups, declarations| 9 | { 10 | "selectors" => selector_groups.map(&:to_s), 11 | "count" => declarations.count, 12 | "declarations" => declarations.map(&:to_s) 13 | } 14 | }) 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/csscss/parser/background.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Background 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Color 8 | 9 | rule(:bg_color) { color | symbol("inherit") } 10 | rule(:bg_image) { (url | symbol("none") | symbol("inherit")).as(:image_literal) } 11 | rule(:bg_repeat) { symbol_list(%w(repeat-x repeat-y repeat no-repeat inherit)).as(:repeat) } 12 | rule(:bg_attachment) { symbol_list(%w(scroll fixed inherit)).as(:attachment) } 13 | 14 | rule(:bg_position) { 15 | lcr_symbols = symbol_list(%w(left center right)) 16 | tcb_symbols = symbol_list(%w(top center bottom)) 17 | 18 | (symbol("inherit") | ( 19 | lcr = (percent | length | lcr_symbols) 20 | tcb = (percent | length | tcb_symbols) 21 | 22 | lcr >> tcb | lcr | tcb 23 | )).as(:position) 24 | } 25 | 26 | rule(:background) { 27 | ( 28 | symbol("inherit") >> eof | ( 29 | bg_color.maybe.as(:bg_color) >> 30 | bg_image.maybe.as(:bg_image) >> 31 | bg_repeat.maybe.as(:bg_repeat) >> 32 | bg_attachment.maybe.as(:bg_attachment) >> 33 | bg_position.maybe.as(:bg_position) 34 | ) 35 | ).as(:background) 36 | } 37 | root(:background) 38 | end 39 | 40 | class Transformer < Parslet::Transform 41 | @property = :background_color 42 | extend Color::Transformer 43 | 44 | rule(image_literal:simple(:value)) {Declaration.from_parser("background-image", value) } 45 | 46 | rule(:repeat => simple(:repeat)) { 47 | Declaration.from_parser("background-repeat", repeat) 48 | } 49 | 50 | rule(:attachment => simple(:attachment)) { 51 | Declaration.from_parser("background-attachment", attachment) 52 | } 53 | 54 | rule(:position => simple(:position)) { 55 | Declaration.from_parser("background-position", position) 56 | } 57 | 58 | rule(:background => simple(:inherit)) {[]} 59 | 60 | rule(background: { 61 | bg_color:simple(:color), 62 | bg_image:simple(:url), 63 | bg_repeat:simple(:repeat), 64 | bg_attachment:simple(:attachment), 65 | bg_position:simple(:position) 66 | }) { 67 | [color, url, repeat, attachment, position].compact 68 | } 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/csscss/parser/base.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Base 4 | def parse(_, inputs) 5 | input = Array(inputs).join(" ") 6 | 7 | if parsed = self::Parser.new.try_parse(input) 8 | self::Transformer.new.apply(parsed) 9 | end 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/csscss/parser/border.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Border 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Color 8 | 9 | rule(:border_side) { BorderSide::Parser.new(:top).border_side_anonymous } 10 | 11 | rule(:border) { 12 | ( 13 | symbol("inherit") >> eof | 14 | border_side.maybe.as(:side) 15 | ).as(:border) 16 | } 17 | 18 | root(:border) 19 | end 20 | 21 | class Transformer < Parslet::Transform 22 | extend Color::Transformer 23 | extend Color::PlainColorValue 24 | extend BorderSide::Transformer::Helpers 25 | 26 | rule(border: simple(:inherit)) {[]} 27 | rule(border: { 28 | side: { 29 | width:simple(:width), 30 | style:simple(:style), 31 | color:simple(:color) 32 | } 33 | }) {|context| 34 | [].tap do |declarations| 35 | [:top, :right, :bottom, :left].each do |side| 36 | declarations << transform_side(side, context) 37 | end 38 | 39 | declarations.flatten! 40 | end 41 | } 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/csscss/parser/border_color.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module BorderColor 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Color 8 | 9 | rule(:border_color_side) { 10 | color | symbol("transparent") 11 | } 12 | 13 | rule(:border_color) { 14 | ( 15 | symbol("inherit") >> eof | ( 16 | border_color_side.maybe.as(:top) >> 17 | border_color_side.maybe.as(:right) >> 18 | border_color_side.maybe.as(:bottom) >> 19 | border_color_side.maybe.as(:left) 20 | ) 21 | ).as(:border_color) 22 | } 23 | 24 | root(:border_color) 25 | end 26 | 27 | class Transformer < Parslet::Transform 28 | @property = :border_color 29 | extend MultiSideTransformer 30 | extend Color::Transformer 31 | extend Color::PlainColorValue 32 | 33 | class << self 34 | def side_declaration(side, value) 35 | Declaration.from_parser("border-#{side}-color", value) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/csscss/parser/border_side.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module BorderSide 4 | class << self 5 | def parse(property, inputs) 6 | input = Array(inputs).join(" ") 7 | side = find_side(property) 8 | 9 | if parsed = self::Parser.new(side).try_parse(input) 10 | self::Transformer.new.apply(parsed) 11 | end 12 | end 13 | 14 | def find_side(property) 15 | case property 16 | when "border-top" then :top 17 | when "border-right" then :right 18 | when "border-bottom" then :bottom 19 | when "border-left" then :left 20 | else raise "Unknown property #{property}" 21 | end 22 | end 23 | end 24 | 25 | class Parser < Parslet::Parser 26 | include Common 27 | 28 | attr_reader :side 29 | def initialize(side) 30 | @side = side.to_sym 31 | end 32 | 33 | rule(:border_width) { BorderWidth::Parser.new.border_width_side } 34 | rule(:border_style) { BorderStyle::Parser.new.border_style_side } 35 | rule(:border_color) { BorderColor::Parser.new.border_color_side } 36 | 37 | rule(:border_side_anonymous) { 38 | border_width.maybe.as(:width) >> 39 | border_style.maybe.as(:style) >> 40 | border_color.maybe.as(:color) 41 | } 42 | 43 | rule(:border_side) { 44 | ( 45 | symbol("inherit") >> eof | 46 | border_side_anonymous.as(side) 47 | ).as(:border_side) 48 | } 49 | 50 | root(:border_side) 51 | end 52 | 53 | class Transformer < Parslet::Transform 54 | extend Color::Transformer 55 | extend Color::PlainColorValue 56 | 57 | module Helpers 58 | def transform_top(context); transform_side("top", context); end 59 | def transform_right(context); transform_side("right", context); end 60 | def transform_bottom(context); transform_side("bottom", context); end 61 | def transform_left(context); transform_side("left", context); end 62 | 63 | def transform_side(side, context) 64 | width = context[:width] 65 | style = context[:style] 66 | color = context[:color] 67 | 68 | [].tap do |declarations| 69 | declarations << Declaration.from_parser("border-#{side}-width", width) if width 70 | declarations << Declaration.from_parser("border-#{side}-style", style) if style 71 | declarations << Declaration.from_parser("border-#{side}-color", color) if color 72 | end 73 | end 74 | end 75 | extend Helpers 76 | 77 | rule(border_side: simple(:inherit)) {[]} 78 | 79 | [:top, :right, :bottom, :left].each do |side| 80 | rule(border_side: { 81 | side => { 82 | width:simple(:width), 83 | style:simple(:style), 84 | color:simple(:color) 85 | } 86 | }, &method("transform_#{side}")) 87 | end 88 | end 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/csscss/parser/border_style.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module BorderStyle 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:border_style_side) { 10 | symbol_list(%w(none hidden dotted dashed solid 11 | double groove ridge inset outset 12 | )) 13 | } 14 | 15 | rule(:border_style) { 16 | ( 17 | symbol("inherit") >> eof | ( 18 | border_style_side.maybe.as(:top) >> 19 | border_style_side.maybe.as(:right) >> 20 | border_style_side.maybe.as(:bottom) >> 21 | border_style_side.maybe.as(:left) 22 | ) 23 | ).as(:border_style) 24 | } 25 | 26 | root(:border_style) 27 | end 28 | 29 | class Transformer < Parslet::Transform 30 | @property = :border_style 31 | extend MultiSideTransformer 32 | 33 | class << self 34 | def side_declaration(side, value) 35 | Declaration.from_parser("border-#{side}-style", value) 36 | end 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/csscss/parser/border_width.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module BorderWidth 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:border_width_side) { 10 | symbol_list(%w(thin medium thick inherit)) | length 11 | } 12 | 13 | rule(:border_width) { 14 | ( 15 | symbol("inherit") >> eof | ( 16 | border_width_side.maybe.as(:top) >> 17 | border_width_side.maybe.as(:right) >> 18 | border_width_side.maybe.as(:bottom) >> 19 | border_width_side.maybe.as(:left) 20 | ) 21 | ).as(:border_width) 22 | } 23 | 24 | root(:border_width) 25 | end 26 | 27 | class Transformer < Parslet::Transform 28 | @property = :border_width 29 | extend MultiSideTransformer 30 | 31 | def self.side_declaration(side, value) 32 | Declaration.from_parser("border-#{side}-width", value) 33 | end 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/csscss/parser/color.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Color 4 | include Parslet 5 | include Common 6 | 7 | rule(:color) { (hexcolor | rgb | color_keyword).as(:color) } 8 | rule(:rgb) { (rgb_with(numbers) | rgb_with(percent)).as(:rgb) } 9 | rule(:hexcolor) { (str("#") >> match["a-fA-F0-9"].repeat(1)).as(:hexcolor) >> space? } 10 | rule(:color_keyword) { 11 | colors = %w(inherit black silver gray white maroon 12 | red purple fuchsia green lime olive 13 | yellow navy blue teal aqua) 14 | colors.map {|c| symbol(c) }.reduce(:|).as(:keyword) 15 | } 16 | 17 | private 18 | def rgb_with(parser) 19 | symbol("rgb") >> parens do 20 | parser >> space? >> 21 | symbol(",") >> 22 | parser >> space? >> 23 | symbol(",") >> 24 | parser >> space? 25 | end 26 | end 27 | 28 | module Transformer 29 | def self.extended(base) 30 | base.instance_eval do 31 | extend ClassMethods 32 | 33 | rule(color:{rgb:simple(:value)}) {|c| transform_color(c)} 34 | rule(color:{keyword:simple(:value)}) {|c| transform_color(c)} 35 | rule(color:{hexcolor:simple(:value)}) {|c| transform_color(c)} 36 | end 37 | end 38 | 39 | module ClassMethods 40 | def transform_color(context) 41 | Declaration.from_parser(@property.to_s.gsub("_", "-"), context[:value]) 42 | end 43 | end 44 | end 45 | 46 | module PlainColorValue 47 | def transform_color(context) 48 | context[:value] 49 | end 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/csscss/parser/common.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Common 4 | include Parslet 5 | 6 | UNITS = %w(px em ex in cm mm pt pc) 7 | 8 | rule(:space) { match['\s'].repeat(1) } 9 | rule(:space?) { space.maybe } 10 | rule(:number) { match["0-9"] } 11 | rule(:numbers) { number.repeat(1) } 12 | rule(:decimal) { numbers >> str(".").maybe >> numbers.maybe } 13 | rule(:percent) { decimal >> stri("%") >> space? } 14 | rule(:non_zero_length) { decimal >> stri_list(UNITS) >> space? } 15 | rule(:zero_length) { match["0"] } 16 | rule(:length) { zero_length | non_zero_length } 17 | rule(:identifier) { match["a-zA-Z"].repeat(1) } 18 | rule(:inherit) { stri("inherit") } 19 | rule(:eof) { any.absent? } 20 | rule(:nada) { any.repeat.as(:nada) } 21 | 22 | rule(:http) { 23 | (match['a-zA-Z0-9.:/\-'] | str('\(') | str('\)')).repeat >> space? 24 | } 25 | 26 | rule(:data) { 27 | stri("data:") >> match['a-zA-Z0-9.:/+;,=\-'].repeat >> space? 28 | } 29 | 30 | rule(:url) { 31 | stri("url") >> parens do 32 | (any_quoted { http } >> space?) | 33 | (any_quoted { data } >> space?) | 34 | data | http 35 | end 36 | } 37 | 38 | def stri(str) 39 | key_chars = str.split(//) 40 | key_chars. 41 | collect! { |char| 42 | if char.upcase == char.downcase 43 | str(char) 44 | else 45 | match["#{char.upcase}#{char.downcase}"] 46 | end 47 | }.reduce(:>>) 48 | end 49 | 50 | def symbol(s, label = nil) 51 | if label 52 | stri(s).as(label) >> space? 53 | else 54 | stri(s) >> space? 55 | end 56 | end 57 | 58 | def between(left, right) 59 | raise "block not given" unless block_given? 60 | symbol(left) >> yield >> symbol(right) 61 | end 62 | 63 | def parens(&block) 64 | between("(", ")", &block) 65 | end 66 | 67 | def double_quoted(&block) 68 | between('"', '"', &block) 69 | end 70 | 71 | def single_quoted(&block) 72 | between("'", "'", &block) 73 | end 74 | 75 | def any_quoted(&block) 76 | double_quoted(&block) | single_quoted(&block) 77 | end 78 | 79 | def stri_list(list) 80 | list.map {|u| stri(u) }.reduce(:|) 81 | end 82 | 83 | def symbol_list(list) 84 | list.map {|u| symbol(u) }.reduce(:|) 85 | end 86 | 87 | def try_parse(input) 88 | parsed = (root | nada).parse(input) 89 | parsed[:nada] ? false : parsed 90 | end 91 | end 92 | end 93 | end 94 | -------------------------------------------------------------------------------- /lib/csscss/parser/css.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | # This is heavily based on the haskell css parser on 4 | # https://github.com/yesodweb/css-text/blob/add139487c38b68845246449d01c13dbcebac39d/Text/CSS/Parse.hs 5 | module Css 6 | class << self 7 | def parse(source) 8 | Transformer.new.apply(Parser.new.parse(source)) 9 | end 10 | end 11 | 12 | class Parser < Parslet::Parser 13 | include Common 14 | 15 | rule(:raw_comment) { 16 | space? >> str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') >> space? 17 | } 18 | rule(:comment) { raw_comment.as(:comment) } 19 | 20 | rule(:blank_attribute) { str(";") >> space? } 21 | 22 | rule(:attribute_value) { any_quoted { any } | (str('/*').absent? >> match["^;}"]) | raw_comment } 23 | 24 | rule(:attribute) { 25 | match["^:{}"].repeat(1).as(:property) >> 26 | str(":") >> 27 | ( 28 | (stri("data:").absent? >> attribute_value) | 29 | (stri("data:").present? >> attribute_value.repeat(1) >> str(";") >> attribute_value.repeat(1)) 30 | ).repeat(1).as(:value) >> 31 | str(";").maybe >> 32 | space? 33 | } 34 | 35 | rule(:mixin_attributes) { 36 | ( 37 | str('/* CSSCSS START MIXIN') >> 38 | (str('*/').absent? >> any).repeat >> 39 | str('*/') >> 40 | (str('/* CSSCSS END MIXIN').absent? >> any).repeat >> 41 | str('/* CSSCSS END MIXIN') >> 42 | (str('*/').absent? >> any).repeat >> 43 | str('*/') >> 44 | space? 45 | ).as(:mixin) 46 | } 47 | 48 | rule(:ruleset) { 49 | ( 50 | match["^{}"].repeat(1).as(:selector) >> 51 | str("{") >> 52 | space? >> 53 | ( 54 | mixin_attributes | 55 | comment | 56 | attribute | 57 | blank_attribute 58 | ).repeat(0).as(:properties) >> 59 | str("}") >> 60 | space? 61 | ).as(:ruleset) 62 | } 63 | 64 | rule(:nested_ruleset) { 65 | ( 66 | str("@") >> 67 | match["^{}"].repeat(1) >> 68 | str("{") >> 69 | space? >> 70 | (comment | ruleset | nested_ruleset).repeat(1) >> 71 | str("}") >> 72 | space? 73 | ).as(:nested_ruleset) 74 | } 75 | 76 | rule(:import) { 77 | ( 78 | stri("@import") >> 79 | match["^;"].repeat(1) >> 80 | str(";") >> 81 | space? 82 | ).as(:import) 83 | } 84 | 85 | rule(:blocks) { 86 | space? >> ( 87 | comment | 88 | import | 89 | nested_ruleset | 90 | ruleset 91 | ).repeat(1).as(:blocks) >> space? 92 | } 93 | 94 | root(:blocks) 95 | end 96 | 97 | class Transformer < Parslet::Transform 98 | rule(nested_ruleset: subtree(:rulesets)) { |context| 99 | context[:rulesets].flatten 100 | } 101 | 102 | rule(import: simple(:import)) { [] } 103 | rule(mixin: simple(:mixin)) { nil } 104 | 105 | rule(comment: simple(:comment)) { nil } 106 | 107 | rule(ruleset: { 108 | selector: simple(:selector), 109 | properties: sequence(:properties) 110 | }) { 111 | Ruleset.new(Selector.from_parser(selector), properties.compact) 112 | } 113 | 114 | rule({ 115 | property: simple(:property), 116 | value: simple(:value) 117 | }) { 118 | Declaration.from_parser(property, value) 119 | } 120 | 121 | rule(blocks: subtree(:rulesets)) {|context| 122 | context[:rulesets].flatten.compact 123 | } 124 | end 125 | end 126 | end 127 | end 128 | -------------------------------------------------------------------------------- /lib/csscss/parser/font.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Font 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:literal_font) { 10 | symbol_list(%w(caption icon menu message-box 11 | small-caption status-bar)) 12 | } 13 | 14 | rule(:font_style) { symbol_list(%w(normal italic oblique)) } 15 | rule(:font_variant) { symbol_list(%w(normal small-caps)) } 16 | rule(:font_weight) { 17 | symbol_list(%w(normal bold bolder lighter 100 200 300 400 500 600 700 800 900)) 18 | } 19 | 20 | rule(:font_size_absolute) { 21 | symbol_list(%w(xx-small x-small small medium 22 | large x-large xx-large)) 23 | } 24 | 25 | rule(:font_size_relative) { symbol_list(%w(larger smaller)) } 26 | 27 | rule(:font_size) { 28 | font_size_absolute | font_size_relative | length | percent 29 | } 30 | 31 | rule(:line_height) { 32 | symbol("/") >> ( 33 | symbol("normal") | (length | percent | numbers) >> space? 34 | ).as(:line_height_value) 35 | } 36 | 37 | rule(:font_family) { 38 | family = identifier | any_quoted { identifier >> (space? >> identifier).repeat } 39 | family >> (symbol(",") >> font_family).maybe 40 | } 41 | 42 | rule(:font) { 43 | ( 44 | symbol("inherit") >> eof | ( 45 | ( 46 | literal_font.maybe.as(:literal_font) | 47 | font_style.maybe.as(:font_style) >> 48 | font_variant.maybe.as(:font_variant) >> 49 | font_weight.maybe.as(:font_weight) >> 50 | 51 | font_size.as(:font_size) >> 52 | line_height.maybe.as(:line_height) >> 53 | font_family.as(:font_family) 54 | ) 55 | ) 56 | ).as(:font) 57 | } 58 | root(:font) 59 | end 60 | 61 | class Transformer < Parslet::Transform 62 | rule(font: simple(:inherit)) {[]} 63 | rule(font: {literal_font:simple(:literal)}) {[]} 64 | 65 | rule(line_height_value: simple(:value)) { value } 66 | 67 | rule(font: { 68 | font_style: simple(:font_style), 69 | font_variant: simple(:font_variant), 70 | font_weight: simple(:font_weight), 71 | font_size: simple(:font_size), 72 | line_height: simple(:line_height), 73 | font_family: simple(:font_family) 74 | }) {|context| 75 | [].tap do |declarations| 76 | context.each do |property, value| 77 | declarations << Declaration.from_parser(property.to_s.gsub("_", "-"), value, property != :font_family) if value 78 | end 79 | end 80 | } 81 | 82 | #rule(outline: { 83 | #outline_width:simple(:width), 84 | #outline_style:simple(:style), 85 | #outline_color:simple(:color) 86 | #}) { 87 | #[].tap do |declarations| 88 | #declarations << Declaration.from_parser("outline-width", width) if width 89 | #declarations << Declaration.from_parser("outline-style", style) if style 90 | #declarations << Declaration.from_parser("outline-color", color) if color 91 | #end 92 | #} 93 | end 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/csscss/parser/list_style.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module ListStyle 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:type) { 10 | symbol_list(%w(disc circle square decimal decimal-leading-zero lower-roman upper-roman lower-greek lower-latin upper-latin armenian georgian lower-alpha upper-alpha none inherit)).as(:type) 11 | } 12 | 13 | rule(:position) { symbol_list(%w(inside outside inherit)).as(:position) } 14 | rule(:image) { (url | symbol_list(%w(none inherit))).as(:image) } 15 | 16 | rule(:list_style) { 17 | ( 18 | symbol("inherit") >> eof | ( 19 | type.maybe.as(:list_style_type) >> 20 | position.maybe.as(:list_style_position) >> 21 | image.maybe.as(:list_style_image) 22 | ) 23 | ).as(:list_style) 24 | } 25 | root(:list_style) 26 | end 27 | 28 | class Transformer < Parslet::Transform 29 | rule(:list_style => simple(:inherit)) {[]} 30 | rule(type:simple(:type)) { Declaration.from_parser("list-style-type", type) } 31 | rule(position:simple(:position)) { Declaration.from_parser("list-style-position", position) } 32 | rule(image:simple(:image)) { Declaration.from_parser("list-style-image", image) } 33 | 34 | rule(list_style: { 35 | list_style_type:simple(:type), 36 | list_style_position:simple(:position), 37 | list_style_image:simple(:image) 38 | }) { 39 | [type, position, image].compact 40 | } 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/csscss/parser/margin.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Margin 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:margin_side) { 10 | length | percent | symbol_list(%w(inherit auto)) 11 | } 12 | 13 | rule(:margin) { 14 | ( 15 | symbol("inherit") >> eof | ( 16 | margin_side.maybe.as(:top) >> 17 | margin_side.maybe.as(:right) >> 18 | margin_side.maybe.as(:bottom) >> 19 | margin_side.maybe.as(:left) 20 | ) 21 | ).as(:margin) 22 | } 23 | root(:margin) 24 | end 25 | 26 | class Transformer < Parslet::Transform 27 | @property = :margin 28 | extend MultiSideTransformer 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/csscss/parser/multi_side_transformer.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module MultiSideTransformer 4 | def self.extended(base) 5 | base.instance_eval do 6 | extend ClassMethods 7 | 8 | rule(@property => simple(:inherit)) {[]} 9 | 10 | rule({@property => { 11 | top:simple(:top), 12 | right:simple(:right), 13 | bottom:simple(:bottom), 14 | left:simple(:left) 15 | }}, &method(:transform_sides)) 16 | end 17 | end 18 | 19 | module ClassMethods 20 | def side_declaration(side, value) 21 | Declaration.from_parser("#{@property}-#{side}", value) 22 | end 23 | 24 | def transform_sides(context) 25 | values = [context[:top], context[:right], context[:bottom], context[:left]].compact 26 | case values.size 27 | when 4 28 | %w(top right bottom left).zip(values).map {|side, value| side_declaration(side, value) } 29 | when 3 30 | %w(top right bottom).zip(values).map {|side, value| side_declaration(side, value) }.tap do |declarations| 31 | declarations << side_declaration("left", values[1]) 32 | end 33 | when 2 34 | %w(top right).zip(values).map {|side, value| side_declaration(side, value) }.tap do |declarations| 35 | declarations << side_declaration("bottom", values[0]) 36 | declarations << side_declaration("left", values[1]) 37 | end 38 | when 1 39 | %w(top right bottom left).map do |side| 40 | side_declaration(side, values[0]) 41 | end 42 | end 43 | end 44 | end 45 | end 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /lib/csscss/parser/outline.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Outline 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Color 8 | 9 | rule(:outline_width) { BorderWidth::Parser.new.border_width_side } 10 | rule(:outline_style) { BorderStyle::Parser.new.border_style_side } 11 | rule(:outline_color) { BorderColor::Parser.new.border_color_side } 12 | 13 | rule(:outline) { 14 | ( 15 | symbol("inherit") >> eof | ( 16 | outline_width.maybe.as(:outline_width) >> 17 | outline_style.maybe.as(:outline_style) >> 18 | outline_color.maybe.as(:outline_color) 19 | ) 20 | ).as(:outline) 21 | } 22 | root(:outline) 23 | end 24 | 25 | class Transformer < Parslet::Transform 26 | extend Color::Transformer 27 | extend Color::PlainColorValue 28 | 29 | rule(outline: simple(:inherit)) {[]} 30 | 31 | rule(outline: { 32 | outline_width:simple(:width), 33 | outline_style:simple(:style), 34 | outline_color:simple(:color) 35 | }) { 36 | [].tap do |declarations| 37 | declarations << Declaration.from_parser("outline-width", width) if width 38 | declarations << Declaration.from_parser("outline-style", style) if style 39 | declarations << Declaration.from_parser("outline-color", color) if color 40 | end 41 | } 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/csscss/parser/padding.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | module Parser 3 | module Padding 4 | extend Parser::Base 5 | 6 | class Parser < Parslet::Parser 7 | include Common 8 | 9 | rule(:padding_side) { 10 | length | percent | symbol("inherit") 11 | } 12 | 13 | rule(:padding) { 14 | ( 15 | symbol("inherit") >> eof | ( 16 | padding_side.maybe.as(:top) >> 17 | padding_side.maybe.as(:right) >> 18 | padding_side.maybe.as(:bottom) >> 19 | padding_side.maybe.as(:left) 20 | ) 21 | ).as(:padding) 22 | } 23 | root(:padding) 24 | end 25 | 26 | class Transformer < Parslet::Transform 27 | @property = :padding 28 | extend MultiSideTransformer 29 | end 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/csscss/redundancy_analyzer.rb: -------------------------------------------------------------------------------- 1 | # TODO: this class really needs some work. It works, but it is horrid. 2 | module Csscss 3 | class RedundancyAnalyzer 4 | def initialize(raw_css) 5 | @raw_css = raw_css 6 | end 7 | 8 | def redundancies(opts = {}) 9 | minimum = opts[:minimum] 10 | ignored_properties = opts[:ignored_properties] || [] 11 | ignored_selectors = opts[:ignored_selectors] || [] 12 | match_shorthand = opts.fetch(:match_shorthand, true) 13 | 14 | rule_sets = Parser::Css.parse(@raw_css) 15 | matches = {} 16 | parents = {} 17 | rule_sets.each do |rule_set| 18 | next if ignored_selectors.include?(rule_set.selectors.selectors) 19 | sel = rule_set.selectors 20 | 21 | rule_set.declarations.each do |dec| 22 | next if ignored_properties.include?(dec.property) 23 | 24 | if match_shorthand && parser = shorthand_parser(dec.property) 25 | if new_decs = parser.parse(dec.property, dec.value) 26 | if dec.property == "border" 27 | %w(border-top border-right border-bottom border-left).each do |property| 28 | border_dec = Declaration.new(property, dec.value) 29 | parents[border_dec] ||= [] 30 | (parents[border_dec] << dec).uniq! 31 | border_dec.parents = parents[border_dec] 32 | 33 | matches[border_dec] ||= [] 34 | matches[border_dec] << sel 35 | matches[border_dec].uniq! 36 | end 37 | end 38 | 39 | new_decs.each do |new_dec| 40 | # replace any non-derivatives with derivatives 41 | existing = matches.delete(new_dec) || [] 42 | existing << sel 43 | parents[new_dec] ||= [] 44 | (parents[new_dec] << dec).uniq! 45 | new_dec.parents = parents[new_dec] 46 | matches[new_dec] = existing 47 | matches[new_dec].uniq! 48 | end 49 | end 50 | end 51 | 52 | matches[dec] ||= [] 53 | matches[dec] << sel 54 | matches[dec].uniq! 55 | end 56 | end 57 | 58 | inverted_matches = {} 59 | matches.each do |declaration, selector_groups| 60 | if selector_groups.size > 1 61 | selector_groups.combination(2).each do |two_selectors| 62 | inverted_matches[two_selectors] ||= [] 63 | inverted_matches[two_selectors] << declaration 64 | end 65 | end 66 | end 67 | 68 | # trims any derivative declarations alongside shorthand 69 | inverted_matches.each do |selectors, declarations| 70 | redundant_derivatives = declarations.select do |dec| 71 | dec.derivative? && declarations.detect {|dec2| dec2 > dec } 72 | end 73 | unless redundant_derivatives.empty? 74 | inverted_matches[selectors] = declarations - redundant_derivatives 75 | end 76 | 77 | # border needs to be reduced even more 78 | %w(width style color).each do |property| 79 | decs = inverted_matches[selectors].select do |dec| 80 | dec.derivative? && dec.property =~ /border-\w+-#{property}/ 81 | end 82 | if decs.size == 4 && decs.map(&:value).uniq.size == 1 83 | inverted_matches[selectors] -= decs 84 | inverted_matches[selectors] << Declaration.new("border-#{property}", decs.first.value) 85 | end 86 | end 87 | end 88 | 89 | if minimum 90 | inverted_matches.delete_if do |key, declarations| 91 | declarations.size < minimum 92 | end 93 | end 94 | 95 | # combines selector keys by common declarations 96 | final_inverted_matches = inverted_matches.dup 97 | inverted_matches.to_a.each_with_index do |(selector_group1, declarations1), index| 98 | keys = [selector_group1] 99 | inverted_matches.to_a[(index + 1)..-1].each do |selector_group2, declarations2| 100 | if declarations1 == declarations2 && final_inverted_matches[selector_group2] 101 | keys << selector_group2 102 | final_inverted_matches.delete(selector_group2) 103 | end 104 | end 105 | 106 | if keys.size > 1 107 | final_inverted_matches.delete(selector_group1) 108 | key = keys.flatten.sort.uniq 109 | final_inverted_matches[key] = declarations1 110 | end 111 | end 112 | 113 | # sort hash by number of matches 114 | sorted_array = final_inverted_matches.sort {|(_, v1), (_, v2)| v2.size <=> v1.size } 115 | {}.tap do |sorted_hash| 116 | sorted_array.each do |key, value| 117 | sorted_hash[key.sort] = value.sort 118 | end 119 | end 120 | end 121 | 122 | private 123 | def shorthand_parser(property) 124 | case property 125 | when "background" then Parser::Background 126 | when "list-style" then Parser::ListStyle 127 | when "margin" then Parser::Margin 128 | when "padding" then Parser::Padding 129 | when "border" then Parser::Border 130 | when "border-width" then Parser::BorderWidth 131 | when "border-style" then Parser::BorderStyle 132 | when "border-color" then Parser::BorderColor 133 | when "outline" then Parser::Outline 134 | when "font" then Parser::Font 135 | when "border-top", "border-right", "border-bottom", "border-left" 136 | Parser::BorderSide 137 | end 138 | end 139 | end 140 | end 141 | -------------------------------------------------------------------------------- /lib/csscss/reporter.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | class Reporter 3 | def initialize(redundancies) 4 | @redundancies = redundancies 5 | end 6 | 7 | def report(options = {}) 8 | verbose = options.fetch(:verbose, false) 9 | should_color = options.fetch(:color, true) 10 | 11 | io = StringIO.new 12 | @redundancies.each do |selector_groups, declarations| 13 | selector_groups = selector_groups.map {|selectors| "{#{maybe_color(selectors, :red, should_color)}}" } 14 | last_selector = selector_groups.pop 15 | count = declarations.size 16 | io.puts %Q(#{selector_groups.join(", ")} AND #{last_selector} share #{maybe_color(count, :red, should_color)} declaration#{"s" if count > 1}) 17 | if verbose 18 | declarations.each {|dec| io.puts(" - #{maybe_color(dec, :yellow, should_color)}") } 19 | end 20 | end 21 | 22 | io.rewind 23 | io.read 24 | end 25 | 26 | private 27 | def maybe_color(string, color, condition) 28 | condition ? string.to_s.colorize(color) : string 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /lib/csscss/sass_include_extensions.rb: -------------------------------------------------------------------------------- 1 | require "sass" 2 | 3 | Sass::Tree::MixinDefNode.class_eval do 4 | def children 5 | first_child = @children.first 6 | 7 | # not sure why/how we can get here with empty children, but it 8 | # causes issues 9 | unless @children.empty? || (first_child.is_a?(Sass::Tree::CommentNode) && first_child.value.first =~ /CSSCSS START/) 10 | begin_comment = Sass::Tree::CommentNode.new(["/* CSSCSS START MIXIN: #{name} */"], :normal) 11 | end_comment = Sass::Tree::CommentNode.new(["/* CSSCSS END MIXIN: #{name} */"], :normal) 12 | 13 | begin_comment.options = end_comment.options = {} 14 | @children.unshift(begin_comment) 15 | @children.push(end_comment) 16 | end 17 | 18 | @children 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/csscss/types.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | Declaration = Struct.new(:property, :value, :parents) do 3 | def self.from_parser(property, value, clean = true) 4 | value = value.to_s 5 | property = property.to_s 6 | if clean 7 | value = value.downcase 8 | property = property.downcase 9 | end 10 | new(property, value.strip) 11 | end 12 | 13 | def derivative? 14 | !parents.nil? 15 | end 16 | 17 | def without_parents 18 | if derivative? 19 | dup.tap do |duped| 20 | duped.parents = nil 21 | end 22 | else 23 | self 24 | end 25 | end 26 | 27 | def ==(other) 28 | if other.respond_to?(:property) && other.respond_to?(:value) 29 | # using eql? tanks performance 30 | property == other.property && normalize_value(value) == normalize_value(other.value) 31 | else 32 | false 33 | end 34 | end 35 | 36 | def hash 37 | [property, normalize_value(value)].hash 38 | end 39 | 40 | def eql?(other) 41 | hash == other.hash 42 | end 43 | 44 | def <=>(other) 45 | property <=> other.property 46 | end 47 | 48 | def >(other) 49 | other.derivative? && other.parents.include?(self) 50 | end 51 | 52 | def <(other) 53 | other > self 54 | end 55 | 56 | def to_s 57 | "#{property}: #{value}" 58 | end 59 | 60 | def inspect 61 | if parents 62 | "<#{self.class} #{to_s} (parents: #{parents})>" 63 | else 64 | "<#{self.class} #{to_s}>" 65 | end 66 | end 67 | 68 | private 69 | def normalize_value(value) 70 | if value =~ /^0(#{Csscss::Parser::Common::UNITS.join("|")}|%)$/ 71 | "0" 72 | else 73 | value 74 | end 75 | end 76 | end 77 | 78 | Selector = Struct.new(:selectors) do 79 | def self.from_parser(selectors) 80 | new(selectors.to_s.strip) 81 | end 82 | 83 | def <=>(other) 84 | selectors <=> other.selectors 85 | end 86 | 87 | def to_s 88 | selectors 89 | end 90 | 91 | def inspect 92 | "<#{self.class} #{selectors}>" 93 | end 94 | end 95 | 96 | Ruleset = Struct.new(:selectors, :declarations) 97 | end 98 | -------------------------------------------------------------------------------- /lib/csscss/version.rb: -------------------------------------------------------------------------------- 1 | module Csscss 2 | VERSION = "1.3.3" 3 | end 4 | -------------------------------------------------------------------------------- /test/csscss/json_reporter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss 4 | describe JSONReporter do 5 | include TypeHelpers 6 | 7 | it "formats json result" do 8 | reporter = JSONReporter.new({ 9 | [sel(".foo"), sel(".bar")] => [dec("width", "1px"), dec("border", "black")], 10 | [sel("h1, h2"), sel(".foo"), sel(".baz")] => [dec("display", "none")], 11 | [sel("h1, h2"), sel(".bar")] => [dec("position", "relative")] 12 | }) 13 | 14 | expected = [ 15 | { 16 | "selectors" => %w(.foo .bar), 17 | "count" => 2, 18 | "declarations" => ["width: 1px", "border: black"] 19 | }, 20 | { 21 | "selectors" => ["h1, h2", ".foo", ".baz"], 22 | "count" => 1, 23 | "declarations" => ["display: none"] 24 | }, 25 | { 26 | "selectors" => ["h1, h2", ".bar"], 27 | "count" => 1, 28 | "declarations" => ["position: relative"] 29 | }, 30 | ] 31 | reporter.report.must_equal JSON.dump(expected) 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/csscss/parser/background_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Background 5 | describe Background do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "parses position" do 14 | @parser.bg_position.must_parse("10.2%") 15 | @parser.bg_position.must_parse("left") 16 | @parser.bg_position.must_parse("44em") 17 | @parser.bg_position.must_parse("left 11%") 18 | @parser.bg_position.must_parse("left bottom") 19 | @parser.bg_position.must_parse("inherit") 20 | @parser.bg_position.must_parse("bottom") 21 | @parser.bg_position.wont_parse("bottom left") 22 | @parser.bg_position.wont_parse("inherit bottom") 23 | end 24 | 25 | it "converts shorthand rules to longhand" do 26 | trans("rgb(111, 222, 333) none repeat-x scroll").must_equal([ 27 | dec("background-color", "rgb(111, 222, 333)"), 28 | dec("background-image", "none"), 29 | dec("background-repeat", "repeat-x"), 30 | dec("background-attachment", "scroll") 31 | ]) 32 | 33 | trans("inherit none inherit 10% bottom").must_equal([ 34 | dec("background-color", "inherit"), 35 | dec("background-image", "none"), 36 | dec("background-repeat", "inherit"), 37 | dec("background-position", "10% bottom") 38 | ]) 39 | 40 | trans("#fff url(http://foo.com/bar.jpg) bottom").must_equal([ 41 | dec("background-color", "#fff"), 42 | dec("background-image", "url(http://foo.com/bar.jpg)"), 43 | dec("background-position", "bottom") 44 | ]) 45 | 46 | trans("#fff").must_equal([dec("background-color", "#fff")]) 47 | trans("BLACK").must_equal([dec("background-color", "black")]) 48 | end 49 | 50 | it "tries the parse and returns false if it doesn't work" do 51 | @parser.try_parse("foo").must_equal(false) 52 | parsed = @parser.try_parse("black") 53 | parsed[:background][:bg_color].must_equal(color:{keyword:"black"}) 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/csscss/parser/border_color_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module BorderColor 5 | describe BorderColor do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("#fff transparent black rgb(1, 2, 3)").must_equal([ 15 | dec("border-top-color", "#fff"), 16 | dec("border-right-color", "transparent"), 17 | dec("border-bottom-color", "black"), 18 | dec("border-left-color", "rgb(1, 2, 3)") 19 | ]) 20 | 21 | trans("#fff black").must_equal([ 22 | dec("border-top-color", "#fff"), 23 | dec("border-right-color", "black"), 24 | dec("border-bottom-color", "#fff"), 25 | dec("border-left-color", "black") 26 | ]) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/csscss/parser/border_side_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module BorderSide 5 | describe self do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new("top") 10 | end 11 | 12 | def trans_top(s) 13 | Transformer.new.apply(@parser.parse(s)) 14 | end 15 | alias_method :trans, :trans_top 16 | 17 | def trans_bottom(s) 18 | Transformer.new.apply(Parser.new("bottom").parse(s)) 19 | end 20 | 21 | it "converts shorthand rules to longhand" do 22 | trans_top("thin").must_equal([ 23 | dec("border-top-width", "thin") 24 | ]) 25 | 26 | trans_bottom("rgb(1, 2, 3)").must_equal([ 27 | dec("border-bottom-color", "rgb(1, 2, 3)") 28 | ]) 29 | 30 | trans_top("thin solid #fff").must_equal([ 31 | dec("border-top-width", "thin"), 32 | dec("border-top-style", "solid"), 33 | dec("border-top-color", "#fff") 34 | ]) 35 | 36 | trans_bottom("thin solid #fff").must_equal([ 37 | dec("border-bottom-width", "thin"), 38 | dec("border-bottom-style", "solid"), 39 | dec("border-bottom-color", "#fff") 40 | ]) 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/csscss/parser/border_style_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module BorderStyle 5 | describe BorderStyle do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("none dashed solid ridge").must_equal([ 15 | dec("border-top-style", "none"), 16 | dec("border-right-style", "dashed"), 17 | dec("border-bottom-style", "solid"), 18 | dec("border-left-style", "ridge") 19 | ]) 20 | 21 | trans("none dashed").must_equal([ 22 | dec("border-top-style", "none"), 23 | dec("border-right-style", "dashed"), 24 | dec("border-bottom-style", "none"), 25 | dec("border-left-style", "dashed") 26 | ]) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/csscss/parser/border_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Border 5 | describe Border do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("1px solid #fff").must_equal([ 15 | dec("border-top-width", "1px"), 16 | dec("border-top-style", "solid"), 17 | dec("border-top-color", "#fff"), 18 | dec("border-right-width", "1px"), 19 | dec("border-right-style", "solid"), 20 | dec("border-right-color", "#fff"), 21 | dec("border-bottom-width", "1px"), 22 | dec("border-bottom-style", "solid"), 23 | dec("border-bottom-color", "#fff"), 24 | dec("border-left-width", "1px"), 25 | dec("border-left-style", "solid"), 26 | dec("border-left-color", "#fff") 27 | ]) 28 | end 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /test/csscss/parser/border_width_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module BorderWidth 5 | describe BorderWidth do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("thin thick inherit 10em").must_equal([ 15 | dec("border-top-width", "thin"), 16 | dec("border-right-width", "thick"), 17 | dec("border-bottom-width", "inherit"), 18 | dec("border-left-width", "10em") 19 | ]) 20 | 21 | trans("thin thick").must_equal([ 22 | dec("border-top-width", "thin"), 23 | dec("border-right-width", "thick"), 24 | dec("border-bottom-width", "thin"), 25 | dec("border-left-width", "thick") 26 | ]) 27 | end 28 | end 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /test/csscss/parser/color_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | describe Color do 5 | class ColorTest 6 | include Color 7 | end 8 | 9 | before { @parser = ColorTest.new } 10 | 11 | describe "color" do 12 | it "parses color" do 13 | @parser.color.must_parse "rgb(123, 222, 444)" 14 | @parser.color.must_parse "rgb(123%, 222%, 444%)" 15 | @parser.color.must_parse "#ffffff" 16 | @parser.color.must_parse "inherit" 17 | @parser.color.must_parse "black" 18 | end 19 | end 20 | 21 | describe "individual rules" do 22 | it "parses rgb number color" do 23 | @parser.rgb.must_parse "rgb(123, 222, 444)" 24 | @parser.rgb.must_parse "rgb ( 123 , 222 , 444 ) " 25 | @parser.rgb.wont_parse "rgb(1aa, 222, 444)" 26 | end 27 | 28 | it "parses rgb percentage color" do 29 | @parser.rgb.must_parse "rgb(123%, 222%, 444%)" 30 | @parser.rgb.must_parse "rgb ( 123% , 222% , 444% ) " 31 | @parser.rgb.wont_parse "rgb(1aa%, 222%, 444%)" 32 | end 33 | 34 | it "parses hex colors" do 35 | @parser.hexcolor.must_parse "#ffffff" 36 | @parser.hexcolor.must_parse "#ffffff " 37 | @parser.hexcolor.must_parse "#fff " 38 | @parser.hexcolor.must_parse "#fFF123" 39 | @parser.hexcolor.wont_parse "fFF123" 40 | end 41 | 42 | it "parses keyword colors" do 43 | @parser.color_keyword.must_parse "inherit" 44 | @parser.color_keyword.must_parse "inherit " 45 | 46 | @parser.color_keyword.must_parse "black" 47 | @parser.color_keyword.must_parse "BLACK" 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/csscss/parser/common_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | describe Common do 5 | class CommonTest 6 | include Common 7 | end 8 | 9 | before { @parser = CommonTest.new } 10 | 11 | describe "#stri" do 12 | it "parses case insensitive strings" do 13 | @parser.stri("a").must_parse "a" 14 | @parser.stri("A").must_parse "a" 15 | @parser.stri("A").wont_parse "b" 16 | 17 | @parser.stri("This too shall pass").must_parse "this TOO shall PASS" 18 | @parser.stri("[").must_parse "[" 19 | end 20 | end 21 | 22 | describe "#spaces" do 23 | it "parses series of spaces" do 24 | @parser.space.must_parse " " 25 | @parser.space.must_parse " " 26 | @parser.space.must_parse "\n" 27 | @parser.space.wont_parse " a" 28 | 29 | @parser.space.wont_parse "" 30 | @parser.space?.must_parse "" 31 | end 32 | end 33 | 34 | describe "#symbol" do 35 | it "parses case insensitive characters followed by spaces" do 36 | @parser.symbol("foo").must_parse "foo" 37 | @parser.symbol("foo").must_parse "foo " 38 | @parser.symbol("foo").must_parse "Foo " 39 | @parser.symbol("foo").wont_parse " Foo " 40 | end 41 | 42 | it "optionally captures input" do 43 | parsed = @parser.symbol("foo", :foo).parse("Foo ") 44 | parsed[:foo].must_equal "Foo" 45 | end 46 | end 47 | 48 | describe "between and helpers" do 49 | it "parses characters surrounded" do 50 | @parser.between("[", "]") { @parser.symbol("foo") }.must_parse "[foo]" 51 | end 52 | 53 | it "parses input surrounded by parens" do 54 | @parser.parens { @parser.symbol("foo") }.must_parse "(foo)" 55 | @parser.parens { @parser.symbol("foo") }.must_parse "(FOo) " 56 | @parser.parens { @parser.symbol("foo") }.must_parse "(FOo ) " 57 | @parser.parens { @parser.symbol("food") }.wont_parse "(FOo" 58 | end 59 | 60 | it "parses input surrounded by double quotes" do 61 | @parser.double_quoted { @parser.symbol("foo") }.must_parse %("foo") 62 | @parser.double_quoted { @parser.symbol("foo") }.must_parse %("FOo ") 63 | @parser.double_quoted { @parser.symbol("foo") }.must_parse %("FOo " ) 64 | @parser.double_quoted { @parser.symbol("food") }.wont_parse %("FOo) 65 | end 66 | 67 | it "parses input surrounded by single quotes" do 68 | @parser.single_quoted { @parser.symbol('foo') }.must_parse %('foo') 69 | @parser.single_quoted { @parser.symbol('foo') }.must_parse %('FOo ') 70 | @parser.single_quoted { @parser.symbol('foo') }.must_parse %('FOo ' ) 71 | @parser.single_quoted { @parser.symbol('food') }.wont_parse %('FOo) 72 | end 73 | end 74 | 75 | describe "number and numbers" do 76 | it "parses single numbers" do 77 | @parser.number.must_parse "1" 78 | @parser.number.wont_parse "12" 79 | @parser.number.wont_parse "a" 80 | @parser.number.wont_parse "1 " 81 | end 82 | 83 | it "parses multiple numbers" do 84 | @parser.numbers.must_parse "1" 85 | @parser.numbers.must_parse "12" 86 | @parser.numbers.wont_parse "12 " 87 | @parser.numbers.wont_parse "1223a" 88 | end 89 | 90 | it "parses decimals" do 91 | @parser.decimal.must_parse "1" 92 | @parser.decimal.must_parse "12" 93 | @parser.decimal.must_parse "12." 94 | @parser.decimal.must_parse "12.0123" 95 | @parser.decimal.wont_parse "1223a" 96 | end 97 | 98 | it "parses percentages" do 99 | @parser.percent.must_parse "100%" 100 | @parser.percent.must_parse "100% " 101 | @parser.percent.must_parse "100.344%" 102 | @parser.percent.wont_parse "100 %" 103 | end 104 | 105 | it "parses lengths" do 106 | @parser.length.must_parse "123px" 107 | @parser.length.must_parse "123EM" 108 | @parser.length.must_parse "1.23Pt" 109 | @parser.length.must_parse "0" 110 | @parser.length.wont_parse "1" 111 | end 112 | end 113 | 114 | describe "url" do 115 | it "parses http" do 116 | @parser.http.must_parse "foo.jpg" 117 | @parser.http.must_parse 'foo\(bar\).jpg' 118 | @parser.http.must_parse 'http://foo\(bar\).jpg' 119 | @parser.http.must_parse 'http://foo.com/baz/\(bar\).jpg' 120 | @parser.http.must_parse "//foo.com/foo.jpg" 121 | @parser.http.must_parse "https://foo.com/foo.jpg" 122 | @parser.http.must_parse "http://foo100.com/foo.jpg" 123 | @parser.http.must_parse "http://foo-bar.com/foo.jpg" 124 | end 125 | 126 | it "parses data" do 127 | @parser.data.must_parse "data:image/jpg;base64,IMGDATAGOESHERE==" 128 | @parser.data.must_parse "data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==" 129 | @parser.data.must_parse "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC" 130 | end 131 | 132 | it "parses urls" do 133 | @parser.url.must_parse "url(foo.jpg)" 134 | @parser.url.must_parse "url( foo.jpg )" 135 | @parser.url.must_parse 'url("foo.jpg")' 136 | @parser.url.must_parse "url('foo.jpg')" 137 | @parser.url.must_parse "url('foo.jpg' )" 138 | @parser.url.must_parse 'url(foo\(bar\).jpg)' 139 | @parser.url.must_parse "url(data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==)" 140 | @parser.url.must_parse "url('data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==')" 141 | end 142 | 143 | it "parses specials characters" do 144 | @parser.between('"', '"') { @parser.symbol("{") }.must_parse '"{"' 145 | @parser.between('"', '"') { @parser.symbol("}") }.must_parse '"}"' 146 | @parser.between('"', '"') { @parser.symbol("%") }.must_parse '"%"' 147 | @parser.double_quoted { @parser.symbol("{") }.must_parse %("{") 148 | @parser.single_quoted { @parser.symbol('{') }.must_parse %('{') 149 | end 150 | end 151 | end 152 | end 153 | -------------------------------------------------------------------------------- /test/csscss/parser/css_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Css 5 | describe self do 6 | include CommonParserTests::Helpers 7 | include TypeHelpers 8 | 9 | before do 10 | @parser = Parser.new 11 | @trans = Transformer.new 12 | end 13 | 14 | describe "parsing" do 15 | it "parses css" do 16 | @parser.must_parse "h1 { display: none }" 17 | @parser.must_parse "\nh1 { display: none; }" 18 | @parser.must_parse %$ 19 | .bar { border: 1px solid black } 20 | $ 21 | 22 | @parser.wont_parse "" 23 | end 24 | 25 | it "parses comments" do 26 | @parser.comment.must_parse "/* foo */" 27 | @parser.comment.must_parse %$ 28 | /* foo 29 | * bar 30 | */ 31 | $ 32 | 33 | @parser.comment.repeat(1).must_parse %$ 34 | /* foo */ 35 | /* bar */ 36 | $ 37 | end 38 | end 39 | 40 | it "transforms css" do 41 | css = %$ 42 | h1, h2 { display: none; position: relative; outline:none} 43 | .foo { display: none; width: 1px } 44 | .bar { border: 1px solid black } 45 | .baz { 46 | background-color: black; 47 | background-style: solid 48 | } 49 | $ 50 | 51 | trans(css).must_equal([ 52 | rs(sel("h1, h2"), [dec("display", "none"), dec("position", "relative"), dec("outline", "none")]), 53 | rs(sel(".foo"), [dec("display", "none"), dec("width", "1px")]), 54 | rs(sel(".bar"), [dec("border", "1px solid black")]), 55 | rs(sel(".baz"), [dec("background-color", "black"), dec("background-style", "solid")]) 56 | ]) 57 | end 58 | 59 | it "skips comments" do 60 | css = %$ 61 | /* some comment 62 | * foo 63 | */ 64 | .bar { border: 1px solid black /* sdflk */ } 65 | .baz { background: white /* sdflk */ } 66 | .baz2 { background: white /* {sdflk} */ } 67 | $ 68 | 69 | trans(css).must_equal([ 70 | rs(sel(".bar"), [dec("border", "1px solid black /* sdflk */")]), 71 | rs(sel(".baz"), [dec("background", "white /* sdflk */")]), 72 | rs(sel(".baz2"), [dec("background", "white /* {sdflk} */")]) 73 | ]) 74 | end 75 | 76 | it "skips rules that are commented out" do 77 | css = %$ 78 | /* 79 | bar { border: 1px solid black } 80 | */ 81 | 82 | /* foo */ 83 | .baz { 84 | background: white; 85 | /* bar */ 86 | border: 1px; 87 | } 88 | $ 89 | 90 | trans(css).must_equal([ 91 | rs(sel(".baz"), [dec("background", "white"), dec("border", "1px")]) 92 | ]) 93 | end 94 | 95 | it "parses commented attributes" do 96 | css = %$ 97 | .foo { 98 | /* 99 | some comment 100 | */ 101 | } 102 | $ 103 | 104 | trans(css).must_equal([ 105 | rs(sel(".foo"), []) 106 | ]) 107 | end 108 | 109 | it "recognizes @media queries" do 110 | css = %$ 111 | @media only screen { 112 | /* some comment */ 113 | #foo { 114 | background-color: black; 115 | } 116 | 117 | #bar { 118 | display: none; 119 | } 120 | } 121 | 122 | @media only screen { 123 | @-webkit-keyframes webkitSiblingBugfix { 124 | from { position: relative; } 125 | to { position: relative; } 126 | } 127 | 128 | a { position: relative } 129 | } 130 | 131 | h1 { 132 | outline: 1px; 133 | } 134 | $ 135 | 136 | trans(css).must_equal([ 137 | rs(sel("#foo"), [dec("background-color", "black")]), 138 | rs(sel("#bar"), [dec("display", "none")]), 139 | rs(sel("from"), [dec("position", "relative")]), 140 | rs(sel("to"), [dec("position", "relative")]), 141 | rs(sel("a"), [dec("position", "relative")]), 142 | rs(sel("h1"), [dec("outline", "1px")]) 143 | ]) 144 | end 145 | 146 | it "recognizes empty @media queries with no spaces" do 147 | css = %$ 148 | @media (min-width: 768px) and (max-width: 979px) {} 149 | $ 150 | 151 | trans(css).must_equal([ 152 | rs(sel("@media (min-width: 768px) and (max-width: 979px)"), []), 153 | ]) 154 | end 155 | 156 | it "recognizes empty @media queries with spaces" do 157 | css = %$ 158 | @media (min-width: 768px) and (max-width: 979px) { 159 | } 160 | $ 161 | 162 | trans(css).must_equal([ 163 | rs(sel("@media (min-width: 768px) and (max-width: 979px)"), []), 164 | ]) 165 | end 166 | 167 | it "ignores @import statements" do 168 | css = %$ 169 | @import "foo.css"; 170 | @import "bar.css"; 171 | 172 | /* 173 | .x { 174 | padding: 3px; 175 | } 176 | */ 177 | 178 | h1 { 179 | outline: 1px; 180 | } 181 | $ 182 | 183 | trans(css).must_equal([ 184 | rs(sel("h1"), [dec("outline", "1px")]) 185 | ]) 186 | end 187 | 188 | it "ignores double semicolons" do 189 | trans("h1 { display:none;;}").must_equal([ 190 | rs(sel("h1"), [dec("display", "none")]) 191 | ]) 192 | end 193 | 194 | it "ignores mixin selectors" do 195 | css = %$ 196 | h1 { 197 | /* CSSCSS START MIXIN: foo */ 198 | font-family: serif; 199 | font-size: 10px; 200 | display: block; 201 | /* CSSCSS END MIXIN: foo */ 202 | 203 | /* CSSCSS START MIXIN: bar */ 204 | outline: 1px; 205 | /* CSSCSS END MIXIN: bar */ 206 | 207 | float: left; 208 | } 209 | $ 210 | 211 | trans(css).must_equal([ 212 | rs(sel("h1"), [dec("float", "left")]) 213 | ]) 214 | end 215 | 216 | it "parses attributes with encoded data that include semicolons" do 217 | trans(%$ 218 | .foo1 { 219 | background: rgb(123, 123, 123) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x; 220 | display: block; 221 | } 222 | 223 | .foo2 { 224 | background: white url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x 225 | } 226 | 227 | .foo3 { 228 | outline: 1px; 229 | background: white url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x; 230 | display: block; 231 | } 232 | 233 | .foo4 { 234 | background: blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px; 235 | display: block; 236 | } 237 | $).must_equal([ 238 | rs(sel(".foo1"), [dec("background", "rgb(123, 123, 123) url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x"), 239 | dec("display", "block")]), 240 | rs(sel(".foo2"), [dec("background", "white url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x")]), 241 | rs(sel(".foo3"), [dec("outline", "1px"), 242 | dec("background", "white url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x"), 243 | dec("display", "block")]), 244 | rs(sel(".foo4"), [dec("background", "blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px"), 245 | dec("display", "block")]) 246 | ]) 247 | end 248 | 249 | it "parses attributes with special characters" do 250 | css = %$ 251 | 252 | #menu a::before { 253 | content: "{"; 254 | left: -6px; 255 | } 256 | 257 | #menu a::after { 258 | content: "}"; 259 | right: -6px; 260 | } 261 | 262 | #menu a::weird { 263 | content: "@"; 264 | up: -2px; 265 | } 266 | 267 | #menu a::after_all { 268 | content: '{'; 269 | right: -6px; 270 | } 271 | 272 | $ 273 | 274 | trans(css).must_equal([ 275 | rs(sel("#menu a::before"), [dec("content", '"{"'), 276 | dec("left", "-6px") 277 | ]), 278 | rs(sel("#menu a::after"), [dec("content", '"}"'), 279 | dec("right", "-6px") 280 | ]), 281 | rs(sel("#menu a::weird"), [dec("content", '"@"'), 282 | dec("up", "-2px") 283 | ]), 284 | rs(sel("#menu a::after_all"), [dec("content", "'{'"), 285 | dec("right", "-6px") 286 | ]) 287 | ]) 288 | end 289 | end 290 | end 291 | end 292 | -------------------------------------------------------------------------------- /test/csscss/parser/font_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Font 5 | describe self do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("10% Gill, 'Lucida Sans'").must_equal([ 15 | dec("font-size", "10%"), 16 | dec("font-family", "Gill, 'Lucida Sans'") 17 | ]) 18 | 19 | trans("normal small-caps 100 10% / 33 Gill, Helvetica, \"Lucida Sans\", cursive").must_equal([ 20 | dec("font-style", "normal"), 21 | dec("font-variant", "small-caps"), 22 | dec("font-weight", "100"), 23 | dec("font-size", "10%"), 24 | dec("line-height", "33"), 25 | dec("font-family", 'Gill, Helvetica, "Lucida Sans", cursive') 26 | ]) 27 | end 28 | 29 | it "parses font family" do 30 | @parser.font_family.must_parse("\"Lucida\"") 31 | @parser.font_family.must_parse("\"Lucida Sans\"") 32 | @parser.font_family.must_parse("Gill") 33 | @parser.font_family.must_parse('Gill, Helvetica, "Lucida Sans", cursive') 34 | end 35 | 36 | it "ignores literal fonts" do 37 | trans("caption").must_equal([]) 38 | trans("icon").must_equal([]) 39 | trans("menu").must_equal([]) 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/csscss/parser/list_style_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module ListStyle 5 | describe ListStyle do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("circle outside url('foo.jpg')").must_equal([ 15 | dec("list-style-type", "circle"), 16 | dec("list-style-position", "outside"), 17 | dec("list-style-image", "url('foo.jpg')") 18 | ]) 19 | end 20 | 21 | it "tries the parse and returns false if it doesn't work" do 22 | @parser.try_parse("foo").must_equal(false) 23 | parsed = @parser.try_parse("circle") 24 | parsed[:list_style][:list_style_type].must_equal(type:"circle") 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /test/csscss/parser/margin_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Margin 5 | describe Margin do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("1px 10% inherit auto").must_equal([ 15 | dec("margin-top", "1px"), 16 | dec("margin-right", "10%"), 17 | dec("margin-bottom", "inherit"), 18 | dec("margin-left", "auto") 19 | ]) 20 | 21 | trans("1px 10% inherit").must_equal([ 22 | dec("margin-top", "1px"), 23 | dec("margin-right", "10%"), 24 | dec("margin-bottom", "inherit"), 25 | dec("margin-left", "10%") 26 | ]) 27 | 28 | trans("1px 10%").must_equal([ 29 | dec("margin-top", "1px"), 30 | dec("margin-right", "10%"), 31 | dec("margin-bottom", "1px"), 32 | dec("margin-left", "10%") 33 | ]) 34 | 35 | trans("1px").must_equal([ 36 | dec("margin-top", "1px"), 37 | dec("margin-right", "1px"), 38 | dec("margin-bottom", "1px"), 39 | dec("margin-left", "1px") 40 | ]) 41 | end 42 | 43 | it "tries the parse and returns false if it doesn't work" do 44 | @parser.try_parse("foo").must_equal(false) 45 | parsed = @parser.try_parse("1px") 46 | parsed[:margin][:top].must_equal("1px") 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/csscss/parser/outline_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Outline 5 | describe self do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("1px solid blue").must_equal([ 15 | dec("outline-width", "1px"), 16 | dec("outline-style", "solid"), 17 | dec("outline-color", "blue") 18 | ]) 19 | 20 | trans("solid").must_equal([ 21 | dec("outline-style", "solid") 22 | ]) 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/csscss/parser/padding_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss::Parser 4 | module Padding 5 | describe Padding do 6 | include CommonParserTests 7 | 8 | before do 9 | @parser = Parser.new 10 | @trans = Transformer.new 11 | end 12 | 13 | it "converts shorthand rules to longhand" do 14 | trans("1px 10% inherit 4em").must_equal([ 15 | dec("padding-top", "1px"), 16 | dec("padding-right", "10%"), 17 | dec("padding-bottom", "inherit"), 18 | dec("padding-left", "4em") 19 | ]) 20 | 21 | trans("1px 10% inherit").must_equal([ 22 | dec("padding-top", "1px"), 23 | dec("padding-right", "10%"), 24 | dec("padding-bottom", "inherit"), 25 | dec("padding-left", "10%") 26 | ]) 27 | 28 | trans("1px 10%").must_equal([ 29 | dec("padding-top", "1px"), 30 | dec("padding-right", "10%"), 31 | dec("padding-bottom", "1px"), 32 | dec("padding-left", "10%") 33 | ]) 34 | 35 | trans("1px").must_equal([ 36 | dec("padding-top", "1px"), 37 | dec("padding-right", "1px"), 38 | dec("padding-bottom", "1px"), 39 | dec("padding-left", "1px") 40 | ]) 41 | end 42 | 43 | it "tries the parse and returns false if it doesn't work" do 44 | @parser.try_parse("foo").must_equal(false) 45 | parsed = @parser.try_parse("1px") 46 | parsed[:padding][:top].must_equal("1px") 47 | end 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /test/csscss/redundancy_analyzer_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss 4 | describe RedundancyAnalyzer do 5 | include TypeHelpers 6 | 7 | it "finds and trims redundant rule_sets" do 8 | css = %$ 9 | h1, h2 { display: none; position: relative; outline:none} 10 | .foo { display: none; width: 1px } 11 | .bar { position: relative; width: 1px; outline: none } 12 | .baz { display: none } 13 | $ 14 | 15 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 16 | [sel(".bar"), sel("h1, h2")] => [dec("outline", "none"), dec("position", "relative")], 17 | [sel(".bar"), sel(".foo")] => [dec("width", "1px")], 18 | [sel(".baz"), sel(".foo"), sel("h1, h2")] => [dec("display", "none")] 19 | }) 20 | 21 | RedundancyAnalyzer.new(css).redundancies.first.must_equal [ 22 | [sel(".bar"), sel("h1, h2")] , [dec("outline", "none"), dec("position", "relative")] 23 | ] 24 | 25 | RedundancyAnalyzer.new(css).redundancies(minimum:2).must_equal({ 26 | [sel(".bar"), sel("h1, h2")] => [dec("outline", "none"), dec("position", "relative")] 27 | }) 28 | end 29 | 30 | it "finds ignores case with rule_sets" do 31 | css = %$ 32 | .foo { WIDTH: 1px } 33 | .bar { width: 1px } 34 | $ 35 | 36 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 37 | [sel(".bar"), sel(".foo")] => [dec("width", "1px")] 38 | }) 39 | end 40 | 41 | it "doesn't return solo selectors" do 42 | css = %$ 43 | .foo { 44 | -webkit-border-radius: 4px; 45 | -moz-border-radius: 4px; 46 | } 47 | $ 48 | RedundancyAnalyzer.new(css).redundancies.must_equal({}) 49 | end 50 | 51 | it "correctly finds counts" do 52 | css = %$ 53 | .foo { 54 | -webkit-border-radius: 4px; 55 | -moz-border-radius: 4px; 56 | } 57 | 58 | .bar { 59 | background: white; 60 | 61 | -webkit-border-radius: 4px; 62 | -moz-border-radius: 4px; 63 | box-shadow: 1px 1px 10px #CCCCCC; 64 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 65 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 66 | } 67 | 68 | .baz { 69 | margin: 3px 3px 30px 3px; 70 | padding: 10px 30px; 71 | background: blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px; 72 | 73 | -webkit-border-radius: 4px; 74 | -moz-border-radius: 4px; 75 | box-shadow: 1px 1px 10px #CCCCCC; 76 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 77 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 78 | } 79 | 80 | .bar2 { 81 | background: white; 82 | 83 | -webkit-border-radius: 4px; 84 | -moz-border-radius: 4px; 85 | box-shadow: 1px 1px 10px #CCCCCC; 86 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 87 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 88 | } 89 | $ 90 | 91 | redundancies = RedundancyAnalyzer.new(css).redundancies(minimum:3) 92 | redundancies[[sel(".bar"), sel(".bar2"), sel(".baz")]].size.must_equal(5) 93 | end 94 | 95 | it "correctly finds counts with 3+ shared rules" do 96 | css = %$ 97 | .foo1 { 98 | background: white; 99 | 100 | -webkit-border-radius: 4px; 101 | -moz-border-radius: 4px; 102 | box-shadow: 1px 1px 10px #CCCCCC; 103 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 104 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 105 | } 106 | 107 | .foo2 { 108 | background: white; 109 | 110 | -webkit-border-radius: 4px; 111 | -moz-border-radius: 4px; 112 | box-shadow: 1px 1px 10px #CCCCCC; 113 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 114 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 115 | } 116 | 117 | .foo3 { 118 | background: white; 119 | 120 | -webkit-border-radius: 4px; 121 | -moz-border-radius: 4px; 122 | box-shadow: 1px 1px 10px #CCCCCC; 123 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 124 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 125 | } 126 | 127 | .foo4 { 128 | background: white; 129 | 130 | -webkit-border-radius: 4px; 131 | -moz-border-radius: 4px; 132 | box-shadow: 1px 1px 10px #CCCCCC; 133 | -moz-box-shadow: 1px 1px 10px #CCCCCC; 134 | -webkit-box-shadow: 1px 1px 10px #CCCCCC; 135 | } 136 | $ 137 | 138 | redundancies = RedundancyAnalyzer.new(css).redundancies 139 | redundancies.keys.size.must_equal 1 140 | redundancies[[sel(".foo1"), sel(".foo2"), sel(".foo3"), sel(".foo4")]].size.must_equal(6) 141 | end 142 | 143 | it "also matches shorthand rules" do 144 | css = %$ 145 | .foo { background-color: #fff } 146 | .bar { background: #fff top } 147 | $ 148 | 149 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 150 | [sel(".bar"), sel(".foo")] => [dec("background-color", "#fff")] 151 | }) 152 | end 153 | 154 | it "keeps full shorthand together" do 155 | css = %$ 156 | .baz { background-color: #fff } 157 | .foo { background: #fff top } 158 | .bar { background: #fff top } 159 | $ 160 | 161 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 162 | [sel(".bar"), sel(".foo")] => [dec("background", "#fff top")], 163 | [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("background-color", "#fff")] 164 | }) 165 | end 166 | 167 | it "doesn't consolidate explicit short/longhand" do 168 | css = %$ 169 | .foo { background-color: #fff } 170 | .bar { background: #fff } 171 | $ 172 | 173 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 174 | [sel(".bar"), sel(".foo")] => [dec("background-color", "#fff")] 175 | }) 176 | 177 | css = %$ 178 | .bar { background: #fff } 179 | .foo { background-color: #fff } 180 | $ 181 | 182 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 183 | [sel(".bar"), sel(".foo")] => [dec("background-color", "#fff")] 184 | }) 185 | end 186 | 187 | it "doesn't match shorthand when explicitly turned off" do 188 | css = %$ 189 | .foo { background-color: #fff } 190 | .bar { background: #fff } 191 | $ 192 | 193 | RedundancyAnalyzer.new(css).redundancies(match_shorthand:false).must_equal({}) 194 | end 195 | 196 | it "3-way case consolidation" do 197 | css = %$ 198 | .bar { background: #fff } 199 | .baz { background: #fff top } 200 | .foo { background-color: #fff } 201 | $ 202 | 203 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 204 | [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("background-color", "#fff")] 205 | }) 206 | end 207 | 208 | it "handles border and border-top matches appropriately" do 209 | css = %$ 210 | .bar { border: 1px solid #fff } 211 | .baz { border-top: 1px solid #fff } 212 | .foo { border-top-width: 1px } 213 | $ 214 | 215 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 216 | [sel(".bar"), sel(".baz")] => [dec("border-top", "1px solid #fff")], 217 | [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("border-top-width", "1px")] 218 | }) 219 | end 220 | 221 | it "reduces border matches appropriately" do 222 | css = %$ 223 | .bar { border: 1px solid #FFF } 224 | .baz { border: 1px solid #FFF } 225 | $ 226 | 227 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 228 | [sel(".bar"), sel(".baz")] => [dec("border", "1px solid #fff")] 229 | }) 230 | 231 | css = %$ 232 | .bar { border: 4px solid #4F4F4F } 233 | .baz { border: 4px solid #4F4F4F } 234 | .foo { border: 4px solid #3A86CE } 235 | $ 236 | 237 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 238 | [sel(".bar"), sel(".baz")] => [dec("border", "4px solid #4f4f4f")], 239 | [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("border-style", "solid"), dec("border-width", "4px")] 240 | }) 241 | 242 | RedundancyAnalyzer.new(css).redundancies(minimum:2).must_equal({ 243 | [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("border-style", "solid"), dec("border-width", "4px")] 244 | }) 245 | end 246 | 247 | it "reduces padding and margin" do 248 | css = %$ 249 | .bar { padding: 4px; margin: 5px } 250 | .baz { padding-bottom: 4px} 251 | .foo { margin-right: 5px } 252 | $ 253 | 254 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 255 | [sel(".bar"), sel(".baz")] => [dec("padding-bottom", "4px")], 256 | [sel(".bar"), sel(".foo")] => [dec("margin-right", "5px")] 257 | }) 258 | end 259 | 260 | it "ignores specific properties" do 261 | css = %$ 262 | h1, h2 { display: none; position: relative; outline:none} 263 | .foo { DISPLAY: none; width: 1px } 264 | .bar { position: relative; width: 1px; outline: none } 265 | .baz { display: none } 266 | $ 267 | 268 | RedundancyAnalyzer.new(css).redundancies(ignored_properties:%w(display outline)).must_equal({ 269 | [sel(".bar"), sel("h1, h2")] => [dec("position", "relative")], 270 | [sel(".bar"), sel(".foo")] => [dec("width", "1px")], 271 | }) 272 | 273 | RedundancyAnalyzer.new(css).redundancies(ignored_properties:%w(display outline), ignored_selectors:%w(.foo)).must_equal({ 274 | [sel(".bar"), sel("h1, h2")] => [dec("position", "relative")] 275 | }) 276 | end 277 | 278 | it "matches 0 and 0px" do 279 | css = %$ 280 | .bar { padding: 0; } 281 | .foo { padding: 0px; } 282 | $ 283 | 284 | RedundancyAnalyzer.new(css).redundancies.must_equal({ 285 | [sel(".bar"), sel(".foo")] => [dec("padding", "0")] 286 | }) 287 | end 288 | 289 | 290 | # TODO: someday 291 | # it "reports duplication within the same selector" do 292 | # css = %$ 293 | # .bar { background: #fff top; background-color: #fff } 294 | # $ 295 | 296 | # # TODO: need to update the reporter for this 297 | # RedundancyAnalyzer.new(css).redundancies.must_equal({ 298 | # [sel(".bar")] => [dec("background-color", "#fff")] 299 | # }) 300 | # end 301 | end 302 | end 303 | -------------------------------------------------------------------------------- /test/csscss/reporter_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss 4 | describe Reporter do 5 | include TypeHelpers 6 | 7 | it "formats string result" do 8 | reporter = Reporter.new({ 9 | [sel(".foo"), sel(".bar")] => [dec("width", "1px"), dec("border", "black")], 10 | [sel("h1, h2"), sel(".foo"), sel(".baz")] => [dec("display", "none")], 11 | [sel("h1, h2"), sel(".bar")] => [dec("position", "relative")] 12 | }) 13 | 14 | expected =<<-EXPECTED 15 | {.foo} AND {.bar} share 2 declarations 16 | {h1, h2}, {.foo} AND {.baz} share 1 declaration 17 | {h1, h2} AND {.bar} share 1 declaration 18 | EXPECTED 19 | reporter.report(color:false).must_equal expected 20 | 21 | expected =<<-EXPECTED 22 | {.foo} AND {.bar} share 2 declarations 23 | - width: 1px 24 | - border: black 25 | {h1, h2}, {.foo} AND {.baz} share 1 declaration 26 | - display: none 27 | {h1, h2} AND {.bar} share 1 declaration 28 | - position: relative 29 | EXPECTED 30 | reporter.report(verbose:true, color:false).must_equal expected 31 | end 32 | 33 | it "prints a new line if there is nothing" do 34 | reporter = Reporter.new({}) 35 | reporter.report().must_equal "" 36 | end 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/csscss/sass_include_extensions_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | require "tempfile" 3 | require "csscss/sass_include_extensions" 4 | 5 | module Csscss 6 | describe "sass import extensions" do 7 | it "should add comments before and after mixin properties" do 8 | scss =<<-SCSS 9 | @mixin foo { 10 | font: { 11 | family: serif; 12 | size: 10px; 13 | } 14 | 15 | display: block; 16 | } 17 | 18 | @mixin bar { 19 | outline: 1px; 20 | } 21 | 22 | h1 { 23 | @include foo; 24 | @include bar; 25 | } 26 | SCSS 27 | 28 | 29 | css =<<-CSS 30 | h1 { 31 | /* CSSCSS START MIXIN: foo */ 32 | font-family: serif; 33 | font-size: 10px; 34 | display: block; 35 | /* CSSCSS END MIXIN: foo */ 36 | /* CSSCSS START MIXIN: bar */ 37 | outline: 1px; 38 | /* CSSCSS END MIXIN: bar */ } 39 | CSS 40 | 41 | Sass::Engine.new(scss, syntax: :scss, cache: false).render.must_equal(css) 42 | end 43 | 44 | it "should insert comments even with imported stylesheets" do 45 | Tempfile.open(['foo', '.scss']) do |f| 46 | f << <<-SCSS 47 | @mixin foo { 48 | outline: 1px; 49 | } 50 | 51 | h1 { 52 | @include foo; 53 | } 54 | SCSS 55 | f.close 56 | 57 | css =<<-CSS 58 | h1 { 59 | /* CSSCSS START MIXIN: foo */ 60 | outline: 1px; 61 | /* CSSCSS END MIXIN: foo */ } 62 | CSS 63 | 64 | Sass::Engine.new("@import '#{File.basename(f.path)}'", syntax: :scss, cache: false, load_paths: ["/tmp"]).render.must_equal(css) 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /test/csscss/types_test.rb: -------------------------------------------------------------------------------- 1 | require "test_helper" 2 | 3 | module Csscss 4 | describe Declaration do 5 | it "< and > checks parents" do 6 | dec1 = Declaration.new("background", "#fff") 7 | dec2 = Declaration.new("background", "#fff") 8 | dec3 = Declaration.new("background-color", "#fff", [dec1]) 9 | dec4 = Declaration.new("background-color", "#fff", nil) 10 | 11 | dec1.must_be :>, dec3 12 | dec3.must_be :<, dec1 13 | 14 | dec1.wont_be :>, dec4 15 | dec1.wont_be :>, dec2 16 | dec4.wont_be :>, dec1 17 | end 18 | 19 | it "checks ancestory against all parents" do 20 | dec1 = Declaration.new("border", "#fff") 21 | dec2 = Declaration.new("border", "#fff top") 22 | dec3 = Declaration.new("border-top", "#fff", [dec1, dec2]) 23 | 24 | dec1.must_be :>, dec3 25 | dec2.must_be :>, dec3 26 | 27 | dec1.wont_be :>, dec1 28 | dec2.wont_be :>, dec1 29 | dec1.wont_be :>, dec2 30 | dec3.wont_be :>, dec1 31 | end 32 | 33 | it "is a derivative if it has parents" do 34 | dec1 = Declaration.new("background", "#fff") 35 | dec1.wont_be :derivative? 36 | Declaration.new("background-color", "#fff", [dec1]).must_be :derivative? 37 | end 38 | 39 | it "ignores parents when checking equality" do 40 | dec1 = Declaration.new("background", "#fff") 41 | dec2 = Declaration.new("background-color", "#fff", [dec1]) 42 | dec3 = Declaration.new("background-color", "#fff", nil) 43 | 44 | dec1.wont_equal dec2 45 | dec2.wont_equal dec1 46 | 47 | dec2.must_equal dec3 48 | dec3.must_equal dec2 49 | 50 | dec2.hash.must_equal dec3.hash 51 | dec3.hash.must_equal dec2.hash 52 | dec3.hash.wont_equal dec1.hash 53 | 54 | dec2.must_be :eql?, dec3 55 | dec3.must_be :eql?, dec2 56 | dec2.wont_be :eql?, dec1 57 | end 58 | 59 | it "derivatives are handled correctly in a hash" do 60 | dec1 = Declaration.new("background", "#fff") 61 | dec2 = Declaration.new("background-color", "#fff", [dec1]) 62 | dec3 = Declaration.new("background-color", "#fff", nil) 63 | 64 | h = {} 65 | h[dec2] = false 66 | h[dec3] = true 67 | 68 | h.keys.size.must_equal 1 69 | h[dec2].must_equal true 70 | h[dec3].must_equal true 71 | end 72 | 73 | it "equates 0 length with and without units" do 74 | Declaration.new("padding", "0px").must_equal Declaration.new("padding", "0") 75 | Declaration.new("padding", "0%").must_equal Declaration.new("padding", "0") 76 | Declaration.new("padding", "0").must_equal Declaration.new("padding", "0em") 77 | 78 | Declaration.new("padding", "1").wont_equal Declaration.new("padding", "1px") 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /test/just_parse.rb: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env ruby 2 | 3 | require "byebug" 4 | require "csscss" 5 | 6 | raise "need a file name" unless ARGV[0] 7 | contents = File.read(ARGV[0]) 8 | rule_sets = Csscss::Parser::Css.parse(contents) 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'minitest/autorun' 5 | require 'minitest/rg' 6 | require 'byebug' 7 | 8 | require 'csscss' 9 | 10 | module TypeHelpers 11 | def sel(s) 12 | Csscss::Selector.new(s) 13 | end 14 | 15 | def dec(p, v) 16 | Csscss::Declaration.new(p, v) 17 | end 18 | 19 | def rs(selectors, decs) 20 | Csscss::Ruleset.new(selectors, decs) 21 | end 22 | end 23 | 24 | module MiniTest::Assertions 25 | def assert_parse(parser, string) 26 | assert parser.parse(string) 27 | rescue Parslet::ParseFailed => ex 28 | assert false, ex.cause.ascii_tree 29 | end 30 | 31 | def assert_not_parse(parser, string) 32 | parser.parse(string) 33 | assert false, "expected #{parser} to not successfully parse \"#{string}\" and it did" 34 | rescue Parslet::ParseFailed => ex 35 | assert ex 36 | end 37 | end 38 | 39 | Parslet::Atoms::DSL.infect_an_assertion :assert_parse, :must_parse, :do_not_flip 40 | Parslet::Atoms::DSL.infect_an_assertion :assert_not_parse, :wont_parse, :do_not_flip 41 | 42 | module CommonParserTests 43 | def self.included(base) 44 | base.instance_eval do 45 | include Helpers 46 | include TypeHelpers 47 | 48 | describe "common parser tests" do 49 | it "parses inherit" do 50 | trans("inherit").must_equal([]) 51 | end 52 | 53 | it "doesn't parse unknown values" do 54 | @parser.wont_parse("foo") 55 | end 56 | end 57 | end 58 | end 59 | 60 | module Helpers 61 | def trans(s) 62 | @trans.apply(@parser.parse(s)) 63 | rescue Parslet::ParseFailed => e 64 | puts e.cause.ascii_tree 65 | raise e 66 | end 67 | end 68 | end 69 | --------------------------------------------------------------------------------