Compass/compass

View on GitHub
cli/lib/compass/commands/project_stats.rb

Summary

Maintainability
A
3 hrs
Test Coverage
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)
        total_label = "Total (#{sass_files.size} files):"
        rows       = [[           :-,           :-,           :-,            :-,            :-,          :-,              :-,               :-,             :- ],
                      [   'Filename',      'Rules', 'Properties', 'Mixins Defs', 'Mixins Used',  'Filesize', 'CSS Selectors', 'CSS Properties', 'CSS Filesize' ],
                      [           :-,           :-,           :-,            :-,            :-,          :-,              :-,               :-,             :- ]]
        maximums   =  [ total_label.length,      5,           10,            14,            11,          13,              13,               14,             14 ]
        alignments =  [        :left,       :right,       :right,        :right,        :right,      :right,          :right,           :right,         :right ]
        delimiters =  [ ['| ', ' |'],  [' ', ' |'],  [' ', ' |'],   [' ', ' |'],   [' ', ' |'], [' ', ' |'],     [' ', ' |'],      [' ', ' |'],    [' ', ' |'] ]
        formatters =  [          nil,          nil,          nil,           nil,           nil,         :kb,             nil,             nil,             :kb ]
        totals     =  [ total_label,             0,            0,             0,             0,           0,               0,                0,              0 ]

        columns = rows.first.size

        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 += css_columns(css_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 << [:-] * columns
        rows << totals.map{|t| t.to_s}
        rows << [:-] * columns
        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, :formatter => formatters[i])
          end
          print "\n"
        end
        if @missing_css_parser
          puts "\nInstall #{@missing_css_parser} to enable stats on your css files:\n\n\tgem install #{@missing_css_parser}"
        end
      end

      def pad(c, max, options = {})
        options[:align] ||= :left
        if c == :-
          filler = '-'
          c = ''
        else
          filler = ' '
        end
        c = send(:"format_#{options[:formatter]}", c) if options[:formatter]
        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 format_kb(v)
        return v unless v =~ /^\d+$/
        v = Integer(v)
        if v < 1024
          "#{v} B"
        else
          v = v / 1024.0
          "#{v.ceil} KB"
        end
      end

      def sorted_sass_files(compiler)
        sass_files = compiler.sass_files
        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 file_size).map do |t|
          sf.send(t).to_s
        end
      end

      def css_columns(css_file)
        if File.exists?(css_file)
          cf = Compass::Stats::CssFile.new(css_file)
          cf.analyze!
          %w(selector_count prop_count file_size).map do |t|
            cf.send(t).to_s
          end
        else
          return [ '--', '--' , '--']
        end
      rescue LoadError => e
        @missing_css_parser = e.message =~ /iconv/ ? "iconv" : "css_parser"
        return [ 'DISABLED', 'DISABLED', 'DISABLED' ]
      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 primary; false; 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