haml 3.0.0.beta.1 → 3.0.0.beta.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of haml might be problematic. Click here for more details.

Files changed (67) hide show
  1. data/README.md +7 -7
  2. data/REMEMBER +2 -1
  3. data/Rakefile +6 -4
  4. data/VERSION +1 -1
  5. data/lib/haml/exec.rb +119 -24
  6. data/lib/haml/filters.rb +5 -1
  7. data/lib/haml/helpers.rb +4 -2
  8. data/lib/haml/helpers/action_view_mods.rb +2 -3
  9. data/lib/haml/precompiler.rb +1 -0
  10. data/lib/sass.rb +1 -1
  11. data/lib/sass/css.rb +25 -6
  12. data/lib/sass/engine.rb +23 -7
  13. data/lib/sass/environment.rb +47 -1
  14. data/lib/sass/files.rb +9 -10
  15. data/lib/sass/plugin.rb +6 -27
  16. data/lib/sass/plugin/merb.rb +1 -0
  17. data/lib/sass/plugin/rails.rb +1 -0
  18. data/lib/sass/plugin/staleness_checker.rb +123 -0
  19. data/lib/sass/script/bool.rb +1 -1
  20. data/lib/sass/script/color.rb +1 -1
  21. data/lib/sass/script/css_lexer.rb +1 -4
  22. data/lib/sass/script/funcall.rb +2 -2
  23. data/lib/sass/script/functions.rb +102 -7
  24. data/lib/sass/script/interpolation.rb +5 -5
  25. data/lib/sass/script/lexer.rb +10 -8
  26. data/lib/sass/script/literal.rb +11 -1
  27. data/lib/sass/script/node.rb +10 -1
  28. data/lib/sass/script/number.rb +25 -10
  29. data/lib/sass/script/operation.rb +7 -6
  30. data/lib/sass/script/parser.rb +37 -28
  31. data/lib/sass/script/string.rb +12 -7
  32. data/lib/sass/script/string_interpolation.rb +70 -0
  33. data/lib/sass/script/unary_operation.rb +3 -3
  34. data/lib/sass/script/variable.rb +2 -2
  35. data/lib/sass/scss/css_parser.rb +1 -0
  36. data/lib/sass/scss/parser.rb +58 -44
  37. data/lib/sass/scss/rx.rb +1 -0
  38. data/lib/sass/tree/comment_node.rb +3 -2
  39. data/lib/sass/tree/debug_node.rb +1 -1
  40. data/lib/sass/tree/for_node.rb +1 -1
  41. data/lib/sass/tree/if_node.rb +1 -1
  42. data/lib/sass/tree/import_node.rb +3 -0
  43. data/lib/sass/tree/mixin_def_node.rb +3 -3
  44. data/lib/sass/tree/mixin_node.rb +7 -3
  45. data/lib/sass/tree/node.rb +8 -0
  46. data/lib/sass/tree/prop_node.rb +21 -16
  47. data/lib/sass/tree/rule_node.rb +3 -3
  48. data/lib/sass/tree/variable_node.rb +1 -1
  49. data/lib/sass/tree/warn_node.rb +41 -0
  50. data/lib/sass/tree/while_node.rb +1 -1
  51. data/test/haml/engine_test.rb +9 -0
  52. data/test/sass/conversion_test.rb +127 -15
  53. data/test/sass/css2sass_test.rb +34 -3
  54. data/test/sass/engine_test.rb +82 -5
  55. data/test/sass/functions_test.rb +31 -0
  56. data/test/sass/plugin_test.rb +25 -24
  57. data/test/sass/results/script.css +4 -4
  58. data/test/sass/results/warn.css +0 -0
  59. data/test/sass/results/warn_imported.css +0 -0
  60. data/test/sass/script_conversion_test.rb +43 -1
  61. data/test/sass/script_test.rb +3 -3
  62. data/test/sass/scss/css_test.rb +46 -5
  63. data/test/sass/scss/scss_test.rb +72 -0
  64. data/test/sass/templates/import.sass +1 -1
  65. data/test/sass/templates/warn.sass +3 -0
  66. data/test/sass/templates/warn_imported.sass +4 -0
  67. metadata +10 -3
