diff --git a/lib/compass/commands.rb b/lib/compass/commands.rb index be65f1b9..7389a508 100644 --- a/lib/compass/commands.rb +++ b/lib/compass/commands.rb @@ -5,7 +5,7 @@ require 'compass/commands/registry' %w(base generate_grid_background help list_frameworks project_base update_project watch_project create_project installer_command - print_version stamp_pattern validate_project + print_version project_stats stamp_pattern validate_project write_configuration).each do |lib| require "compass/commands/#{lib}" end diff --git a/lib/compass/commands/project_stats.rb b/lib/compass/commands/project_stats.rb new file mode 100644 index 00000000..2cabb0d7 --- /dev/null +++ b/lib/compass/commands/project_stats.rb @@ -0,0 +1,141 @@ +require 'compass/commands/project_base' +require 'compass/commands/update_project' + +module Compass + module Commands + module StatsOptionsParser + def set_options(opts) + opts.banner = %Q{ + Usage: compass stats [path/to/project] [options] + + Description: + Compile project at the path specified (or the current + directory if not specified) and then compute statistics + for the sass and css files in the project. + + Options: + }.strip.split("\n").map{|l| l.gsub(/^ {0,10}/,'')}.join("\n") + + super + end + end + class ProjectStats < UpdateProject + + register :stats + + def initialize(working_path, options) + super + assert_project_directory_exists! + end + + def perform + super + require 'compass/stats' + compiler = new_compiler_instance + sass_files = sorted_sass_files(compiler) + rows = [[ :-, :-, :-, :-, :- ], + [ 'Filename', 'Rules', 'Properties', 'Mixins Defs', 'Mixins Used' ], + [ :-, :-, :-, :-, :- ]] + maximums = [ 8, 5, 10, 14, 11 ] + alignments = [ :left, :right, :right, :right, :right ] + delimiters = [ ['| ', ' |'], [' ', ' |'], [' ', ' |'], [' ', ' |'], [' ', ' |'] ] + totals = [ "Total (#{sass_files.size} files):", 0, 0, 0, 0 ] + + sass_files.each do |sass_file| + css_file = compiler.corresponding_css_file(sass_file) unless sass_file[0..0] == '_' + row = filename_columns(sass_file) + row += sass_columns(sass_file) + row.each_with_index do |c, i| + maximums[i] = [maximums[i].to_i, c.size].max + totals[i] = totals[i] + c.to_i if i > 0 + end + rows << row + end + rows << [:-, :-, :-, :-, :-] + rows << totals.map{|t| t.to_s} + rows << [:-, :-, :-, :-, :-] + rows.each do |row| + row.each_with_index do |col, i| + print pad(col, maximums[i], :align => alignments[i], :left => delimiters[i].first, :right => delimiters[i].last) + end + print "\n" + end + end + + def pad(c, max, options = {}) + options[:align] ||= :left + if c == :- + filler = '-' + c = '' + else + filler = ' ' + end + spaces = max - c.size + filled = filler * [spaces,0].max + "#{options[:left]}#{filled if options[:align] == :right}#{c}#{filled if options[:align] == :left}#{options[:right]}" + end + + def sorted_sass_files(compiler) + sass_files = compiler.sass_files(:exclude_partials => false) + sass_files.map! do |s| + filename = Compass.deprojectize(s, File.join(Compass.configuration.project_path, Compass.configuration.sass_dir)) + [s, File.dirname(filename), File.basename(filename)] + end + sass_files = sass_files.sort_by do |s,d,f| + File.join(d, f[0] == ?_ ? f[1..-1] : f) + end + sass_files.map!{|s,d,f| s} + end + + def filename_columns(sass_file) + filename = Compass.deprojectize(sass_file, working_path) + [filename] + end + + def sass_columns(sass_file) + sf = Compass::Stats::SassFile.new(sass_file) + sf.analyze! + %w(rule_count prop_count mixin_def_count mixin_count).map do |t| + sf.send(t).to_s + end + end + + class << self + + def option_parser(arguments) + parser = Compass::Exec::CommandOptionParser.new(arguments) + parser.extend(Compass::Exec::GlobalOptionsParser) + parser.extend(Compass::Exec::ProjectOptionsParser) + parser.extend(StatsOptionsParser) + end + + def usage + option_parser([]).to_s + end + + def description(command) + "Report statistics about your stylesheets" + end + + def parse!(arguments) + parser = option_parser(arguments) + parser.parse! + parse_arguments!(parser, arguments) + parser.options + end + + def parse_arguments!(parser, arguments) + if arguments.size == 1 + parser.options[:project_name] = arguments.shift + elsif arguments.size == 0 + # default to the current directory. + else + raise Compass::Error, "Too many arguments were specified." + end + end + + end + + end + end +end diff --git a/lib/compass/compiler.rb b/lib/compass/compiler.rb index c48b2b63..a38fba76 100644 --- a/lib/compass/compiler.rb +++ b/lib/compass/compiler.rb @@ -13,8 +13,9 @@ module Compass self.options[:cache_location] ||= File.join(from, ".sass-cache") end - def sass_files - @sass_files || Dir.glob(separate("#{from}/**/[^_]*.sass")) + def sass_files(options = {}) + exclude_partials = options.fetch(:exclude_partials, true) + @sass_files || Dir.glob(separate("#{from}/**/#{'[^_]' if exclude_partials}*.sass")) end def stylesheet_name(sass_file) @@ -51,4 +52,4 @@ module Compass end end end -end \ No newline at end of file +end diff --git a/lib/compass/configuration/helpers.rb b/lib/compass/configuration/helpers.rb index f779a973..18bd60ac 100644 --- a/lib/compass/configuration/helpers.rb +++ b/lib/compass/configuration/helpers.rb @@ -66,6 +66,15 @@ module Compass File.join(project_path, *path.split('/')) end + def deprojectize(path, project_path = nil) + project_path ||= configuration.project_path + if path[0..(project_path.size - 1)] == project_path + path[(project_path.size + 1)..-1] + else + path + end + end + # TODO: Deprecate the src/config.rb location. KNOWN_CONFIG_LOCATIONS = [".compass/config.rb", "config/compass.config", "config.rb", "src/config.rb"] diff --git a/lib/compass/sass_extensions/monkey_patches.rb b/lib/compass/sass_extensions/monkey_patches.rb index 04cc8a72..c612ea21 100644 --- a/lib/compass/sass_extensions/monkey_patches.rb +++ b/lib/compass/sass_extensions/monkey_patches.rb @@ -1,3 +1,3 @@ -%w(stylesheet_updating).each do |patch| +%w(stylesheet_updating traversal).each do |patch| require "compass/sass_extensions/monkey_patches/#{patch}" -end \ No newline at end of file +end diff --git a/lib/compass/sass_extensions/monkey_patches/traversal.rb b/lib/compass/sass_extensions/monkey_patches/traversal.rb new file mode 100644 index 00000000..4b40e2ec --- /dev/null +++ b/lib/compass/sass_extensions/monkey_patches/traversal.rb @@ -0,0 +1,23 @@ +module Sass + module Tree + class Node + unless method_defined?(:visit_depth_first) + def visit_depth_first(visitor) + visitor.visit(self) + visitor.down(self) if children.any? and visitor.respond_to?(:down) + if is_a?(ImportNode) && visitor.import?(self) + root = Sass::Files.tree_for(import, @options) + imported_children = root.children + end + + (imported_children || children).each do |child| + break if visitor.respond_to?(:stop?) && visitor.stop? + child.visit_depth_first(visitor) + end + visitor.up(self) if children.any? + end + end + end + end +end + diff --git a/lib/compass/stats.rb b/lib/compass/stats.rb new file mode 100644 index 00000000..18a93790 --- /dev/null +++ b/lib/compass/stats.rb @@ -0,0 +1,78 @@ +module Compass + module Stats + class StatsVisitor + attr_accessor :rule_count, :prop_count, :mixin_def_count, :mixin_count + def initialize + self.rule_count = 0 + self.prop_count = 0 + self.mixin_def_count = 0 + self.mixin_count = 0 + end + def visit(node) + self.prop_count += 1 if node.is_a?(Sass::Tree::PropNode) && !node.children.any? + if node.is_a?(Sass::Tree::RuleNode) + self.rule_count += node.rules.map{|r| r.split(/,/)}.flatten.compact.size + end + self.mixin_def_count += 1 if node.is_a?(Sass::Tree::MixinDefNode) + self.mixin_count += 1 if node.is_a?(Sass::Tree::MixinNode) + end + def up(node) + end + def down(node) + end + def import?(node) + return false + full_filename = node.send(:import) + full_filename != Compass.deprojectize(full_filename) + end + end + class CssFile + attr_accessor :path + def initialize(path) + self.path = path + end + def contents + @contents ||= File.read(path) + end + def lines + contents.inject(0){|m,c| m + 1 } + end + end + class SassFile + attr_accessor :path + attr_reader :visitor + def initialize(path) + self.path = path + end + def contents + @contents ||= File.read(path) + end + def tree + @tree = Sass::Engine.new(contents, Compass.configuration.to_sass_engine_options).to_tree + end + def visit_tree! + @visitor = StatsVisitor.new + tree.visit_depth_first(@visitor) + @visitor + end + def analyze! + visit_tree! + end + def lines + contents.inject(0){|m,c| m + 1 } + end + def rule_count + visitor.rule_count + end + def prop_count + visitor.prop_count + end + def mixin_def_count + visitor.mixin_def_count + end + def mixin_count + visitor.mixin_count + end + end + end +end