From f59ca512ce56c222bb37e93a5985e9f6f56f9015 Mon Sep 17 00:00:00 2001 From: Chris Eppstein Date: Tue, 25 Aug 2009 14:18:58 -0700 Subject: [PATCH] Refactor of the internal datastructures used to access project configuration. Configuration is now a singly linked list of configuration objects that inherit values and defaults from the next configuration instance. All instances hold a reference to the top of the configuration chain. There is now a consistent API for reading configuration property values: : Reads the fully-resolved attribute after taking configuration inheritance and defaults into account. raw_: reads attribute from a configuration object without inheritance or defaults. default_for(): reads the default value for an attribute default_for_: specifies the default value for an attribute. _without_default: reads the inherited attribute without applying defaults. comment_for_: Specifies a comment that will be emitted above the property when serializing the configuration to a file. Additionally, method_missing and respond_to both work down the configuration chain, so any method that is added to a configuration instance, can be accessed from the top level. The distinction between default and explicitly set values allows compass to more correctly manage the serialization of attributes when creating configuration files for projects. The compass configuration can still be accessed via Compass.configuration, however, the configuration object is no longer a singleton. This means that you can build several configuration chains to track several projects at once. This should ease the use of compass in other frameworks and plugins that want to use compass internally. --- lib/compass/app_integration/stand_alone.rb | 0 .../stand_alone/configuration.rb | 0 lib/compass/commands/installer_command.rb | 6 +- lib/compass/commands/project_base.rb | 6 +- lib/compass/configuration.rb | 326 +----------------- lib/compass/configuration/adapters.rb | 59 ++++ lib/compass/configuration/comments.rb | 22 ++ lib/compass/configuration/data.rb | 103 ++++++ lib/compass/configuration/defaults.rb | 92 +++++ lib/compass/configuration/helpers.rb | 61 ++++ lib/compass/configuration/inheritance.rb | 170 +++++++++ lib/compass/configuration/serialization.rb | 82 +++++ lib/compass/installers/base.rb | 6 +- lib/compass/installers/rails.rb | 61 ++-- lib/compass/installers/stand_alone.rb | 27 +- test/command_line_helper.rb | 8 - test/command_line_test.rb | 3 +- test/compass_test.rb | 4 +- test/configuration_test.rb | 52 +-- test/io_helper.rb | 19 + test/rails_integration_test.rb | 3 +- test/test_helper.rb | 1 + 22 files changed, 732 insertions(+), 379 deletions(-) create mode 100644 lib/compass/app_integration/stand_alone.rb create mode 100644 lib/compass/app_integration/stand_alone/configuration.rb create mode 100644 lib/compass/configuration/adapters.rb create mode 100644 lib/compass/configuration/comments.rb create mode 100644 lib/compass/configuration/data.rb create mode 100644 lib/compass/configuration/defaults.rb create mode 100644 lib/compass/configuration/helpers.rb create mode 100644 lib/compass/configuration/inheritance.rb create mode 100644 lib/compass/configuration/serialization.rb create mode 100644 test/io_helper.rb diff --git a/lib/compass/app_integration/stand_alone.rb b/lib/compass/app_integration/stand_alone.rb new file mode 100644 index 00000000..e69de29b diff --git a/lib/compass/app_integration/stand_alone/configuration.rb b/lib/compass/app_integration/stand_alone/configuration.rb new file mode 100644 index 00000000..e69de29b diff --git a/lib/compass/commands/installer_command.rb b/lib/compass/commands/installer_command.rb index b59d451b..8d3c2441 100644 --- a/lib/compass/commands/installer_command.rb +++ b/lib/compass/commands/installer_command.rb @@ -6,10 +6,10 @@ module Compass include Compass::Installers def configure! + Compass.add_configuration(installer.default_configuration) read_project_configuration - Compass.configuration.set_maybe(options) - Compass.configuration.default_all(installer.configuration_defaults) - Compass.configuration.set_defaults! + Compass.add_configuration(options) + Compass.add_configuration(installer.completed_configuration) end def installer diff --git a/lib/compass/commands/project_base.rb b/lib/compass/commands/project_base.rb index dc082eeb..0065721a 100644 --- a/lib/compass/commands/project_base.rb +++ b/lib/compass/commands/project_base.rb @@ -23,8 +23,6 @@ module Compass def configure! read_project_configuration - Compass.configuration.set_maybe(options) - Compass.configuration.set_defaults! end def projectize(path) @@ -49,8 +47,8 @@ module Compass # Read the configuration file for this project def read_project_configuration - if file = detect_configuration_file - Compass.configuration.parse(file) if File.readable?(file) + if (file = detect_configuration_file) && File.readable?(file) + Compass.add_configuration(file) end end diff --git a/lib/compass/configuration.rb b/lib/compass/configuration.rb index eaa5b8a4..a495a1fa 100644 --- a/lib/compass/configuration.rb +++ b/lib/compass/configuration.rb @@ -1,324 +1,36 @@ -require 'singleton' - module Compass - class Configuration + module Configuration ATTRIBUTES = [ :project_type, :project_path, - :http_path, :css_dir, :sass_dir, :images_dir, :javascripts_dir, - :output_style, - :environment, - :relative_assets, + :css_path, + :sass_path, + :images_path, + :javascripts_path, + :http_path, + :http_images_dir, + :http_stylesheets_dir, + :http_javascripts_dir, :http_images_path, :http_stylesheets_path, :http_javascripts_path, + :output_style, + :environment, + :relative_assets, :additional_import_paths, - :sass_options + :sass_options, + :asset_host, + :asset_cache_buster ] - attr_accessor *ATTRIBUTES - - attr_accessor :required_libraries - - def initialize - self.required_libraries = [] - end - - # parses a manifest file which is a ruby script - # evaluated in a Manifest instance context - def parse(config_file) - open(config_file) do |f| - parse_string(f.read, config_file) - end - end - - def parse_string(contents, filename) - bind = binding - eval(contents, bind, filename) - ATTRIBUTES.each do |prop| - value = eval(prop.to_s, bind) rescue nil - self.send("#{prop}=", value) if value - end - if @added_import_paths - self.additional_import_paths ||= [] - self.additional_import_paths += @added_import_paths - end - issue_deprecation_warnings - end - - def set_all(options) - ATTRIBUTES.each do |a| - self.send("#{a}=", options[a]) if options.has_key?(a) - end - end - - def set_maybe(options) - ATTRIBUTES.each do |a| - self.send("#{a}=", options[a]) if options[a] - end - end - - def default_all(options) - ATTRIBUTES.each do |a| - set_default_unless_set(a, options[a]) - end - end - - def set_default_unless_set(attribute, value) - self.send("#{attribute}=", value) unless self.send(attribute) - end - - def set_defaults! - ATTRIBUTES.each do |a| - set_default_unless_set(a, default_for(a)) - end - end - - def default_for(attribute) - method = "default_#{attribute}".to_sym - self.send(method) if respond_to?(method) - end - - def default_sass_dir - "src" - end - - def default_css_dir - "stylesheets" - end - - def default_images_dir - "images" - end - - def default_http_path - "/" - end - - def comment_for_http_path - "# Set this to the root of your project when deployed:\n" - end - - def relative_assets? - # the http_images_path is deprecated, but here for backwards compatibility. - relative_assets || http_images_path == :relative - end - - def comment_for_relative_assets - unless relative_assets - %q{# To enable relative paths to assets via compass helper functions. Uncomment: -# relative_assets = true -} - else - "" - end - end - - def default_output_style - if environment == :development - :expanded - else - :compact - end - end - - def default_line_comments - environment == :development - end - - def sass_path - if project_path && sass_dir - File.join(project_path, sass_dir) - end - end - - def css_path - if project_path && css_dir - File.join(project_path, css_dir) - end - end - - def http_root_relative(path) - hp = http_path || default_http_path - hp = hp[0..-2] if hp[-1..-1] == "/" - "#{hp}/#{path}" - end - - def add_import_path(*paths) - # The @added_import_paths variable works around an issue where - # the additional_import_paths gets overwritten during parse - @added_import_paths ||= [] - @added_import_paths += paths - self.additional_import_paths ||= [] - self.additional_import_paths += paths - end - - # When called with a block, defines the asset host url to be used. - # The block must return a string that starts with a protocol (E.g. http). - # The block will be passed the root-relative url of the asset. - # When called without a block, returns the block that was previously set. - def asset_host(&block) - if block_given? - @asset_host = block - else - @asset_host - end - end - - # When called with a block, defines the cache buster strategy to be used. - # The block must return nil or a string that can be appended to a url as a query parameter. - # The returned string must not include the starting '?'. - # The block will be passed the root-relative url of the asset. - # If the block accepts two arguments, it will also be passed a File object - # that points to the asset on disk -- which may or may not exist. - # When called without a block, returns the block that was previously set. - def asset_cache_buster(&block) - if block_given? - @asset_cache_buster = block - else - @asset_cache_buster - end - end - - - def serialize - if asset_cache_buster - raise Compass::Error, "Cannot serialize a configuration with asset_cache_buster set." - end - if asset_host - raise Compass::Error, "Cannot serialize a configuration with asset_host set." - end - contents = "" - required_libraries.each do |lib| - contents << %Q{require '#{lib}'\n} - end - contents << "# Require any additional compass plugins here.\n" - contents << "\n" if required_libraries.any? - ATTRIBUTES.each do |prop| - value = send(prop) - if respond_to?("comment_for_#{prop}") - contents << send("comment_for_#{prop}") - end - if block_given? && (to_emit = yield(prop, value)) - contents << to_emit - else - contents << Configuration.serialize_property(prop, value) unless value.nil? - end - end - contents - end - - def self.serialize_property(prop, value) - %Q(#{prop} = #{value.inspect}\n) - end - - def to_compiler_arguments(additional_options) - [project_path, sass_path, css_path, to_sass_engine_options.merge(additional_options)] - end - - def to_sass_plugin_options - locations = {} - locations[sass_path] = css_path if sass_path && css_path - Compass::Frameworks::ALL.each do |framework| - locations[framework.stylesheets_directory] = css_path || css_dir || "." - end - resolve_additional_import_paths.each do |additional_path| - locations[additional_path] = File.join(css_path || css_dir || ".", File.basename(additional_path)) - end - plugin_opts = {:template_location => locations} - plugin_opts[:style] = output_style if output_style - plugin_opts[:line_comments] = default_line_comments if environment - plugin_opts.merge!(sass_options || {}) - plugin_opts - end - - def resolve_additional_import_paths - (additional_import_paths || []).map do |path| - if project_path && !absolute_path?(path) - File.join(project_path, path) - else - path - end - end - end - - def to_sass_engine_options - engine_opts = {:load_paths => sass_load_paths} - engine_opts[:style] = output_style if output_style - engine_opts[:line_comments] = default_line_comments if environment - engine_opts.merge!(sass_options || {}) - end - - def sass_load_paths - load_paths = [] - load_paths << sass_path if sass_path - Compass::Frameworks::ALL.each do |framework| - load_paths << framework.stylesheets_directory - end - load_paths += resolve_additional_import_paths - load_paths - end - - # Support for testing. - def reset! - ATTRIBUTES.each do |attr| - send("#{attr}=", nil) - end - @asset_cache_buster = nil - @asset_host = nil - @added_import_paths = nil - self.required_libraries = [] - end - - def issue_deprecation_warnings - if http_images_path == :relative - puts "DEPRECATION WARNING: Please set relative_assets = true to enable relative paths." - end - end - - def require(lib) - required_libraries << lib - super - end - - def absolute_path?(path) - # This is only going to work on unix, gonna need a better implementation. - path.index(File::SEPARATOR) == 0 - end end - - module ConfigHelpers - def configuration - @configuration ||= Configuration.new - if block_given? - yield @configuration - end - @configuration - end - - def sass_plugin_configuration - configuration.to_sass_plugin_options - end - - def configure_sass_plugin! - @sass_plugin_configured = true - Sass::Plugin.options.merge!(sass_plugin_configuration) - end - - def sass_plugin_configured? - @sass_plugin_configured - end - - def sass_engine_options - configuration.to_sass_engine_options - end - end - - extend ConfigHelpers - +end + +['adapters', 'comments', 'defaults', 'helpers', 'inheritance', 'serialization', 'data'].each do |lib| + require File.join(File.dirname(__FILE__), 'configuration', lib) end diff --git a/lib/compass/configuration/adapters.rb b/lib/compass/configuration/adapters.rb new file mode 100644 index 00000000..e2dbf7d1 --- /dev/null +++ b/lib/compass/configuration/adapters.rb @@ -0,0 +1,59 @@ +module Compass + module Configuration + # The adapters module provides methods that make configuration data from a compass project + # adapt to various consumers of configuration data + module Adapters + def to_compiler_arguments(additional_options) + [project_path, sass_path, css_path, to_sass_engine_options.merge(additional_options)] + end + + def to_sass_plugin_options + locations = {} + locations[sass_path] = css_path if sass_path && css_path + Compass::Frameworks::ALL.each do |framework| + locations[framework.stylesheets_directory] = css_path || css_dir || "." + end + resolve_additional_import_paths.each do |additional_path| + locations[additional_path] = File.join(css_path || css_dir || ".", File.basename(additional_path)) + end + plugin_opts = {:template_location => locations} + plugin_opts[:style] = output_style if output_style + plugin_opts[:line_comments] = line_comments if environment + plugin_opts.merge!(sass_options || {}) + plugin_opts + end + + def resolve_additional_import_paths + (additional_import_paths || []).map do |path| + if project_path && !absolute_path?(path) + File.join(project_path, path) + else + path + end + end + end + + def absolute_path?(path) + # This is only going to work on unix, gonna need a better implementation. + path.index(File::SEPARATOR) == 0 + end + + def to_sass_engine_options + engine_opts = {:load_paths => sass_load_paths} + engine_opts[:style] = output_style if output_style + engine_opts[:line_comments] = line_comments if environment + engine_opts.merge!(sass_options || {}) + end + + def sass_load_paths + load_paths = [] + load_paths << sass_path if sass_path + Compass::Frameworks::ALL.each do |framework| + load_paths << framework.stylesheets_directory + end + load_paths += resolve_additional_import_paths + load_paths + end + end + end +end diff --git a/lib/compass/configuration/comments.rb b/lib/compass/configuration/comments.rb new file mode 100644 index 00000000..b62f6a92 --- /dev/null +++ b/lib/compass/configuration/comments.rb @@ -0,0 +1,22 @@ +module Compass + module Configuration + # Comments are emitted into the configuration file when serialized and make it easier to understand for new users. + module Comments + + def comment_for_http_path + "# Set this to the root of your project when deployed:\n" + end + + def comment_for_relative_assets + unless relative_assets + %q{# To enable relative paths to assets via compass helper functions. Uncomment: +# relative_assets = true +} + else + "" + end + end + + end + end +end diff --git a/lib/compass/configuration/data.rb b/lib/compass/configuration/data.rb new file mode 100644 index 00000000..755ccfd1 --- /dev/null +++ b/lib/compass/configuration/data.rb @@ -0,0 +1,103 @@ +module Compass + module Configuration + # The Compass configuration data storage class manages configuration data that comes from a variety of + # different sources and aggregates them together into a consistent API + # Some of the possible sources of configuration data: + # * Compass default project structure for stand alone projects + # * App framework specific project structures for rails, etc. + # * User supplied explicit configuration + # * Configuration data provided via the command line interface + # + # There are two kinds of configuration data that doesn't come from the user: + # + # 1. Configuration data that is defaulted as if the user had provided it themselves. + # This is useful for providing defaults that the user is likely to want to edit + # but shouldn't have to provide explicitly when getting started + # 2. Configuration data that is defaulted behind the scenes because _some_ value is + # required. + class Data + + attr_accessor :required_libraries + + include Compass::Configuration::Inheritance + include Compass::Configuration::Serialization + include Compass::Configuration::Adapters + + inherited_accessor *ATTRIBUTES + + def initialize(attr_hash = nil) + self.required_libraries = [] + set_all(attr_hash) if attr_hash + self.top_level = self + end + + def set_all(attr_hash) + # assert_valid_keys!(attr_hash) + attr_hash.each do |a, v| + if self.respond_to?("#{a}=") + self.send("#{a}=", v) + end + end + end + + def add_import_path(*paths) + # The @added_import_paths variable works around an issue where + # the additional_import_paths gets overwritten during parse + @added_import_paths ||= [] + @added_import_paths += paths + self.additional_import_paths ||= [] + self.additional_import_paths += paths + end + + # When called with a block, defines the asset host url to be used. + # The block must return a string that starts with a protocol (E.g. http). + # The block will be passed the root-relative url of the asset. + # When called without a block, returns the block that was previously set. + def asset_host(&block) + if block_given? + @asset_host = block + else + @asset_host + end + end + + # When called with a block, defines the cache buster strategy to be used. + # The block must return nil or a string that can be appended to a url as a query parameter. + # The returned string must not include the starting '?'. + # The block will be passed the root-relative url of the asset. + # If the block accepts two arguments, it will also be passed a File object + # that points to the asset on disk -- which may or may not exist. + # When called without a block, returns the block that was previously set. + def asset_cache_buster(&block) + if block_given? + @asset_cache_buster = block + else + @asset_cache_buster + end + end + + # Require a compass plugin and capture that it occured so that the configuration serialization works next time. + def require(lib) + required_libraries << lib + super + end + + def relative_assets? + # the http_images_path is deprecated, but here for backwards compatibility. + relative_assets || http_images_path == :relative + end + + private + + def assert_valid_keys!(attr_hash) + illegal_attrs = attr_hash.keys - ATTRIBUTES + if illegal_attrs.size == 1 + raise Error, "#{illegal_attrs.first.inspect} is not a valid configuration attribute." + elsif illegal_attrs.size > 0 + raise Error, "Illegal configuration attributes: #{illegal_attrs.map{|a| a.inspect}.join(", ")}" + end + end + + end + end +end \ No newline at end of file diff --git a/lib/compass/configuration/defaults.rb b/lib/compass/configuration/defaults.rb new file mode 100644 index 00000000..b646a412 --- /dev/null +++ b/lib/compass/configuration/defaults.rb @@ -0,0 +1,92 @@ +module Compass + module Configuration + module Defaults + + def http_path_without_default + "/" + end + + def default_output_style + if top_level.environment == :development + :expanded + else + :compact + end + end + + def default_line_comments + top_level.environment == :development + end + + def default_sass_path + if (pp = top_level.project_path) && (dir = top_level.sass_dir) + File.join(pp, dir) + end + end + + def default_css_path + if (pp = top_level.project_path) && (dir = top_level.css_dir) + File.join(pp, dir) + end + end + + def default_images_path + if (pp = top_level.project_path) && (dir = top_level.images_dir) + File.join(pp, dir) + end + end + + def default_javascripts_path + if (pp = top_level.project_path) && (dir = top_level.javascripts_dir) + File.join(pp, dir) + end + end + + def default_http_images_dir + top_level.images_dir + end + + def default_http_images_path + http_root_relative top_level.http_images_dir + end + + def default_http_stylesheets_dir + top_level.css_dir + end + + def default_http_stylesheets_path + http_root_relative top_level.http_stylesheets_dir + end + + def default_http_javascripts_dir + top_level.javascripts_dir + end + + def default_http_javascripts_path + http_root_relative top_level.http_javascripts_dir + end + + # helper functions + + def http_join(*segments) + segments.map do |segment| + segment = http_pathify(segment) + segment[-1..-1] == "/" ? segment[0..-2] : segment + end.join("/") + end + + def http_pathify(path) + if File::SEPARATOR == "/" + path + else + path.gsub(File::SEPARATOR, "/") + end + end + + def http_root_relative(path) + http_join top_level.http_path, path + end + + end + end +end \ No newline at end of file diff --git a/lib/compass/configuration/helpers.rb b/lib/compass/configuration/helpers.rb new file mode 100644 index 00000000..ea8ad504 --- /dev/null +++ b/lib/compass/configuration/helpers.rb @@ -0,0 +1,61 @@ +module Compass + module Configuration + # The helpers are available as methods on the Compass module. E.g. Compass.configuration + module Helpers + def configuration + @configuration ||= default_configuration + if block_given? + yield @configuration + end + @configuration + end + + def default_configuration + Data.new.extend(Defaults).extend(Comments) + end + + def add_configuration(config, filename = nil) + return if config.nil? + data = if config.is_a?(Compass::Configuration::Data) + config + elsif config.respond_to?(:read) + Compass::Configuration::Data.new_from_string(config.read, filename) + elsif config.is_a?(Hash) + Compass::Configuration::Data.new(config) + elsif config.is_a?(String) + Compass::Configuration::Data.new_from_file(config) + else + raise "I don't know what to do with: #{config.inspect}" + end + data.inherit_from!(configuration) + data.on_top! + @configuration = data + end + + # Support for testing. + def reset_configuration! + @configuration = nil + end + + def sass_plugin_configuration + configuration.to_sass_plugin_options + end + + def configure_sass_plugin! + @sass_plugin_configured = true + Sass::Plugin.options.merge!(sass_plugin_configuration) + end + + def sass_plugin_configured? + @sass_plugin_configured + end + + def sass_engine_options + configuration.to_sass_engine_options + end + end + end + + extend Configuration::Helpers + +end diff --git a/lib/compass/configuration/inheritance.rb b/lib/compass/configuration/inheritance.rb new file mode 100644 index 00000000..fd030172 --- /dev/null +++ b/lib/compass/configuration/inheritance.rb @@ -0,0 +1,170 @@ +module Compass + module Configuration + # The inheritance module makes it easy for configuration data to inherit from + # other instances of configuration data. This makes it easier for external code to layer + # bits of configuration from various sources. + module Inheritance + + def self.included(base) + # inherited_data stores configuration data that this configuration object will + # inherit if not provided explicitly. + base.send :attr_accessor, :inherited_data, :set_attributes, :top_level + + base.send(:include, InstanceMethods) + base.extend(ClassMethods) + end + + module ClassMethods + def inherited_writer(*attributes) + attributes.each do |attribute| + line = __LINE__ + 1 + class_eval %Q{ + def #{attribute}=(value) # def css_dir=(value) + @set_attributes ||= {} # @set_attributes ||= {} + @set_attributes[#{attribute.inspect}] = true # @set_attributes[:css_dir] = true + @#{attribute} = value # @css_dir = value + end # end + + def unset_#{attribute}! # def unset_css_dir! + unset!(#{attribute.inspect}) # unset!(:css_dir) + end # end + + def #{attribute}_set? # def css_dir_set? + set?(#{attribute.inspect}) # set?(:css_dir) + end # end + }, __FILE__, line + end + end + + # Defines the default reader to be an inherited_reader that will look at the inherited_data for its + # value when not set. The inherited reader calls to a raw reader that acts like a normal attribute + # reader but prefixes the attribute name with "raw_". + def inherited_reader(*attributes) + attributes.each do |attribute| + line = __LINE__ + 1 + class_eval %Q{ + def raw_#{attribute} # def raw_css_dir + @#{attribute} # @css_dir + end # end + def #{attribute}_without_default # def css_dir_without_default + read_without_default(#{attribute.inspect}) # read_without_default(:css_dir) + end # end + def #{attribute} # def css_dir + read(#{attribute.inspect}) # read(:css_dir) + end # end + }, __FILE__, line + end + end + + def inherited_accessor(*attributes) + inherited_reader(*attributes) + inherited_writer(*attributes) + end + + + end + + module InstanceMethods + + def on_top! + self.set_top_level(self) + end + + def set_top_level(new_top) + self.top_level = new_top + if self.inherited_data.respond_to?(:set_top_level) + self.inherited_data.set_top_level(new_top) + end + end + + + def inherit_from!(data) + if self.inherited_data + self.inherited_data.inherit_from!(data) + else + self.inherited_data = data + end + self + end + + def unset!(attribute) + @set_attributes ||= {} + send("#{attribute}=", nil) + @set_attributes.delete(attribute) + nil + end + + def set?(attribute) + @set_attributes ||= {} + @set_attributes[attribute] + end + + def default_for(attribute) + method = "default_#{attribute}".to_sym + if respond_to?(method) + send(method) + end + end + + # Read an explicitly set value that is either inherited or set on this instance + def read_without_default(attribute) + if set?(attribute) + send("raw_#{attribute}") + elsif inherited_data.respond_to?("#{attribute}_without_default") + inherited_data.send("#{attribute}_without_default") + elsif inherited_data.respond_to?(attribute) + inherited_data.send(attribute) + end + end + + # Read a value that is either inherited or set on this instance, if we get to the bottom-most configuration instance, + # we ask for the default starting at the top level. + def read(attribute) + if !(v = send("#{attribute}_without_default")).nil? + v + else + top_level.default_for(attribute) + end + end + + def method_missing(meth) + if inherited_data + inherited_data.send(meth) + else + raise NoMethodError, meth.to_s + end + end + + def respond_to?(meth) + if super + true + elsif inherited_data + inherited_data.respond_to?(meth) + else + false + end + end + + def debug + instances = [self] + instances << instances.last.inherited_data while instances.last.inherited_data + normalized_attrs = {} + ATTRIBUTES.each do |prop| + values = [] + instances.each do |instance| + values << { + :raw => (instance.send("raw_#{prop}") rescue nil), + :value => (instance.send("#{prop}_without_default") rescue nil), + :default => (instance.send("default_#{prop}") rescue nil), + :resoved => instance.send(prop) + } + end + normalized_attrs[prop] = values + end + normalized_attrs + end + + end + end + end +end diff --git a/lib/compass/configuration/serialization.rb b/lib/compass/configuration/serialization.rb new file mode 100644 index 00000000..4a0e1c0d --- /dev/null +++ b/lib/compass/configuration/serialization.rb @@ -0,0 +1,82 @@ +module Compass + module Configuration + # The serialization module manages reading and writing the configuration file(s). + module Serialization + def self.included(base) + base.send(:include, InstanceMethods) + base.extend ClassMethods + end + + module ClassMethods + def new_from_file(config_file) + data = Data.new + data.parse(config_file) + data + end + def new_from_string(contents, filename) + data = Data.new + data.parse_string(contents, filename) + data + end + end + + module InstanceMethods + # parses a configuration file which is a ruby script + def parse(config_file) + open(config_file) do |f| + parse_string(f.read, config_file) + end + end + + def parse_string(contents, filename) + bind = binding + eval(contents, bind, filename) + ATTRIBUTES.each do |prop| + value = eval(prop.to_s, bind) rescue nil + self.send("#{prop}=", value) if value + end + if @added_import_paths + self.additional_import_paths ||= [] + self.additional_import_paths += @added_import_paths + end + issue_deprecation_warnings + end + + def serialize + contents = "" + required_libraries.each do |lib| + contents << %Q{require '#{lib}'\n} + end + contents << "# Require any additional compass plugins here.\n" + contents << "\n" if required_libraries.any? + ATTRIBUTES.each do |prop| + value = send("#{prop}_without_default") + if value.is_a?(Proc) + $stderr.puts "WARNING: #{prop} is code and cannot be written to a file. You'll need to copy it yourself." + end + if respond_to?("comment_for_#{prop}") + contents << send("comment_for_#{prop}") + end + if block_given? && (to_emit = yield(prop, value)) + contents << to_emit + else + contents << serialize_property(prop, value) unless value.nil? + end + end + contents + end + + def serialize_property(prop, value) + %Q(#{prop} = #{value.inspect}\n) + end + + def issue_deprecation_warnings + if http_images_path == :relative + $stderr.puts "DEPRECATION WARNING: Please set relative_assets = true to enable relative paths." + end + end + + end + end + end +end diff --git a/lib/compass/installers/base.rb b/lib/compass/installers/base.rb index 6d9b6295..67120b40 100644 --- a/lib/compass/installers/base.rb +++ b/lib/compass/installers/base.rb @@ -26,12 +26,16 @@ module Compass define_method dir do Compass.configuration.send(dir) end + define_method "#{dir}_without_default" do + Compass.configuration.send("#{dir}_without_default") + end end # Initializes the project to work with compass def init dirs = manifest.map do |entry| - File.dirname(send("install_location_for_#{entry.type}", entry.to, entry.options)) + loc = send("install_location_for_#{entry.type}", entry.to, entry.options) + File.dirname(loc) end if manifest.has_stylesheet? diff --git a/lib/compass/installers/rails.rb b/lib/compass/installers/rails.rb index f2b7df40..cce33d9e 100644 --- a/lib/compass/installers/rails.rb +++ b/lib/compass/installers/rails.rb @@ -3,16 +3,38 @@ module Compass class RailsInstaller < Base - def configuration_defaults - { - :sass_dir => (sass_dir || prompt_sass_dir), - :css_dir => (css_dir || prompt_css_dir), - :images_dir => default_images_dir, - :javascripts_dir => default_javascripts_dir, - :http_stylesheets_path => default_http_stylesheets_path, - :http_javascripts_path => default_http_javascripts_path, - :http_images_path => default_http_images_path - } + module ConfigurationDefaults + def default_images_dir + File.join("public", "images") + end + + def default_javascripts_dir + File.join("public", "javascripts") + end + + def default_http_images_path + "/images" + end + + def default_http_javascripts_path + "/javascripts" + end + + def default_http_stylesheets_path + "/stylesheets" + end + + end + + def default_configuration + Compass::Configuration::Data.new.extend(ConfigurationDefaults) + end + + def completed_configuration + config = {} + config[:sass_dir] = prompt_sass_dir unless sass_dir_without_default + config[:css_dir] = prompt_css_dir unless css_dir_without_default + config unless config.empty? end def write_configuration_files(config_file = nil) @@ -45,25 +67,6 @@ NEXTSTEPS puts "\n(You are using haml, aren't you?)" end - def default_images_dir - separate("public/images") - end - - def default_javascripts_dir - separate("public/javascripts") - end - - def default_http_images_path - "/images" - end - - def default_http_javascripts_path - "/javascripts" - end - - def default_http_stylesheets_path - "/stylesheets" - end def install_location_for_html(to, options) separate("public/#{pattern_name_as_dir}#{to}") diff --git a/lib/compass/installers/stand_alone.rb b/lib/compass/installers/stand_alone.rb index 5ae61d5d..053670b3 100644 --- a/lib/compass/installers/stand_alone.rb +++ b/lib/compass/installers/stand_alone.rb @@ -3,6 +3,24 @@ module Compass class StandAloneInstaller < Base + module ConfigurationDefaults + def sass_dir_without_default + "src" + end + + def javascripts_dir_without_default + "javascripts" + end + + def css_dir_without_default + "stylesheets" + end + + def images_dir_without_default + "images" + end + end + def init directory targetize("") super @@ -28,9 +46,12 @@ module Compass write_configuration_files unless config_files_exist? end - # We want to rely on the defaults provided by Configuration - def configuration_defaults - {} + def default_configuration + Compass::Configuration::Data.new.extend(ConfigurationDefaults) + end + + def completed_configuration + nil end def finalize(options = {}) diff --git a/test/command_line_helper.rb b/test/command_line_helper.rb index 029994c3..d74b191a 100644 --- a/test/command_line_helper.rb +++ b/test/command_line_helper.rb @@ -88,14 +88,6 @@ module Compass::CommandLineHelper FileUtils.rm_rf(d) end - def capture_output - real_stdout, $stdout = $stdout, StringIO.new - yield - $stdout.string - ensure - $stdout = real_stdout - end - def execute(*arguments) Compass::Exec::Compass.new(arguments).run! end diff --git a/test/command_line_test.rb b/test/command_line_test.rb index 8e621deb..d02ecae7 100644 --- a/test/command_line_test.rb +++ b/test/command_line_test.rb @@ -7,9 +7,10 @@ require 'timeout' class CommandLineTest < Test::Unit::TestCase include Compass::TestCaseHelper include Compass::CommandLineHelper + include Compass::IoHelper def teardown - Compass.configuration.reset! + Compass.reset_configuration! end def test_print_version diff --git a/test/compass_test.rb b/test/compass_test.rb index 89e3f988..e22eec31 100644 --- a/test/compass_test.rb +++ b/test/compass_test.rb @@ -5,7 +5,7 @@ require 'compass' class CompassTest < Test::Unit::TestCase include Compass::TestCaseHelper def setup - Compass.configuration.reset! + Compass.reset_configuration! end def teardown @@ -90,7 +90,7 @@ private def within_project(project_name) @current_project = project_name - Compass.configuration.parse(configuration_file(project_name)) if File.exists?(configuration_file(project_name)) + Compass.add_configuration(configuration_file(project_name)) if File.exists?(configuration_file(project_name)) Compass.configuration.project_path = project_path(project_name) args = Compass.configuration.to_compiler_arguments(:logger => Compass::NullLogger.new) if Compass.configuration.sass_path && File.exists?(Compass.configuration.sass_path) diff --git a/test/configuration_test.rb b/test/configuration_test.rb index 0191d9a6..5974274c 100644 --- a/test/configuration_test.rb +++ b/test/configuration_test.rb @@ -1,71 +1,75 @@ require File.dirname(__FILE__)+'/test_helper' require 'compass' +require 'stringio' class ConfigurationTest < Test::Unit::TestCase + include Compass::IoHelper def setup - Compass.configuration.reset! + Compass.reset_configuration! end def test_parse_and_serialize - contents = <<-CONFIG + contents = StringIO.new(<<-CONFIG) require 'compass' # Require any additional compass plugins here. project_type = :stand_alone - # Set this to the root of your project when deployed: - http_path = "/" css_dir = "css" sass_dir = "sass" images_dir = "img" javascripts_dir = "js" + # Set this to the root of your project when deployed: + http_path = "/" output_style = :nested # To enable relative paths to assets via compass helper functions. Uncomment: # relative_assets = true CONFIG - Compass.configuration.parse_string(contents, "test_parse") + Compass.add_configuration(contents, "test_parse") assert_equal 'sass', Compass.configuration.sass_dir assert_equal 'css', Compass.configuration.css_dir assert_equal 'img', Compass.configuration.images_dir assert_equal 'js', Compass.configuration.javascripts_dir - expected_lines = contents.split("\n").map{|l|l.strip} + expected_lines = contents.string.split("\n").map{|l|l.strip} actual_lines = Compass.configuration.serialize.split("\n").map{|l|l.strip} assert_equal expected_lines, actual_lines end - def test_serialization_fails_with_asset_host_set - contents = <<-CONFIG + def test_serialization_warns_with_asset_host_set + contents = StringIO.new(<<-CONFIG) asset_host do |path| "http://example.com" end CONFIG - Compass.configuration.parse_string(contents, "test_serialization_fails_with_asset_host_set") + Compass.add_configuration(contents, "test_serialization_warns_with_asset_host_set") - assert_raise Compass::Error do + warning = capture_warning do Compass.configuration.serialize end + assert_equal "WARNING: asset_host is code and cannot be written to a file. You'll need to copy it yourself.\n", warning end - def test_serialization_fails_with_asset_cache_buster_set - contents = <<-CONFIG + def test_serialization_warns_with_asset_cache_buster_set + contents = StringIO.new(<<-CONFIG) asset_cache_buster do |path| "http://example.com" end CONFIG - Compass.configuration.parse_string(contents, "test_serialization_fails_with_asset_cache_buster_set") + Compass.add_configuration(contents, "test_serialization_warns_with_asset_cache_buster_set") - assert_raise Compass::Error do + warning = capture_warning do Compass.configuration.serialize end + assert_equal "WARNING: asset_cache_buster is code and cannot be written to a file. You'll need to copy it yourself.\n", warning end def test_additional_import_paths - contents = <<-CONFIG + contents = StringIO.new(<<-CONFIG) http_path = "/" project_path = "/home/chris/my_compass_project" css_dir = "css" @@ -73,7 +77,7 @@ class ConfigurationTest < Test::Unit::TestCase add_import_path "/path/to/my/framework" CONFIG - Compass.configuration.parse_string(contents, "test_additional_import_paths") + Compass.add_configuration(contents, "test_additional_import_paths") assert Compass.configuration.to_sass_engine_options[:load_paths].include?("/home/chris/my_compass_project/../foo") assert Compass.configuration.to_sass_engine_options[:load_paths].include?("/path/to/my/framework"), Compass.configuration.to_sass_engine_options[:load_paths].inspect @@ -83,30 +87,38 @@ class ConfigurationTest < Test::Unit::TestCase expected_serialization = < 'bar'} CONFIG - Compass.configuration.parse_string(contents, "test_sass_options") + Compass.add_configuration(contents, "test_sass_options") assert_equal 'bar', Compass.configuration.to_sass_engine_options[:foo] assert_equal 'bar', Compass.configuration.to_sass_plugin_options[:foo] expected_serialization = <"bar"} diff --git a/test/io_helper.rb b/test/io_helper.rb new file mode 100644 index 00000000..499dec93 --- /dev/null +++ b/test/io_helper.rb @@ -0,0 +1,19 @@ +module Compass + module IoHelper + def capture_output + real_stdout, $stdout = $stdout, StringIO.new + yield + $stdout.string + ensure + $stdout = real_stdout + end + + def capture_warning + real_stderr, $stderr = $stderr, StringIO.new + yield + $stderr.string + ensure + $stderr = real_stderr + end + end +end \ No newline at end of file diff --git a/test/rails_integration_test.rb b/test/rails_integration_test.rb index df144c7a..9f689a1b 100644 --- a/test/rails_integration_test.rb +++ b/test/rails_integration_test.rb @@ -7,9 +7,10 @@ require 'timeout' class RailsIntegrationTest < Test::Unit::TestCase include Compass::TestCaseHelper include Compass::CommandLineHelper + include Compass::IoHelper def setup - Compass.configuration.reset! + Compass.reset_configuration! end def test_rails_install diff --git a/test/test_helper.rb b/test/test_helper.rb index 30ee1245..d731494b 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -18,4 +18,5 @@ require 'compass' require 'test/unit' require File.join(File.dirname(__FILE__), 'test_case_helper') +require File.join(File.dirname(__FILE__), 'io_helper') require File.join(File.dirname(__FILE__), 'command_line_helper')