@@ -23,7 +23,7 @@ module Sass
23
23
  @vars = {}
24
24
  @mixins = {}
25
25
  @parent = parent
26
-
26
+ @stack = [] unless parent
27
27
  set_var("important", Script::String.new("!important")) unless @parent
28
28
  end
29
29
 
@@ -35,6 +35,52 @@ module Sass
35
35
  @options || (parent && parent.options) || {}
36
36
  end
37
37
 
38
+ # Push a new stack frame onto the mixin/include stack.
39
+ #
40
+ # @param frame_info [{Symbol => Object}]
41
+ # Frame information has the following keys:
42
+ #
43
+ # `:filename`
44
+ # : The name of the file in which the lexical scope changed.
45
+ #
46
+ # `:mixin`
47
+ # : The name of the mixin in which the lexical scope changed,
48
+ # or `nil` if it wasn't within in a mixin.
49
+ #
50
+ # `:line`
51
+ # : The line of the file on which the lexical scope changed. Never nil.
52
+ def push_frame(frame_info)
53
+ if stack.last && stack.last[:prepared]
54
+ stack.last.delete(:prepared)
55
+ stack.last.merge!(frame_info)
56
+ else
57
+ stack.push(frame_info)
58
+ end
59
+ end
60
+
61
+ # Like \{#push\_frame}, but next time a stack frame is pushed,
62
+ # it will be merged with this frame.
63
+ #
64
+ # @param frame_info [{Symbol => Object}] Same as for \{#push\_frame}.
65
+ def prepare_frame(frame_info)
66
+ push_frame(frame_info.merge(:prepared => true))
67
+ end
68
+
69
+ # Pop a stack frame from the mixin/include stack.
70
+ def pop_frame
71
+ stack.pop if stack.last[:prepared]
72
+ stack.pop
73
+ end
74
+
75
+ # A list of stack frames in the mixin/include stack.
76
+ # The last element in the list is the most deeply-nested frame.
77
+ #
78
+ # @return [Array<{Symbol => Object}>] The stack frames,
79
+ # of the form passed to \{#push\_frame}.
80
+ def stack
81
+ @stack ||= @parent.stack
82
+ end
83
+
38
84
  class << self
39
85
  private
40
86
 
@@ -76,8 +76,11 @@ module Sass
76
76
  return filename
77
77
  end
78
78
 
79
- new_filename = find_full_path("#{filename}.sass", load_paths) unless was_scss
80
- new_filename ||= find_full_path("#{filename}.scss", load_paths) unless was_sass
79
+ new_filename = nil
80
+ load_paths.each do |load_path|
81
+ new_filename ||= find_full_path("#{filename}.sass", load_path) unless was_scss
82
+ new_filename ||= find_full_path("#{filename}.scss", load_path) unless was_sass
83
+ end
81
84
 
82
85
  return new_filename if new_filename
83
86
  unless was_sass || was_scss
@@ -134,7 +137,7 @@ END
134
137
  end
135
138
  end
136
139
 
137
- def find_full_path(filename, load_paths)
140
+ def find_full_path(filename, load_path)
138
141
  partial_name = File.join(File.dirname(filename), "_#{File.basename(filename)}")
139
142
 
140
143
  if Pathname.new(filename).absolute?
@@ -144,13 +147,9 @@ END
144
147
  return nil
145
148
  end
146
149
 
147
- load_paths.each do |path|
148
- [partial_name, filename].each do |name|
149
- full_path = File.join(path, name)
150
- if File.readable?(full_path)
151
- return full_path
152
- end
153
- end
150
+ [partial_name, filename].each do |name|
151
+ full_path = File.join(load_path, name)
152
+ return full_path if File.readable?(full_path)
154
153
  end
155
154
  nil
156
155
  end
@@ -3,6 +3,7 @@ require 'rbconfig'
3
3
 
4
4
  require 'sass'
5
5
  require 'sass/callbacks'
