diff --git a/.gitignore b/.gitignore index af6864a..fd69015 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ spec/reports test/tmp test/version_tmp tmp +_site +.vscode diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..73462a5 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.5.1 diff --git a/.travis.yml b/.travis.yml index 222078e..9323019 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,5 @@ language: ruby rvm: - - 1.9.3 - - 2.0.0 + - 2.5 + - 2.4 + - 2.3 diff --git a/CHANGELOG.md b/CHANGELOG.md index 90aca2e..024f5ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,55 @@ +## 1.3.3 - 5/30/2014 ## + +* Upgrades parslet dependency to v1.6.1 and drops optimization monkeypatch +* Use correct terminology "declartions" instead of "rules" in the output + +## 1.3.2 - 6/22/2013 ## + +* Fixes attribute parsing bug that includes comments with braces +* Fixes parsing bug with empty media selectors #67 +* Fixes parsing bug with quoted brackets #72 +* Fixes parsing bug with nested media queries #73 +* Removes --compass-with-config deprecation +* Adds a CONTRIBUTING.md file with instructions + +## 1.3.1 - 4/20/2013 ## + +* Fixes --ignore-sass-mixins bug with @importing + +## 1.3.0 - 4/20/2013 ## + +* Adds --require switch for user configuration +* Deprecates --compass-with-config config.rb in favor of --compass --require config.rb +* Ignores @import statements. Users will need to run csscss on those directly +* Adds --ignore-sass-mixins which won't match declarations coming from sass mixins + +## 1.2.0 - 4/14/2013 ## + +* 0 and 0px are now reconciled as redundancies +* Disables color support by default for windows & ruby < 2.0 +* Fixes bug where unquoted url(data...) isn't parsed correctly +* Adds support for LESS files + +## 1.1.0 - 4/12/2013 ## + +* Fixes bug where CLI --no-color wasn't respected +* Added ruby version requirement for >= 1.9 +* Added CONTRIBUTORS.md +* Fixes bugs with urls that have dashes in them +* Fixes bugs with urls containing encoded data (usually images) +* Deprecates CSSCSS_DEBUG in favor of --show-parser-errors +* Fixes line/column output during parser errors +* --compass now grabs config.rb by default if it exists +* Adds --compass-with-config that lets users specify a config +* Fixes parser error bug when trying to parse blank files +* Fixes bug where rules from multiple files aren't consolidated +* Adds --no-match-shorthand to allow users to opt out of shorthand matching + +## 1.0.0 - 4/7/2013 ## + +* Allows the user to specify ignored properties and selectors +* Better parse error messages + ## 0.2.1 - 3/28/2013 ## * Changes coloring to the selectors and declarations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..fc1d02f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,53 @@ +# Contributing to csscss + +First of all: Thanks! + +## Bugs & Feature Requests + +You can file bugs on the [issues +tracker](https://github.com/zmoazeni/csscss/issues), and tag them with +'bug'. Feel free to discuss features there, too. + +## Good report structure + +Please include the following four things in your report: + +1. The smallest CSS snippet to explain the problem. +2. What you did. +3. What you expected to happen. +4. What happened instead. + +The more information the better. + +## Contributing Code + +It's easy to contribute code to csscss: + +1. Fork csscss. +2. Create a topic branch - `git checkout -b my_branch` +3. Push to your branch - `git push origin my_branch` +4. Make sure the code follows the contributing guidelines below. +5. Create a [Pull Request](http://help.github.com/pull-requests/) from your + branch. +6. That's it! + +## First Time OSS Contributors + +Submitting your first pull request can be a little daunting. If this is +your first Open Source contribution, please mention it in your pull +request and I'll help guide you through the process. + +## Contributing Guidelines + +* Make sure your code follows typical [ruby +conventions](https://github.com/bbatsov/ruby-style-guide). +* Make sure the test suite is green. A simple `bundle exec rake test` +will run all the tests. +* Include a CHANGELOG entry with your change. Add an `(Unreleased)` +section at the top if one doesn't exist. +* Try to keep the git commits squashed and concise. Keep tests and code +changes together in the same commit. Keep only logical changes together +in a single commit. +* I strongly encourage [well written git commit +messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). +* Make sure all whitespace is trimmed from the end of lines. diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..30a5a63 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,8 @@ +* Zach Moazeni @zmoazeni +* Carson McDonald @carsonmcdonald +* Martin Kuckert @MKuckert +* Ivan Lazarevic @kopipejst +* Matt DuVall @mduvall twitter:@mduvall_ +* Mekka Okereke @mekka @mekkaokereke +* JoseLuis Torres @joseluistorres twitter:@joseluis_torres +* Paul Simpson @prsimp diff --git a/Gemfile b/Gemfile index fa46110..f34d154 100644 --- a/Gemfile +++ b/Gemfile @@ -4,14 +4,16 @@ source 'https://rubygems.org' gemspec # optional runtime dependencies -gem "sass" -gem "compass" +gem 'sass' +gem 'compass' +gem 'less' +gem 'therubyracer', :platform => :mri -gem "rake", :require => false -gem "debugger" +gem 'rake', :require => false +gem 'byebug' -gem "minitest" -gem "m" -gem "minitest-rg" +gem 'minitest' +gem 'm' +gem 'minitest-rg' -gem "ruby-prof" +gem 'ruby-prof' diff --git a/Gemfile.lock b/Gemfile.lock index 2795a01..33ae5c4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,50 +1,64 @@ PATH remote: . specs: - csscss (0.2.1) + csscss (1.3.3) colorize - parslet (~> 1.5) + parslet (>= 1.6.1, < 2.0) GEM remote: https://rubygems.org/ specs: - blankslate (2.1.2.4) - chunky_png (1.2.7) - colorize (0.5.8) - columnize (0.3.6) + byebug (10.0.2) + chunky_png (1.3.11) + colorize (0.8.1) + commonjs (0.2.7) compass (0.12.2) chunky_png (~> 1.2) fssm (>= 0.2.7) sass (~> 3.1) - debugger (1.5.0) - columnize (>= 0.3.1) - debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.2.0) - debugger-linecache (1.2.0) - debugger-ruby_core_source (1.2.0) + ffi (1.9.25) fssm (0.2.10) - m (1.3.1) + less (2.6.0) + commonjs (~> 0.2.7) + libv8 (3.16.14.19) + m (1.5.1) method_source (>= 0.6.7) rake (>= 0.9.2.2) - method_source (0.8.1) - minitest (2.12.1) - minitest-rg (1.1.0) - parslet (1.5.0) - blankslate (~> 2.0) - rake (10.0.3) - ruby-prof (0.13.0) - sass (3.2.7) + method_source (0.9.2) + minitest (5.11.3) + minitest-rg (5.2.0) + minitest (~> 5.0) + parslet (1.8.2) + rake (12.3.1) + rb-fsevent (0.10.3) + rb-inotify (0.9.10) + ffi (>= 0.5.0, < 2) + ref (2.0.0) + ruby-prof (0.17.0) + sass (3.7.2) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + therubyracer (0.12.3) + libv8 (~> 3.16.14.15) + ref PLATFORMS ruby DEPENDENCIES + byebug compass csscss! - debugger + less m minitest minitest-rg rake ruby-prof sass + therubyracer + +BUNDLED WITH + 1.16.4 diff --git a/README.md b/README.md index 372db46..ea7df7d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ [![Build Status](https://travis-ci.org/zmoazeni/csscss.png?branch=master)](https://travis-ci.org/zmoazeni/csscss) +[![Code Climate](https://codeclimate.com/github/zmoazeni/csscss.png)](https://codeclimate.com/github/zmoazeni/csscss) ## What is it? ## @@ -20,13 +21,15 @@ First you need to install it. It is currently packaged as a ruby gem: $ gem install csscss +Note: csscss only works on ruby 1.9.x and up. It will have trouble with ruby 1.8.x. + Then you can run it in at the command line against CSS files. $ csscss path/to/styles.css path/to/other-styles.css {.contact .content .primary} and {article, #comments} share 5 rules {.profile-picture}, {.screenshot img} and {a.blurb img} share 4 rules - {.work h2:first-child, .archive h2:first-child, .missing h2:first-child, .about h2, .contact h2} and {body.home h2} share 4 rules + {.work h2:first-child, .contact h2} and {body.home h2} share 4 rules {article.blurb:hover} and {article:hover} share 3 rules Run it in a verbose mode to see all the duplicated styles. @@ -42,11 +45,38 @@ rulesets that have fewer matches. $ csscss -n 10 -v path/to/style.css # ignores rulesets with < 10 matches -If you prefer writing in sass, you can also parse your sass/scss files. +If you prefer writing in [Sass](http://sass-lang.com/), you can also parse your sass/scss files. $ gem install sass $ csscss path/to/style.scss +Sass users may be interested in the `--ignore-sass-mixins` +experimental flag that won't match duplicate declarations from including mixins. + +If you prefer writing in [LESS](http://lesscss.org/), you can also parse your LESS files. + + $ gem install less + $ csscss path/to/style.less + +LESS requires an additional javascript runtime. +[v8/therubyracer](https://rubygems.org/gems/therubyracer) on most +rubies, and [therubyrhino](https://rubygems.org/gems/therubyrhino) on +jruby. + +## Are there any community extensions? ## + +* [compass-csscss](https://github.com/Comcast/compass-csscss) integrates csscss with [compass](http://compass-style.org/) projects. +* [grunt-csscss](https://github.com/peterkeating/grunt-csscss) a [grunt](http://gruntjs.com/) task to automatically run csscss. +* [gulp-csscss](https://www.npmjs.org/package/gulp-csscss/) a [gulp](http://gulpjs.com/) task to automatically run csscss. + +_Please submit [an issue](https://github.com/zmoazeni/csscss/issues/new) if you know of any others._ + +## Why doesn't csscss automatically remove duplications for me? ## + +I have been asked this a lot, but csscss is intentionally designed this +way. Check out [this +post](https://connectionrequired.com/blog/2013/04/why-csscss-doesnt-remove-duplication-for-you) +for my reasoning. ## I found bugs ## @@ -57,12 +87,6 @@ If you find any issues, please report them on Please include the smallest CSS snippet to describe the issue and the output you expect to see. -## Who are you? ## - -My name is [Zach Moazeni](https://twitter.com/zmoazeni). I work for [an -awesome company](http://www.getharvest.com/). And [we're -hiring!](http://www.getharvest.com/careers) - ## I'm a dev, I can help ## Awesome! Thanks! Here are the steps I ask: diff --git a/csscss.gemspec b/csscss.gemspec index c085f2e..85304e8 100644 --- a/csscss.gemspec +++ b/csscss.gemspec @@ -10,13 +10,15 @@ Gem::Specification.new do |gem| gem.email = ["zach.moazeni@gmail.com"] gem.summary = %q{A CSS redundancy analyzer that analyzes redundancy.} gem.description = %q{csscss will parse any CSS files you give it and let you know which rulesets have duplicated declarations.} - gem.homepage = "http://zmoazeni.github.com/csscss/" + gem.homepage = "http://zmoazeni.github.io/csscss/" gem.files = `git ls-files`.split($/) gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) gem.require_paths = ["lib"] - gem.add_dependency "parslet", "~> 1.5" + gem.required_ruby_version = ">= 1.9" + + gem.add_dependency "parslet", ">= 1.6.1", "< 2.0" gem.add_dependency "colorize" end diff --git a/docker-ruby b/docker-ruby new file mode 100755 index 0000000..33ea97f --- /dev/null +++ b/docker-ruby @@ -0,0 +1,3 @@ +#!/bin/bash +VERSION=${VERSION:-latest} +docker run -it --rm -v $(pwd):/app -v csscss_gem_data:/usr/local/bundle -w /app ruby:$VERSION $@ diff --git a/lib/csscss.rb b/lib/csscss.rb index 29076ff..62f3d42 100644 --- a/lib/csscss.rb +++ b/lib/csscss.rb @@ -5,7 +5,6 @@ require "colorize" require "parslet" -require "csscss/parslet_optimizations" require "csscss/version" require "csscss/cli" diff --git a/lib/csscss/cli.rb b/lib/csscss/cli.rb index aed4007..2b8430d 100644 --- a/lib/csscss/cli.rb +++ b/lib/csscss/cli.rb @@ -1,11 +1,15 @@ module Csscss class CLI def initialize(argv) - @argv = argv - @verbose = false - @color = true - @minimum = 3 - @compass = false + @argv = argv + @verbose = false + @color = !windows_1_9 + @minimum = 3 + @compass = false + @ignored_properties = [] + @ignored_selectors = [] + @match_shorthand = true + @ignore_sass_mixins = false end def run @@ -13,61 +17,54 @@ def run execute end + private def execute + deprecate("Use --show-parser-errors instead of CSSCSS_DEBUG") if ENV["CSSCSS_DEBUG"] - all_redundancies = @argv.map do |filename| - contents = if %w(.scss .sass).include?(File.extname(filename).downcase) && !(filename =~ URI.regexp) - begin - require "sass" - rescue LoadError - puts "Must install sass gem before parsing sass/scss files" - exit 1 - end - - sass_options = {cache:false} - sass_options[:load_paths] = Compass.configuration.sass_load_paths if @compass - begin - Sass::Engine.for_file(filename, sass_options).render - rescue Sass::SyntaxError => e - if e.message =~ /compass/ && !@compass - puts "Enable --compass option to use compass's extensions" - exit 1 - else - raise e - end - end + all_contents= @argv.map do |filename| + if filename =~ URI.regexp + load_css_file(filename) else - open(filename) {|f| f.read } + case File.extname(filename).downcase + when ".scss", ".sass" + load_sass_file(filename) + when ".less" + load_less_file(filename) + else + load_css_file(filename) + end end - - RedundancyAnalyzer.new(contents).redundancies(@minimum) - end - - combined_redundancies = all_redundancies.inject({}) do |combined, redundancies| - if combined.empty? - redundancies + end.join("\n") + + unless all_contents.strip.empty? + redundancies = RedundancyAnalyzer.new(all_contents).redundancies( + minimum: @minimum, + ignored_properties: @ignored_properties, + ignored_selectors: @ignored_selectors, + match_shorthand: @match_shorthand + ) + + if @json + puts JSONReporter.new(redundancies).report else - combined.merge(redundancies) do |_, v1, v2| - (v1 + v2).uniq - end + report = Reporter.new(redundancies).report(verbose:@verbose, color:@color) + puts report unless report.empty? end end - if @json - puts JSONReporter.new(combined_redundancies).report + rescue Parslet::ParseFailed => e + line, column = e.cause.source.line_and_column(e.cause.pos) + puts "Had a problem parsing the css at line: #{line}, column: #{column}".red + if @show_parser_errors || ENV['CSSCSS_DEBUG'] == 'true' + puts e.cause.ascii_tree.red else - report = Reporter.new(combined_redundancies).report(verbose:@verbose, color:true) - puts report unless report.empty? + puts "Run with --show-parser-errors for verbose parser errors".red end - - rescue Parslet::ParseFailed => e - puts "Had a problem parsing the css" - puts e.cause.ascii_tree exit 1 end def parse(argv) - opts = OptionParser.new do |opts| + options = OptionParser.new do |opts| opts.banner = "Usage: csscss [files..]" opts.version = Csscss::VERSION @@ -75,51 +72,129 @@ def parse(argv) @verbose = v end - opts.on("--[no-]color", "Colorizes output") do |c| + opts.on("--[no-]color", "Colorize output", "(default is #{@color})") do |c| @color = c end - opts.on("-n", "--num N", Integer, "Print matches with at least this many rules. Defaults to 3") do |n| + opts.on("-n", "--num N", Integer, "Print matches with at least this many rules.", "(default is 3)") do |n| @minimum = n end - opts.on("-V", "--version", "Show version") do |v| - puts opts.ver - exit - end - - opts.on("--[no-]compass", "Enables compass extensions when parsing sass/scss") do |compass| - if @compass = compass - begin - require "compass" - rescue LoadError - puts "Must install compass gem before enabling its extensions" - exit 1 - end - end + opts.on("--[no-]match-shorthand", "Expand shorthand rules and matches on explicit rules", "(default is true)") do |match_shorthand| + @match_shorthand = match_shorthand end opts.on("-j", "--[no-]json", "Output results in JSON") do |j| @json = j end + opts.on("--ignore-sass-mixins", "EXPERIMENTAL: Ignore matches that come from including sass/scss mixins", + "This is an experimental feature and may not be included in future releases", + "(default is false)") do |ignore| + @ignore_sass_mixins = ignore + end + + opts.on("--[no-]compass", "Enable compass extensions when parsing sass/scss (default is false)") do |compass| + enable_compass if @compass = compass + end + + opts.on("--compass-with-config config", "Enable compass extensions when parsing sass/scss and pass config file") do |config| + @compass = true + enable_compass(config) + end + + opts.on("--require file.rb", "Load ruby file before running csscss.", "Great for bootstrapping requires/configurations") do |file| + load file + end + + opts.on("--ignore-properties property1,property2,...", Array, "Ignore these properties when finding matches") do |ignored_properties| + @ignored_properties = ignored_properties + end + + opts.on('--ignore-selectors "selector1","selector2",...', Array, "Ignore these selectors when finding matches") do |ignored_selectors| + @ignored_selectors = ignored_selectors + end + + opts.on("--show-parser-errors", "Print verbose parser errors") do |show_parser_errors| + @show_parser_errors = show_parser_errors + end + + opts.on("-V", "--version", "Show version") do |v| + puts opts.ver + exit + end + opts.on_tail("-h", "--help", "Show this message") do print_help(opts) end end - opts.parse!(argv) + options.parse!(argv) - print_help(opts) if argv.empty? + print_help(options) if argv.empty? rescue OptionParser::ParseError - print_help(opts) + print_help(options) end - private def print_help(opts) puts opts exit end + def deprecate(message) + $stderr.puts("DEPRECATED: #{message}".yellow) + end + + def enable_compass(config = nil) + abort 'Must install the "compass" gem before enabling its extensions' unless gem_installed?("compass") + + if config + Compass.add_configuration(config) + else + Compass.add_configuration("config.rb") if File.exist?("config.rb") + end + end + + def windows_1_9 + RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ && RUBY_VERSION =~ /^1\.9/ + end + + def gem_installed?(gem_name) + begin + require gem_name + true + rescue LoadError + false + end + end + + def load_sass_file(filename) + abort 'Must install the "sass" gem before parsing sass/scss files' unless gem_installed?("sass") + require "csscss/sass_include_extensions" if @ignore_sass_mixins + + sass_options = {cache:false} + sass_options[:load_paths] = Compass.configuration.sass_load_paths if @compass + begin + Sass::Engine.for_file(filename, sass_options).render + rescue Sass::SyntaxError => e + if e.message =~ /compass/ && !@compass + puts "Enable --compass option to use compass's extensions" + exit 1 + else + raise e + end + end + end + + def load_less_file(filename) + abort 'Must install the "less" gem before parsing less files' unless gem_installed?("less") + contents = load_css_file(filename) + Less::Parser.new.parse(contents).to_css + end + + def load_css_file(filename) + open(filename) {|f| f.read } + end + class << self def run(argv) new(argv).run diff --git a/lib/csscss/parser/common.rb b/lib/csscss/parser/common.rb index 18116c4..84abb82 100644 --- a/lib/csscss/parser/common.rb +++ b/lib/csscss/parser/common.rb @@ -5,26 +5,33 @@ module Common UNITS = %w(px em ex in cm mm pt pc) - rule(:space) { match['\s'].repeat(1) } - rule(:space?) { space.maybe } - rule(:number) { match["0-9"] } - rule(:numbers) { number.repeat(1) } - rule(:decimal) { numbers >> str(".").maybe >> numbers.maybe } - rule(:percent) { decimal >> stri("%") >> space? } - rule(:length) { decimal >> stri_list(UNITS) >> space? } - rule(:identifier) { match["a-zA-Z"].repeat(1) } - rule(:inherit) { stri("inherit") } - rule(:eof) { any.absent? } - rule(:nada) { any.repeat.as(:nada) } + rule(:space) { match['\s'].repeat(1) } + rule(:space?) { space.maybe } + rule(:number) { match["0-9"] } + rule(:numbers) { number.repeat(1) } + rule(:decimal) { numbers >> str(".").maybe >> numbers.maybe } + rule(:percent) { decimal >> stri("%") >> space? } + rule(:non_zero_length) { decimal >> stri_list(UNITS) >> space? } + rule(:zero_length) { match["0"] } + rule(:length) { zero_length | non_zero_length } + rule(:identifier) { match["a-zA-Z"].repeat(1) } + rule(:inherit) { stri("inherit") } + rule(:eof) { any.absent? } + rule(:nada) { any.repeat.as(:nada) } rule(:http) { - (match['a-zA-Z.:/'] | str('\(') | str('\)')).repeat >> space? + (match['a-zA-Z0-9.:/\-'] | str('\(') | str('\)')).repeat >> space? + } + + rule(:data) { + stri("data:") >> match['a-zA-Z0-9.:/+;,=\-'].repeat >> space? } rule(:url) { stri("url") >> parens do (any_quoted { http } >> space?) | - http + (any_quoted { data } >> space?) | + data | http end } diff --git a/lib/csscss/parser/css.rb b/lib/csscss/parser/css.rb index a9025bc..d0c6973 100644 --- a/lib/csscss/parser/css.rb +++ b/lib/csscss/parser/css.rb @@ -12,30 +12,50 @@ def parse(source) class Parser < Parslet::Parser include Common - rule(:comment) { - (space? >> str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') >> space?).as(:comment) - } - - rule(:css_space?) { - comment.repeat(1) | space? + rule(:raw_comment) { + space? >> str('/*') >> (str('*/').absent? >> any).repeat >> str('*/') >> space? } + rule(:comment) { raw_comment.as(:comment) } rule(:blank_attribute) { str(";") >> space? } + rule(:attribute_value) { any_quoted { any } | (str('/*').absent? >> match["^;}"]) | raw_comment } + rule(:attribute) { match["^:{}"].repeat(1).as(:property) >> str(":") >> - match["^;}"].repeat(1).as(:value) >> + ( + (stri("data:").absent? >> attribute_value) | + (stri("data:").present? >> attribute_value.repeat(1) >> str(";") >> attribute_value.repeat(1)) + ).repeat(1).as(:value) >> str(";").maybe >> space? } + rule(:mixin_attributes) { + ( + str('/* CSSCSS START MIXIN') >> + (str('*/').absent? >> any).repeat >> + str('*/') >> + (str('/* CSSCSS END MIXIN').absent? >> any).repeat >> + str('/* CSSCSS END MIXIN') >> + (str('*/').absent? >> any).repeat >> + str('*/') >> + space? + ).as(:mixin) + } + rule(:ruleset) { ( match["^{}"].repeat(1).as(:selector) >> str("{") >> space? >> - (comment | attribute | blank_attribute).repeat(0).as(:properties) >> + ( + mixin_attributes | + comment | + attribute | + blank_attribute + ).repeat(0).as(:properties) >> str("}") >> space? ).as(:ruleset) @@ -46,24 +66,42 @@ class Parser < Parslet::Parser str("@") >> match["^{}"].repeat(1) >> str("{") >> - (comment | ruleset).repeat(0) >> + space? >> + (comment | ruleset | nested_ruleset).repeat(1) >> str("}") >> space? ).as(:nested_ruleset) } + rule(:import) { + ( + stri("@import") >> + match["^;"].repeat(1) >> + str(";") >> + space? + ).as(:import) + } + rule(:blocks) { - space? >> (comment | nested_ruleset | ruleset).repeat(1).as(:blocks) >> space? + space? >> ( + comment | + import | + nested_ruleset | + ruleset + ).repeat(1).as(:blocks) >> space? } root(:blocks) end class Transformer < Parslet::Transform - rule(nested_ruleset: sequence(:rulesets)) { - rulesets + rule(nested_ruleset: subtree(:rulesets)) { |context| + context[:rulesets].flatten } + rule(import: simple(:import)) { [] } + rule(mixin: simple(:mixin)) { nil } + rule(comment: simple(:comment)) { nil } rule(ruleset: { diff --git a/lib/csscss/parslet_optimizations.rb b/lib/csscss/parslet_optimizations.rb deleted file mode 100644 index 32b7130..0000000 --- a/lib/csscss/parslet_optimizations.rb +++ /dev/null @@ -1,77 +0,0 @@ -# These are my multibyte optimizations for parslet. -# More information can be found: -# https://github.com/kschiess/parslet/issues/73 -# https://github.com/kschiess/parslet/pull/74 -# https://github.com/zmoazeni/parslet/tree/optimized-multibyte-parsing - -require 'strscan' -require 'forwardable' - -module Parslet - class Source - extend Forwardable - - def initialize(str) - raise ArgumentError unless str.respond_to?(:to_str) - - @str = StringScanner.new(str) - - @line_cache = LineCache.new - @line_cache.scan_for_line_endings(0, str) - end - - def matches?(pattern) - regexp = pattern.is_a?(String) ? Regexp.new(Regexp.escape(pattern)) : pattern - !@str.match?(regexp).nil? - end - alias match matches? - - def consume(n) - original_pos = @str.pos - slice_str = n.times.map { @str.getch }.join - slice = Parslet::Slice.new( - slice_str, - original_pos, - @line_cache) - - return slice - end - - def chars_left - @str.rest_size - end - - def_delegator :@str, :pos - def pos=(n) - if n > @str.string.bytesize - @str.pos = @str.string.bytesize - else - @str.pos = n - end - end - - - class LineCache - def scan_for_line_endings(start_pos, buf) - return unless buf - - buf = StringScanner.new(buf) - return unless buf.exist?(/\n/) - - ## If we have already read part or all of buf, we already know about - ## line ends in that portion. remove it and correct cur (search index) - if @last_line_end && start_pos < @last_line_end - # Let's not search the range from start_pos to last_line_end again. - buf.pos = @last_line_end - start_pos - end - - ## Scan the string for line endings; store the positions of all endings - ## in @line_ends. - while buf.skip_until(/\n/) - @last_line_end = start_pos + buf.pos - @line_ends << @last_line_end - end - end - end - end -end diff --git a/lib/csscss/redundancy_analyzer.rb b/lib/csscss/redundancy_analyzer.rb index e58344e..be9de91 100644 --- a/lib/csscss/redundancy_analyzer.rb +++ b/lib/csscss/redundancy_analyzer.rb @@ -5,15 +5,23 @@ def initialize(raw_css) @raw_css = raw_css end - def redundancies(minimum = nil) + def redundancies(opts = {}) + minimum = opts[:minimum] + ignored_properties = opts[:ignored_properties] || [] + ignored_selectors = opts[:ignored_selectors] || [] + match_shorthand = opts.fetch(:match_shorthand, true) + rule_sets = Parser::Css.parse(@raw_css) matches = {} parents = {} rule_sets.each do |rule_set| + next if ignored_selectors.include?(rule_set.selectors.selectors) + sel = rule_set.selectors + rule_set.declarations.each do |dec| - sel = rule_set.selectors + next if ignored_properties.include?(dec.property) - if parser = shorthand_parser(dec.property) + if match_shorthand && parser = shorthand_parser(dec.property) if new_decs = parser.parse(dec.property, dec.value) if dec.property == "border" %w(border-top border-right border-bottom border-left).each do |property| @@ -57,7 +65,6 @@ def redundancies(minimum = nil) end end - # trims any derivative declarations alongside shorthand inverted_matches.each do |selectors, declarations| redundant_derivatives = declarations.select do |dec| diff --git a/lib/csscss/reporter.rb b/lib/csscss/reporter.rb index 8509872..96f428c 100644 --- a/lib/csscss/reporter.rb +++ b/lib/csscss/reporter.rb @@ -13,7 +13,7 @@ def report(options = {}) selector_groups = selector_groups.map {|selectors| "{#{maybe_color(selectors, :red, should_color)}}" } last_selector = selector_groups.pop count = declarations.size - io.puts %Q(#{selector_groups.join(", ")} AND #{last_selector} share #{maybe_color(count, :red, should_color)} rule#{"s" if count > 1}) + io.puts %Q(#{selector_groups.join(", ")} AND #{last_selector} share #{maybe_color(count, :red, should_color)} declaration#{"s" if count > 1}) if verbose declarations.each {|dec| io.puts(" - #{maybe_color(dec, :yellow, should_color)}") } end diff --git a/lib/csscss/sass_include_extensions.rb b/lib/csscss/sass_include_extensions.rb new file mode 100644 index 0000000..5b4360c --- /dev/null +++ b/lib/csscss/sass_include_extensions.rb @@ -0,0 +1,20 @@ +require "sass" + +Sass::Tree::MixinDefNode.class_eval do + def children + first_child = @children.first + + # not sure why/how we can get here with empty children, but it + # causes issues + unless @children.empty? || (first_child.is_a?(Sass::Tree::CommentNode) && first_child.value.first =~ /CSSCSS START/) + begin_comment = Sass::Tree::CommentNode.new(["/* CSSCSS START MIXIN: #{name} */"], :normal) + end_comment = Sass::Tree::CommentNode.new(["/* CSSCSS END MIXIN: #{name} */"], :normal) + + begin_comment.options = end_comment.options = {} + @children.unshift(begin_comment) + @children.push(end_comment) + end + + @children + end +end diff --git a/lib/csscss/types.rb b/lib/csscss/types.rb index 0cbcd74..3d01f4d 100644 --- a/lib/csscss/types.rb +++ b/lib/csscss/types.rb @@ -1,9 +1,5 @@ module Csscss - class Declaration < Struct.new(:property, :value, :parents) - def self.from_csspool(dec) - new(dec.property.to_s.downcase, dec.expressions.join(" ").downcase) - end - + Declaration = Struct.new(:property, :value, :parents) do def self.from_parser(property, value, clean = true) value = value.to_s property = property.to_s @@ -30,14 +26,15 @@ def without_parents def ==(other) if other.respond_to?(:property) && other.respond_to?(:value) - property == other.property && value == other.value + # using eql? tanks performance + property == other.property && normalize_value(value) == normalize_value(other.value) else false end end def hash - [property, value].hash + [property, normalize_value(value)].hash end def eql?(other) @@ -67,9 +64,18 @@ def inspect "<#{self.class} #{to_s}>" end end + + private + def normalize_value(value) + if value =~ /^0(#{Csscss::Parser::Common::UNITS.join("|")}|%)$/ + "0" + else + value + end + end end - class Selector < Struct.new(:selectors) + Selector = Struct.new(:selectors) do def self.from_parser(selectors) new(selectors.to_s.strip) end @@ -87,6 +93,5 @@ def inspect end end - class Ruleset < Struct.new(:selectors, :declarations) - end + Ruleset = Struct.new(:selectors, :declarations) end diff --git a/lib/csscss/version.rb b/lib/csscss/version.rb index 731f27b..7623093 100644 --- a/lib/csscss/version.rb +++ b/lib/csscss/version.rb @@ -1,3 +1,3 @@ module Csscss - VERSION = "0.2.1" + VERSION = "1.3.3" end diff --git a/test/csscss/parser/common_test.rb b/test/csscss/parser/common_test.rb index 0087976..c803bce 100644 --- a/test/csscss/parser/common_test.rb +++ b/test/csscss/parser/common_test.rb @@ -106,6 +106,8 @@ class CommonTest @parser.length.must_parse "123px" @parser.length.must_parse "123EM" @parser.length.must_parse "1.23Pt" + @parser.length.must_parse "0" + @parser.length.wont_parse "1" end end @@ -115,6 +117,16 @@ class CommonTest @parser.http.must_parse 'foo\(bar\).jpg' @parser.http.must_parse 'http://foo\(bar\).jpg' @parser.http.must_parse 'http://foo.com/baz/\(bar\).jpg' + @parser.http.must_parse "//foo.com/foo.jpg" + @parser.http.must_parse "https://foo.com/foo.jpg" + @parser.http.must_parse "http://foo100.com/foo.jpg" + @parser.http.must_parse "http://foo-bar.com/foo.jpg" + end + + it "parses data" do + @parser.data.must_parse "data:image/jpg;base64,IMGDATAGOESHERE==" + @parser.data.must_parse "data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==" + @parser.data.must_parse "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC" end it "parses urls" do @@ -124,6 +136,16 @@ class CommonTest @parser.url.must_parse "url('foo.jpg')" @parser.url.must_parse "url('foo.jpg' )" @parser.url.must_parse 'url(foo\(bar\).jpg)' + @parser.url.must_parse "url(data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==)" + @parser.url.must_parse "url('data:image/svg+xml;base64,IMGDATAGOESHERE4/5/h/1+==')" + end + + it "parses specials characters" do + @parser.between('"', '"') { @parser.symbol("{") }.must_parse '"{"' + @parser.between('"', '"') { @parser.symbol("}") }.must_parse '"}"' + @parser.between('"', '"') { @parser.symbol("%") }.must_parse '"%"' + @parser.double_quoted { @parser.symbol("{") }.must_parse %("{") + @parser.single_quoted { @parser.symbol('{') }.must_parse %('{') end end end diff --git a/test/csscss/parser/css_test.rb b/test/csscss/parser/css_test.rb index e6a956a..68e25d1 100644 --- a/test/csscss/parser/css_test.rb +++ b/test/csscss/parser/css_test.rb @@ -23,14 +23,14 @@ module Css end it "parses comments" do - @parser.css_space?.must_parse "/* foo */" - @parser.css_space?.must_parse %$ + @parser.comment.must_parse "/* foo */" + @parser.comment.must_parse %$ /* foo * bar */ $ - @parser.css_space?.must_parse %$ + @parser.comment.repeat(1).must_parse %$ /* foo */ /* bar */ $ @@ -63,11 +63,13 @@ module Css */ .bar { border: 1px solid black /* sdflk */ } .baz { background: white /* sdflk */ } + .baz2 { background: white /* {sdflk} */ } $ trans(css).must_equal([ rs(sel(".bar"), [dec("border", "1px solid black /* sdflk */")]), - rs(sel(".baz"), [dec("background", "white /* sdflk */")]) + rs(sel(".baz"), [dec("background", "white /* sdflk */")]), + rs(sel(".baz2"), [dec("background", "white /* {sdflk} */")]) ]) end @@ -104,7 +106,7 @@ module Css ]) end - it "recognizes media queries" do + it "recognizes @media queries" do css = %$ @media only screen { /* some comment */ @@ -117,6 +119,15 @@ module Css } } + @media only screen { + @-webkit-keyframes webkitSiblingBugfix { + from { position: relative; } + to { position: relative; } + } + + a { position: relative } + } + h1 { outline: 1px; } @@ -125,6 +136,51 @@ module Css trans(css).must_equal([ rs(sel("#foo"), [dec("background-color", "black")]), rs(sel("#bar"), [dec("display", "none")]), + rs(sel("from"), [dec("position", "relative")]), + rs(sel("to"), [dec("position", "relative")]), + rs(sel("a"), [dec("position", "relative")]), + rs(sel("h1"), [dec("outline", "1px")]) + ]) + end + + it "recognizes empty @media queries with no spaces" do + css = %$ + @media (min-width: 768px) and (max-width: 979px) {} + $ + + trans(css).must_equal([ + rs(sel("@media (min-width: 768px) and (max-width: 979px)"), []), + ]) + end + + it "recognizes empty @media queries with spaces" do + css = %$ + @media (min-width: 768px) and (max-width: 979px) { + } + $ + + trans(css).must_equal([ + rs(sel("@media (min-width: 768px) and (max-width: 979px)"), []), + ]) + end + + it "ignores @import statements" do + css = %$ + @import "foo.css"; + @import "bar.css"; + + /* + .x { + padding: 3px; + } + */ + + h1 { + outline: 1px; + } + $ + + trans(css).must_equal([ rs(sel("h1"), [dec("outline", "1px")]) ]) end @@ -134,6 +190,102 @@ module Css rs(sel("h1"), [dec("display", "none")]) ]) end + + it "ignores mixin selectors" do + css = %$ + h1 { + /* CSSCSS START MIXIN: foo */ + font-family: serif; + font-size: 10px; + display: block; + /* CSSCSS END MIXIN: foo */ + + /* CSSCSS START MIXIN: bar */ + outline: 1px; + /* CSSCSS END MIXIN: bar */ + + float: left; + } + $ + + trans(css).must_equal([ + rs(sel("h1"), [dec("float", "left")]) + ]) + end + + it "parses attributes with encoded data that include semicolons" do + trans(%$ + .foo1 { + background: rgb(123, 123, 123) url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x; + display: block; + } + + .foo2 { + background: white url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x + } + + .foo3 { + outline: 1px; + background: white url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAACECAYAAABRaEHiAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAHRJREFUeNqkUjESwCAIw+T/X/UHansdkLTQDnXgCAHNEW2tZbDz/Aq994bzqoY5Z8wEwiEcmmfwiRK+EGOMTVBrtz4mY9kEAyz6+E3sJ7MWBs1PaUy1lHLLmgTqElltNxLiINTBbWi0Vj5DZC9CaqZEOwQYAPhxY/7527NfAAAAAElFTkSuQmCC) repeat-x; + display: block; + } + + .foo4 { + background: blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px; + display: block; + } + $).must_equal([ + rs(sel(".foo1"), [dec("background", "rgb(123, 123, 123) url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x"), + dec("display", "block")]), + rs(sel(".foo2"), [dec("background", "white url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x")]), + rs(sel(".foo3"), [dec("outline", "1px"), + dec("background", "white url(data:image/png;base64,ivborw0kggoaaaansuheugaaaaeaaacecayaaabraehiaaaagxrfwhrtb2z0d2fyzqbbzg9izsbjbwfnzvjlywr5ccllpaaaahrjrefuenqkujeswcaiw+t/x/uhansdkltqdnxgcahnew2tzbdz/aq994bzqoy5z8wewiecmmfwirk+egomtvbrtz4my9keayz6+e3sj7mwbs1pauy1lhllmgtqelltnxliintbbwi0vj5dzc9caqzeowqyaphxy/7527nfaaaaaelftksuqmcc) repeat-x"), + dec("display", "block")]), + rs(sel(".foo4"), [dec("background", "blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px"), + dec("display", "block")]) + ]) + end + + it "parses attributes with special characters" do + css = %$ + + #menu a::before { + content: "{"; + left: -6px; + } + + #menu a::after { + content: "}"; + right: -6px; + } + + #menu a::weird { + content: "@"; + up: -2px; + } + + #menu a::after_all { + content: '{'; + right: -6px; + } + + $ + + trans(css).must_equal([ + rs(sel("#menu a::before"), [dec("content", '"{"'), + dec("left", "-6px") + ]), + rs(sel("#menu a::after"), [dec("content", '"}"'), + dec("right", "-6px") + ]), + rs(sel("#menu a::weird"), [dec("content", '"@"'), + dec("up", "-2px") + ]), + rs(sel("#menu a::after_all"), [dec("content", "'{'"), + dec("right", "-6px") + ]) + ]) + end end end end diff --git a/test/csscss/redundancy_analyzer_test.rb b/test/csscss/redundancy_analyzer_test.rb index ba17ba3..8f72e25 100644 --- a/test/csscss/redundancy_analyzer_test.rb +++ b/test/csscss/redundancy_analyzer_test.rb @@ -22,7 +22,7 @@ module Csscss [sel(".bar"), sel("h1, h2")] , [dec("outline", "none"), dec("position", "relative")] ] - RedundancyAnalyzer.new(css).redundancies(2).must_equal({ + RedundancyAnalyzer.new(css).redundancies(minimum:2).must_equal({ [sel(".bar"), sel("h1, h2")] => [dec("outline", "none"), dec("position", "relative")] }) end @@ -68,7 +68,7 @@ module Csscss .baz { margin: 3px 3px 30px 3px; padding: 10px 30px; - background: white url(images/bg-bolt-inactive.png) no-repeat 99% 5px; + background: blue url(images/bg-bolt-inactive.png) no-repeat 99% 5px; -webkit-border-radius: 4px; -moz-border-radius: 4px; @@ -88,7 +88,7 @@ module Csscss } $ - redundancies = RedundancyAnalyzer.new(css).redundancies(3) + redundancies = RedundancyAnalyzer.new(css).redundancies(minimum:3) redundancies[[sel(".bar"), sel(".bar2"), sel(".baz")]].size.must_equal(5) end @@ -184,6 +184,15 @@ module Csscss }) end + it "doesn't match shorthand when explicitly turned off" do + css = %$ + .foo { background-color: #fff } + .bar { background: #fff } + $ + + RedundancyAnalyzer.new(css).redundancies(match_shorthand:false).must_equal({}) + end + it "3-way case consolidation" do css = %$ .bar { background: #fff } @@ -230,7 +239,7 @@ module Csscss [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("border-style", "solid"), dec("border-width", "4px")] }) - RedundancyAnalyzer.new(css).redundancies(2).must_equal({ + RedundancyAnalyzer.new(css).redundancies(minimum:2).must_equal({ [sel(".bar"), sel(".baz"), sel(".foo")] => [dec("border-style", "solid"), dec("border-width", "4px")] }) end @@ -248,6 +257,36 @@ module Csscss }) end + it "ignores specific properties" do + css = %$ + h1, h2 { display: none; position: relative; outline:none} + .foo { DISPLAY: none; width: 1px } + .bar { position: relative; width: 1px; outline: none } + .baz { display: none } + $ + + RedundancyAnalyzer.new(css).redundancies(ignored_properties:%w(display outline)).must_equal({ + [sel(".bar"), sel("h1, h2")] => [dec("position", "relative")], + [sel(".bar"), sel(".foo")] => [dec("width", "1px")], + }) + + RedundancyAnalyzer.new(css).redundancies(ignored_properties:%w(display outline), ignored_selectors:%w(.foo)).must_equal({ + [sel(".bar"), sel("h1, h2")] => [dec("position", "relative")] + }) + end + + it "matches 0 and 0px" do + css = %$ + .bar { padding: 0; } + .foo { padding: 0px; } + $ + + RedundancyAnalyzer.new(css).redundancies.must_equal({ + [sel(".bar"), sel(".foo")] => [dec("padding", "0")] + }) + end + + # TODO: someday # it "reports duplication within the same selector" do # css = %$ diff --git a/test/csscss/reporter_test.rb b/test/csscss/reporter_test.rb index 17d3607..eb8fcbf 100644 --- a/test/csscss/reporter_test.rb +++ b/test/csscss/reporter_test.rb @@ -12,19 +12,19 @@ module Csscss }) expected =<<-EXPECTED -{.foo} AND {.bar} share 2 rules -{h1, h2}, {.foo} AND {.baz} share 1 rule -{h1, h2} AND {.bar} share 1 rule +{.foo} AND {.bar} share 2 declarations +{h1, h2}, {.foo} AND {.baz} share 1 declaration +{h1, h2} AND {.bar} share 1 declaration EXPECTED reporter.report(color:false).must_equal expected expected =<<-EXPECTED -{.foo} AND {.bar} share 2 rules +{.foo} AND {.bar} share 2 declarations - width: 1px - border: black -{h1, h2}, {.foo} AND {.baz} share 1 rule +{h1, h2}, {.foo} AND {.baz} share 1 declaration - display: none -{h1, h2} AND {.bar} share 1 rule +{h1, h2} AND {.bar} share 1 declaration - position: relative EXPECTED reporter.report(verbose:true, color:false).must_equal expected diff --git a/test/csscss/sass_include_extensions_test.rb b/test/csscss/sass_include_extensions_test.rb new file mode 100644 index 0000000..13f0ea8 --- /dev/null +++ b/test/csscss/sass_include_extensions_test.rb @@ -0,0 +1,68 @@ +require "test_helper" +require "tempfile" +require "csscss/sass_include_extensions" + +module Csscss + describe "sass import extensions" do + it "should add comments before and after mixin properties" do + scss =<<-SCSS + @mixin foo { + font: { + family: serif; + size: 10px; + } + + display: block; + } + + @mixin bar { + outline: 1px; + } + + h1 { + @include foo; + @include bar; + } + SCSS + + + css =<<-CSS +h1 { + /* CSSCSS START MIXIN: foo */ + font-family: serif; + font-size: 10px; + display: block; + /* CSSCSS END MIXIN: foo */ + /* CSSCSS START MIXIN: bar */ + outline: 1px; + /* CSSCSS END MIXIN: bar */ } + CSS + + Sass::Engine.new(scss, syntax: :scss, cache: false).render.must_equal(css) + end + + it "should insert comments even with imported stylesheets" do + Tempfile.open(['foo', '.scss']) do |f| + f << <<-SCSS + @mixin foo { + outline: 1px; + } + + h1 { + @include foo; + } + SCSS + f.close + + css =<<-CSS +h1 { + /* CSSCSS START MIXIN: foo */ + outline: 1px; + /* CSSCSS END MIXIN: foo */ } + CSS + + Sass::Engine.new("@import '#{File.basename(f.path)}'", syntax: :scss, cache: false, load_paths: ["/tmp"]).render.must_equal(css) + end + end + end +end diff --git a/test/csscss/types_test.rb b/test/csscss/types_test.rb index de80520..b2dca94 100644 --- a/test/csscss/types_test.rb +++ b/test/csscss/types_test.rb @@ -69,5 +69,13 @@ module Csscss h[dec2].must_equal true h[dec3].must_equal true end + + it "equates 0 length with and without units" do + Declaration.new("padding", "0px").must_equal Declaration.new("padding", "0") + Declaration.new("padding", "0%").must_equal Declaration.new("padding", "0") + Declaration.new("padding", "0").must_equal Declaration.new("padding", "0em") + + Declaration.new("padding", "1").wont_equal Declaration.new("padding", "1px") + end end end diff --git a/test/just_parse.rb b/test/just_parse.rb index ef9f1f6..47c7249 100644 --- a/test/just_parse.rb +++ b/test/just_parse.rb @@ -1,6 +1,6 @@ #! /usr/bin/env ruby -require "debugger" +require "byebug" require "csscss" raise "need a file name" unless ARGV[0] diff --git a/test/test_helper.rb b/test/test_helper.rb index d0c7ea5..4679d7a 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -1,11 +1,11 @@ -require "rubygems" -require "bundler/setup" +require 'rubygems' +require 'bundler/setup' -require "minitest/autorun" -require "minitest/rg" -require "debugger" +require 'minitest/autorun' +require 'minitest/rg' +require 'byebug' -require "csscss" +require 'csscss' module TypeHelpers def sel(s)