6
+ require 'sass/plugin/staleness_checker'
6
7
 
7
8
  module Sass
8
9
  # This module handles the compilation of Sass/SCSS files.
@@ -211,6 +212,8 @@ module Sass
211
212
  individual_files.each {|t, c| update_stylesheet(t, c)}
212
213
 
213
214
  @checked_for_updates = true
215
+ staleness_checker = StalenessChecker.new
216
+
214
217
  template_locations.zip(css_locations).each do |template_location, css_location|
215
218
 
216
219
  Dir.glob(File.join(template_location, "**", "*.s[ca]ss")).each do |file|
@@ -219,7 +222,7 @@ module Sass
219
222
  css = css_filename(name, css_location)
220
223
 
221
224
  next if forbid_update?(name)
222
- if options[:always_update] || stylesheet_needs_update?(css, file)
225
+ if options[:always_update] || staleness_checker.stylesheet_needs_update?(css, file)
223
226
  update_stylesheet file, css
224
227
  else
225
228
  run_not_updating_stylesheet file, css
@@ -374,33 +377,9 @@ module Sass
374
377
  name.sub(/^.*\//, '')[0] == ?_
375
378
  end
376
379
 
380
+ # Compass expects this to exist
377
381
  def stylesheet_needs_update?(css_file, template_file)
378
- return true unless File.exists?(css_file) && File.exists?(template_file)
379
-
380
- css_mtime = File.mtime(css_file)
381
- File.mtime(template_file) > css_mtime ||
382
- dependencies(template_file).any?(&dependency_updated?(css_mtime))
383
- end
384
-
385
- def dependency_updated?(css_mtime)
386
- lambda do |dep|
387
- begin
388
- File.mtime(dep) > css_mtime ||
389
- dependencies(dep).any?(&dependency_updated?(css_mtime))
390
- rescue Sass::SyntaxError
391
- # If there's an error finding depenencies, default to recompiling.
392
- true
393
- end
394
- end
395
- end
396
-
397
- def dependencies(filename)
398
- Files.tree_for(filename, engine_options).select {|n| n.is_a?(Tree::ImportNode)}.map do |n|
399
- next if n.full_filename =~ /\.css$/
400
- n.full_filename
401
- end.compact
402
- rescue Sass::SyntaxError => e
403
- [] # If the file has an error, we assume it has no dependencies
382
+ StalenessChecker.stylesheet_needs_update?(css_file, template_file)
404
383
  end
405
384
  end
406
385
  end
@@ -14,6 +14,7 @@ unless defined?(Sass::MERB_LOADED)
14
14
  :css_location => root + '/public/stylesheets',
15
15
  :cache_location => root + '/tmp/sass-cache',
16
16
  :always_check => env != "production",
17
+ :quiet => env != "production",
17
18
  :full_exception => env != "production")
18
19
  config = Merb::Plugins.config[:sass] || Merb::Plugins.config["sass"] || {}
19
20
 
@@ -5,6 +5,7 @@ unless defined?(Sass::RAILS_LOADED)
5
5
  :css_location => Haml::Util.rails_root + '/public/stylesheets',
6
6
  :cache_location => Haml::Util.rails_root + '/tmp/sass-cache',
7
7
  :always_check => Haml::Util.rails_env != "production",
8
+ :quiet => Haml::Util.rails_env != "production",
8
9
  :full_exception => Haml::Util.rails_env != "production")
9
10
 
10
11
  if defined?(Rails.configuration) && defined?(Rails.configuration.middleware)
@@ -0,0 +1,123 @@
1
+ module Sass
2
+ module Plugin
3
+ # The class handles `.s[ca]ss` file staleness checks via their mtime timestamps.
4
+ #
5
+ # To speed things up two level of caches are employed:
6
+ #
7
+ # * A class-level dependency cache which stores @import paths for each file.
8
+ # This is a long-lived cache that is reused by every StalenessChecker instance.
9
+ # * Two short-lived instance-level caches, one for file mtimes
10
+ # and one for whether a file is stale during this particular run.
11
+ # These are only used by a single StalenessChecker instance.
12
+ #
13
+ # Usage:
14
+ #
15
+ # * For a one-off staleness check of a single `.s[ca]ss` file,
16
+ # the class-level {stylesheet_needs_update?} method
17
+ # should be used.
18
+ # * For a series of staleness checks (e.g. checking all files for staleness)
19
+ # a StalenessChecker instance should be created,
20
+ # and the instance-level \{#stylesheet\_needs\_update?} method should be used.
21
+ # the caches should make the whole process significantly faster.
22
+ # *WARNING*: It is important not to retain the instance for too long,
23
+ # as its instance-level caches are never explicitly expired.
24
+ class StalenessChecker
25
+ @dependencies_cache = {}
26
+
27
+ class << self
28
+ # @private
29
+ attr_accessor :dependencies_cache
30
+ end
31
+
32
+ # Creates a new StalenessChecker
33
+ # for checking the staleness of several stylesheets at once.
34
+ def initialize
35
+ @dependencies = self.class.dependencies_cache
36
+
37
+ # Entries in the following instance-level caches are never explicitly expired.
38
+ # Instead they are supposed to automaticaly go out of scope when a series of staleness checks
39
+ # (this instance of StalenessChecker was created for) is finished.
40
+ @mtimes, @dependencies_stale = {}, {}
41
+ end
42
+
43
+ # Returns whether or not a given CSS file is out of date
44
+ # and needs to be regenerated.
45
+ #
46
+ # @param css_file [String] The location of the CSS file to check.
47
+ # @param template_file [String] The location of the Sass or SCSS template
48
+ # that is compiled to `css_file`.
49
+ def stylesheet_needs_update?(css_file, template_file)
50
+ template_file = File.expand_path(template_file)
51
+
52
+ unless File.exists?(css_file) && File.exists?(template_file)
53
+ @dependencies.delete(template_file)
54
+ true
55
+ else
56
+ css_mtime = mtime(css_file)
57
+ mtime(template_file) > css_mtime || dependencies_stale?(template_file, css_mtime)
58
+ end
59
+ end
60
+
61
+ # Returns whether or not a given CSS file is out of date
62
+ # and needs to be regenerated.
63
+ #
64
+ # The distinction between this method and the instance-level \{#stylesheet\_needs\_update?}
65
+ # is that the instance method preserves mtime and stale-dependency caches,
66
+ # so it's better to use when checking multiple stylesheets at once.
67
+ #
68
+ # @param css_file [String] The location of the CSS file to check.
69
+ # @param template_file [String] The location of the Sass or SCSS template
70
+ # that is compiled to `css_file`.
71
+ def self.stylesheet_needs_update?(css_file, template_file)
72
+ new.stylesheet_needs_update?(css_file, template_file)
73
+ end
74
+
75
+ private
76
+
77
+ def dependencies_stale?(template_file, css_mtime)
78
+ timestamps = @dependencies_stale[template_file] ||= {}
79
+ timestamps.each_pair do |checked_css_mtime, is_stale|
80
+ if checked_css_mtime <= css_mtime && !is_stale
81
+ return false
82
+ elsif checked_css_mtime > css_mtime && is_stale
83
+ return true
84
+ end
85
+ end
86
+ timestamps[css_mtime] = dependencies(template_file).any?(&dependency_updated?(css_mtime))
87
+ end
88
+
89
+ def mtime(filename)
90
+ @mtimes[filename] ||= File.mtime(filename)
91
+ end
92
+
93
+ def dependencies(filename)
94
+ stored_mtime, dependencies = @dependencies[filename]
95
+
96
+ if !stored_mtime || stored_mtime < mtime(filename)
97
+ @dependencies[filename] = [mtime(filename), dependencies = compute_dependencies(filename)]
98
+ end
99
+
100
+ dependencies
101
+ end
102
+
103
+ def dependency_updated?(css_mtime)
104
+ lambda do |dep|
105
+ begin
106
+ mtime(dep) > css_mtime || dependencies_stale?(dep, css_mtime)
107
+ rescue Sass::SyntaxError
108
+ # If there's an error finding depenencies, default to recompiling.
109
+ true
110
+ end
111
+ end
112
+ end
113
+
114
+ def compute_dependencies(filename)
115
+ Files.tree_for(filename, Plugin.engine_options).grep(Tree::ImportNode) do |n|
116
+ File.expand_path(n.full_filename) unless n.full_filename =~ /\.css$/
117
+ end.compact
118
+ rescue Sass::SyntaxError => e
119
+ [] # If the file has an error, we assume it has no dependencies
120
+ end
121
+ end
122
+ end
123
+ end
@@ -10,7 +10,7 @@ module Sass::Script
10
10
  alias_method :to_bool, :value
11
11
 
12
12
  # @return [String] "true" or "false"
13
- def to_s
13
+ def to_s(opts = {})
14
14
  @value.to_s
15
15
  end
16
16
  alias_method :to_sass, :to_s
@@ -375,7 +375,7 @@ END
375
375
  # but if the color has a name that's used instead.
376
376
  #
377
377
  # @return [String] The string representation
378
- def to_s
378
+ def to_s(opts = {})
379
379
  return rgba_str if alpha?
380
380
  return smallest if options[:style] == :compressed
381
381
  return HTML4_COLORS_REVERSE[rgb] if HTML4_COLORS_REVERSE[rgb]
@@ -7,10 +7,7 @@ module Sass
7
7
 
8
8
  def string(*args)
9
9
  return unless scan(STRING)
10
- str = (@scanner[1] || @scanner[2]).
11
- gsub(/\\([^0-9a-f])/, '\1').
12
- gsub(/\\([0-9a-f]{1,4})/, "\\\\\\1")
13
- [:string, Script::String.new(str, :string)]
10
+ [:string, Script::String.new((@scanner[1] || @scanner[2]).gsub(/\\(['"])/, '\1'), :string)]
14
11
  end
15
12
 
16
13
  def important
@@ -31,8 +31,8 @@ module Sass
31
31
  end
32
32
 
33
33
  # @see Node#to_sass
34
- def to_sass
35
- "#{name}(#{args.map {|a| a.to_sass}.join(', ')})"
34
+ def to_sass(opts = {})
35
+ "#{dasherize(name, opts)}(#{args.map {|a| a.to_sass(opts)}.join(', ')})"
36
36
  end
37
37
 
38
38
  # Returns the arguments to the function.
@@ -105,6 +105,20 @@ module Sass::Script
105
105
  # \{#abs}
106
106
  # : Returns the absolute value of a number.
107
107
  #
108
+ # ## Introspection Functions
109
+ #
110
+ # \{#type_of}
111
+ # : Returns the type of a value.
112
+ #
113
+ # \{#unit}
114
+ # : Returns the units associated with a number.
115
+ #
116
+ # \{#unitless}
117
+ # : Returns whether a number has units or not.
118
+ #
119
+ # \{#comparable}
120
+ # : Returns whether two numbers can be added or compared.
121
+ #
108
122
  # These functions are described in more detail below.
109
123
  #
110
124
  # ## Adding Custom Functions
@@ -388,17 +402,38 @@ module Sass::Script
388
402
  Sass::Script::Number.new(color.lightness, ["%"])
389
403
  end
390
404
 
405
+ # Returns the alpha component (opacity) of a color.
406
+ # This is 1 unless otherwise specified.
407
+ #
408
+ # This function also supports the proprietary Microsoft
409
+ # `alpha(opacity=20)` syntax.
410
+ #
411
+ # @overload def alpha(color)
412
+ # @param color [Color]
413
+ # @return [Number]
414
+ # @raise [ArgumentError] If `color` isn't a color
415
+ def alpha(*args)
416
+ if args.all? do |a|
417
+ a.is_a?(Sass::Script::String) && a.type == :identifier &&
418
+ a.value =~ /^[a-zA-Z]+\s*=/
419
+ end
420
+ # Support the proprietary MS alpha() function
421
+ return Sass::Script::String.new("alpha(#{args.map {|a| a.to_s}.join(", ")})")
422
+ end
423
+
424
+ opacity(*args)
425
+ end
426
+
391
427
  # Returns the alpha component (opacity) of a color.
392
428
  # This is 1 unless otherwise specified.
393
429
  #
394
430
  # @param color [Color]
395
431
  # @return [Number]
396
432
  # @raise [ArgumentError] If `color` isn't a color
397
- def alpha(color)
433
+ def opacity(color)
398
434
  assert_type color, :Color
399
435
  Sass::Script::Number.new(color.alpha)
400
436
  end
401
- alias_method :opacity, :alpha
402
437
 
403
438
  # Makes a color more opaque.
404
439
  # Takes a color and an amount between 0 and 1,
@@ -648,6 +683,71 @@ module Sass::Script
648
683
  Sass::Script::String.new(str.value, :string)
649
684
  end
650
685
 
686
+ # Inspects the type of the argument, returning it as an unquoted string.
687
+ # For example:
688
+ #
689
+ # type-of(100px) => number
690
+ # type-of(asdf) => string
691
+ # type-of("asdf") => string
692
+ # type-of(true) => bool
693
+ # type-of(#fff) => color
694
+ # type-of(blue) => color
695
+ #
696
+ # @param obj [Literal] The object to inspect
697
+ # @return [String] The unquoted string name of the literal's type
698
+ def type_of(obj)
699
+ Sass::Script::String.new(obj.class.name.gsub(/Sass::Script::/,'').downcase)
700
+ end
701
+
702
+ # Inspects the unit of the number, returning it as a quoted string.
703
+ # Complex units are sorted in alphabetical order by numerator and denominator.
704
+ # For example:
705
+ #
706
+ # unit(100) => ""
707
+ # unit(100px) => "px"
708
+ # unit(3em) => "em"
709
+ # unit(10px * 5em) => "em*px"
710
+ # unit(10px * 5em / 30cm / 1rem) => "em*px/cm*rem"
711
+ #
712
+ # @param number [Literal] The number to inspect
713
+ # @return [String] The unit(s) of the number
714
+ # @raise [ArgumentError] if `number` isn't a number
715
+ def unit(number)
716
+ assert_type number, :Number
717
+ Sass::Script::String.new(number.unit_str, :string)
718
+ end
719
+
720
+ # Inspects the unit of the number, returning a boolean indicating if it is unitless.
721
+ # For example:
722
+ #
723
+ # unitless(100) => true
724
+ # unitless(100px) => false
725
+ #
726
+ # @param number [Literal] The number to inspect
727
+ # @return [Bool] Whether or not the number is unitless
728
+ # @raise [ArgumentError] if `number` isn't a number
729
+ def unitless(number)
730
+ assert_type number, :Number
731
+ Sass::Script::Bool.new(number.unitless?)
732
+ end
733
+
734
+ # Returns true if two numbers are similar enough to be added, subtracted, or compared.
735
+ # For example:
736
+ #
737
+ # comparable(2px, 1px) => true
738
+ # comparable(100px, 3em) => false
739
+ # comparable(10cm, 3mm) => true
740
+ #
741
+ # @param number1 [Number]
742
+ # @param number2 [Number]
743
+ # @return [Bool] indicating if the numbers can be compared.
744
+ # @raise [ArgumentError] if `number1` or `number2` aren't numbers
745
+ def comparable(number1, number2)
746
+ assert_type number1, :Number
747
+ assert_type number2, :Number
748
+ Sass::Script::Bool.new(number1.comparable_to?(number2))
749
+ end
750
+
651
751
  # Converts a decimal number to a percentage.
652
752
  # For example:
653
753
  #
@@ -715,11 +815,6 @@ module Sass::Script
715
815
  numeric_transformation(value) {|n| n.abs}
716
816
  end
717
817
 
718
- def unquote(value)
719
- assert_type value, :String
720
- Sass::Script::String.new(value.value)
721
- end
722
-
723
818
  private
724
819
 
725
820
  # This method implements the pattern of transforming a numeric value into