haml 2.0.10 → 2.2.0

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 (107) hide show
  1. data/.yardopts +5 -0
  2. data/MIT-LICENSE +1 -1
  3. data/README.md +347 -0
  4. data/Rakefile +124 -19
  5. data/VERSION +1 -1
  6. data/VERSION_NAME +1 -0
  7. data/extra/haml-mode.el +397 -78
  8. data/extra/sass-mode.el +148 -36
  9. data/extra/update_watch.rb +13 -0
  10. data/lib/haml.rb +15 -993
  11. data/lib/haml/buffer.rb +131 -84
  12. data/lib/haml/engine.rb +129 -97
  13. data/lib/haml/error.rb +7 -7
  14. data/lib/haml/exec.rb +127 -42
  15. data/lib/haml/filters.rb +107 -42
  16. data/lib/haml/helpers.rb +210 -156
  17. data/lib/haml/helpers/action_view_extensions.rb +34 -39
  18. data/lib/haml/helpers/action_view_mods.rb +132 -139
  19. data/lib/haml/html.rb +77 -65
  20. data/lib/haml/precompiler.rb +404 -213
  21. data/lib/haml/shared.rb +78 -0
  22. data/lib/haml/template.rb +14 -14
  23. data/lib/haml/template/patch.rb +2 -2
  24. data/lib/haml/template/plugin.rb +2 -3
  25. data/lib/haml/util.rb +211 -6
  26. data/lib/haml/version.rb +30 -13
  27. data/lib/sass.rb +7 -856
  28. data/lib/sass/css.rb +169 -161
  29. data/lib/sass/engine.rb +344 -328
  30. data/lib/sass/environment.rb +79 -0
  31. data/lib/sass/error.rb +33 -11
  32. data/lib/sass/files.rb +139 -0
  33. data/lib/sass/plugin.rb +160 -117
  34. data/lib/sass/plugin/merb.rb +7 -6
  35. data/lib/sass/plugin/rails.rb +5 -6
  36. data/lib/sass/repl.rb +58 -0
  37. data/lib/sass/script.rb +59 -0
  38. data/lib/sass/script/bool.rb +17 -0
  39. data/lib/sass/script/color.rb +183 -0
  40. data/lib/sass/script/funcall.rb +50 -0
  41. data/lib/sass/script/functions.rb +198 -0
  42. data/lib/sass/script/lexer.rb +178 -0
  43. data/lib/sass/script/literal.rb +177 -0
  44. data/lib/sass/script/node.rb +14 -0
  45. data/lib/sass/script/number.rb +381 -0
  46. data/lib/sass/script/operation.rb +45 -0
  47. data/lib/sass/script/parser.rb +172 -0
  48. data/lib/sass/script/string.rb +12 -0
  49. data/lib/sass/script/unary_operation.rb +34 -0
  50. data/lib/sass/script/variable.rb +31 -0
  51. data/lib/sass/tree/comment_node.rb +73 -10
  52. data/lib/sass/tree/debug_node.rb +30 -0
  53. data/lib/sass/tree/directive_node.rb +42 -17
  54. data/lib/sass/tree/file_node.rb +41 -0
  55. data/lib/sass/tree/for_node.rb +48 -0
  56. data/lib/sass/tree/if_node.rb +54 -0
  57. data/lib/sass/tree/mixin_def_node.rb +29 -0
  58. data/lib/sass/tree/mixin_node.rb +48 -0
  59. data/lib/sass/tree/node.rb +214 -11
  60. data/lib/sass/tree/prop_node.rb +109 -0
  61. data/lib/sass/tree/rule_node.rb +178 -51
  62. data/lib/sass/tree/variable_node.rb +34 -0
  63. data/lib/sass/tree/while_node.rb +31 -0
  64. data/test/haml/engine_test.rb +331 -36
  65. data/test/haml/helper_test.rb +12 -1
  66. data/test/haml/results/content_for_layout.xhtml +0 -3
  67. data/test/haml/results/filters.xhtml +2 -0
  68. data/test/haml/results/list.xhtml +1 -1
  69. data/test/haml/template_test.rb +7 -2
  70. data/test/haml/templates/content_for_layout.haml +0 -2
  71. data/test/haml/templates/list.haml +1 -1
  72. data/test/haml/util_test.rb +92 -0
  73. data/test/sass/css2sass_test.rb +69 -24
  74. data/test/sass/engine_test.rb +586 -64
  75. data/test/sass/functions_test.rb +125 -0
  76. data/test/sass/more_results/more1.css +9 -0
  77. data/test/sass/more_results/more1_with_line_comments.css +26 -0
  78. data/test/sass/more_results/more_import.css +29 -0
  79. data/test/sass/more_templates/_more_partial.sass +2 -0
  80. data/test/sass/more_templates/more1.sass +23 -0
  81. data/test/sass/more_templates/more_import.sass +11 -0
  82. data/test/sass/plugin_test.rb +81 -28
  83. data/test/sass/results/line_numbers.css +49 -0
  84. data/test/sass/results/{constants.css → script.css} +4 -4
  85. data/test/sass/results/subdir/subdir.css +2 -0
  86. data/test/sass/results/units.css +11 -0
  87. data/test/sass/script_test.rb +258 -0
  88. data/test/sass/templates/import.sass +1 -1
  89. data/test/sass/templates/importee.sass +7 -2
  90. data/test/sass/templates/line_numbers.sass +13 -0
  91. data/test/sass/templates/{constants.sass → script.sass} +11 -10
  92. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
  93. data/test/sass/templates/subdir/subdir.sass +2 -2
  94. data/test/sass/templates/units.sass +11 -0
  95. data/test/test_helper.rb +14 -0
  96. metadata +77 -19
  97. data/FAQ +0 -138
  98. data/README.rdoc +0 -319
  99. data/lib/sass/constant.rb +0 -216
  100. data/lib/sass/constant/color.rb +0 -101
  101. data/lib/sass/constant/literal.rb +0 -54
  102. data/lib/sass/constant/nil.rb +0 -9
  103. data/lib/sass/constant/number.rb +0 -87
  104. data/lib/sass/constant/operation.rb +0 -30
  105. data/lib/sass/constant/string.rb +0 -22
  106. data/lib/sass/tree/attr_node.rb +0 -57
  107. data/lib/sass/tree/value_node.rb +0 -20
@@ -0,0 +1,178 @@
1
+ require 'strscan'
2
+
3
+ module Sass
4
+ module Script
5
+ # The lexical analyzer for SassScript.
6
+ # It takes a raw string and converts it to individual tokens
7
+ # that are easier to parse.
8
+ class Lexer
9
+ # A struct containing information about an individual token.
10
+ #
11
+ # `type`: [{Symbol}]
12
+ # : The type of token.
13
+ #
14
+ # `value`: [{Object}]
15
+ # : The Ruby object corresponding to the value of the token.
16
+ #
17
+ # `line`: [{Fixnum}]
18
+ # : The line of the source file on which the token appears.
19
+ #
20
+ # `offset`: [{Fixnum}]
21
+ # : The number of bytes into the line the SassScript token appeared.
22
+ Token = Struct.new(:type, :value, :line, :offset)
23
+
24
+ # A hash from operator strings to the corresponding token types.
25
+ OPERATORS = {
26
+ '+' => :plus,
27
+ '-' => :minus,
28
+ '*' => :times,
29
+ '/' => :div,
30
+ '%' => :mod,
31
+ '(' => :lparen,
32
+ ')' => :rparen,
33
+ ',' => :comma,
34
+ 'and' => :and,
35
+ 'or' => :or,
36
+ 'not' => :not,
37
+ '==' => :eq,
38
+ '!=' => :neq,
39
+ '>=' => :gte,
40
+ '<=' => :lte,
41
+ '>' => :gt,
42
+ '<' => :lt,
43
+ '#{' => :begin_interpolation,
44
+ '}' => :end_interpolation,
45
+ }
46
+
47
+ # A list of operator strings ordered with longer names first
48
+ # so that `>` and `<` don't clobber `>=` and `<=`.
49
+ OP_NAMES = OPERATORS.keys.sort_by {|o| -o.size}
50
+
51
+ # A hash of regular expressions that are used for tokenizing.
52
+ REGULAR_EXPRESSIONS = {
53
+ :whitespace => /\s*/,
54
+ :variable => /!(\w+)/,
55
+ :ident => /(\\.|\#\{|[^\s\\+\-*\/%(),=!])+/,
56
+ :string_end => /((?:\\.|\#[^{]|[^"\\#])*)(?:"|(?=#\{))/,
57
+ :number => /(-)?(?:(\d*\.\d+)|(\d+))([a-zA-Z%]+)?/,
58
+ :color => /\##{"([0-9a-fA-F]{1,2})" * 3}|(#{Color::HTML4_COLORS.keys.join("|")})/,
59
+ :bool => /(true|false)\b/,
60
+ :op => %r{(#{Regexp.union(*OP_NAMES.map{|s| Regexp.new(Regexp.escape(s) + (s =~ /\w$/ ? '(?:\b|$)' : ''))})})}
61
+ }
62
+
63
+ # @param str [String, StringScanner] The source text to lex
64
+ # @param line [Fixnum] The line on which the SassScript appears.
65
+ # Used for error reporting
66
+ # @param offset [Fixnum] The number of characters in on which the SassScript appears.
67
+ # Used for error reporting
68
+ def initialize(str, line, offset)
69
+ @scanner = str.is_a?(StringScanner) ? str : StringScanner.new(str)
70
+ @line = line
71
+ @offset = offset
72
+ @prev = nil
73
+ end
74
+
75
+ # Moves the lexer forward one token.
76
+ #
77
+ # @return [Token] The token that was moved past
78
+ def next
79
+ @tok ||= read_token
80
+ @tok, tok = nil, @tok
81
+ @prev = tok
82
+ return tok
83
+ end
84
+
85
+ # Returns the next token without moving the lexer forward.
86
+ #
87
+ # @return [Token] The next token
88
+ def peek
89
+ @tok ||= read_token
90
+ end
91
+
92
+ # @return [Boolean] Whether or not there's more source text to lex.
93
+ def done?
94
+ whitespace unless after_interpolation?
95
+ @scanner.eos? && @tok.nil?
96
+ end
97
+
98
+ private
99
+
100
+ def read_token
101
+ return if done?
102
+
103
+ value = token
104
+ unless value
105
+ raise SyntaxError.new("Syntax error in '#{@scanner.string}' at character #{current_position}.")
106
+ end
107
+ Token.new(value.first, value.last, @line, last_match_position)
108
+ end
109
+
110
+ def whitespace
111
+ @scanner.scan(REGULAR_EXPRESSIONS[:whitespace])
112
+ end
113
+
114
+ def token
115
+ return string('') if after_interpolation?
116
+ variable || string || number || color || bool || op || ident
117
+ end
118
+
119
+ def variable
120
+ return unless @scanner.scan(REGULAR_EXPRESSIONS[:variable])
121
+ [:const, @scanner[1]]
122
+ end
123
+
124
+ def ident
125
+ return unless s = @scanner.scan(REGULAR_EXPRESSIONS[:ident])
126
+ [:ident, s.gsub(/\\(.)/, '\1')]
127
+ end
128
+
129
+ def string(start_char = '"')
130
+ return unless @scanner.scan(/#{start_char}#{REGULAR_EXPRESSIONS[:string_end]}/)
131
+ [:string, Script::String.new(@scanner[1].gsub(/\\([^0-9a-f])/, '\1').gsub(/\\([0-9a-f]{1,4})/, "\\\\\\1"))]
132
+ end
133
+
134
+ def begin_interpolation
135
+ @scanner.scan
136
+ end
137
+
138
+ def number
139
+ return unless @scanner.scan(REGULAR_EXPRESSIONS[:number])
140
+ value = @scanner[2] ? @scanner[2].to_f : @scanner[3].to_i
141
+ value = -value if @scanner[1]
142
+ [:number, Script::Number.new(value, Array(@scanner[4]))]
143
+ end
144
+
145
+ def color
146
+ return unless @scanner.scan(REGULAR_EXPRESSIONS[:color])
147
+ value = if @scanner[4]
148
+ color = Color::HTML4_COLORS[@scanner[4].downcase]
149
+ else
150
+ (1..3).map {|i| @scanner[i]}.map {|num| num.ljust(2, num).to_i(16)}
151
+ end
152
+ [:color, Script::Color.new(value)]
153
+ end
154
+
155
+ def bool
156
+ return unless s = @scanner.scan(REGULAR_EXPRESSIONS[:bool])
157
+ [:bool, Script::Bool.new(s == 'true')]
158
+ end
159
+
160
+ def op
161
+ return unless op = @scanner.scan(REGULAR_EXPRESSIONS[:op])
162
+ [OPERATORS[op]]
163
+ end
164
+
165
+ def current_position
166
+ @offset + @scanner.pos + 1
167
+ end
168
+
169
+ def last_match_position
170
+ current_position - @scanner.matched_size
171
+ end
172
+
173
+ def after_interpolation?
174
+ @prev && @prev.type == :end_interpolation
175
+ end
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,177 @@
1
+ module Sass::Script
2
+ # The abstract superclass for SassScript objects.
3
+ #
4
+ # Many of these methods, especially the ones that correspond to SassScript operations,
5
+ # are designed to be overridden by subclasses which may change the semantics somewhat.
6
+ # The operations listed here are just the defaults.
7
+ class Literal < Node
8
+ require 'sass/script/string'
9
+ require 'sass/script/number'
10
+ require 'sass/script/color'
11
+ require 'sass/script/bool'
12
+
13
+ # Returns the Ruby value of the literal.
14
+ # The type of this value varies based on the subclass.
15
+ #
16
+ # @return [Object]
17
+ attr_reader :value
18
+
19
+ # Creates a new literal.
20
+ #
21
+ # @param value [Object] The object for \{#value}
22
+ def initialize(value = nil)
23
+ @value = value
24
+ end
25
+
26
+ # Evaluates the literal.
27
+ #
28
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
29
+ # @return [Literal] This literal
30
+ def perform(environment)
31
+ self
32
+ end
33
+
34
+ # The SassScript `and` operation.
35
+ #
36
+ # @param other [Literal] The right-hand side of the operator
37
+ # @return [Literal] The result of a logical and:
38
+ # `other` if this literal isn't a false {Bool},
39
+ # and this literal otherwise
40
+ def and(other)
41
+ to_bool ? other : self
42
+ end
43
+
44
+ # The SassScript `or` operation.
45
+ #
46
+ # @param other [Literal] The right-hand side of the operator
47
+ # @return [Literal] The result of the logical or:
48
+ # this literal if it isn't a false {Bool},
49
+ # and `other` otherwise
50
+ def or(other)
51
+ to_bool ? self : other
52
+ end
53
+
54
+ # The SassScript `==` operation.
55
+ # **Note that this returns a {Sass::Script::Bool} object,
56
+ # not a Ruby boolean**.
57
+ #
58
+ # @param other [Literal] The right-hand side of the operator
59
+ # @return [Bool] True if this literal is the same as the other,
60
+ # false otherwise
61
+ def eq(other)
62
+ Sass::Script::Bool.new(self.class == other.class && self.value == other.value)
63
+ end
64
+
65
+ # The SassScript `!=` operation.
66
+ # **Note that this returns a {Sass::Script::Bool} object,
67
+ # not a Ruby boolean**.
68
+ #
69
+ # @param other [Literal] The right-hand side of the operator
70
+ # @return [Bool] False if this literal is the same as the other,
71
+ # true otherwise
72
+ def neq(other)
73
+ Sass::Script::Bool.new(!eq(other).to_bool)
74
+ end
75
+
76
+ # The SassScript `==` operation.
77
+ # **Note that this returns a {Sass::Script::Bool} object,
78
+ # not a Ruby boolean**.
79
+ #
80
+ # @param other [Literal] The right-hand side of the operator
81
+ # @return [Bool] True if this literal is the same as the other,
82
+ # false otherwise
83
+ def unary_not
84
+ Sass::Script::Bool.new(!to_bool)
85
+ end
86
+
87
+ # The SassScript default operation (e.g. `!a !b`, `"foo" "bar"`).
88
+ #
89
+ # @param other [Literal] The right-hand side of the operator
90
+ # @return [Script::String] A string containing both literals
91
+ # separated by a space
92
+ def concat(other)
93
+ Sass::Script::String.new("#{self.to_s} #{other.to_s}")
94
+ end
95
+
96
+ # The SassScript `,` operation (e.g. `!a, !b`, `"foo", "bar"`).
97
+ #
98
+ # @param other [Literal] The right-hand side of the operator
99
+ # @return [Script::String] A string containing both literals
100
+ # separated by `", "`
101
+ def comma(other)
102
+ Sass::Script::String.new("#{self.to_s}, #{other.to_s}")
103
+ end
104
+
105
+ # The SassScript `+` operation.
106
+ #
107
+ # @param other [Literal] The right-hand side of the operator
108
+ # @return [Script::String] A string containing both literals
109
+ # without any separation
110
+ def plus(other)
111
+ Sass::Script::String.new(self.to_s + other.to_s)
112
+ end
113
+
114
+ # The SassScript `-` operation.
115
+ #
116
+ # @param other [Literal] The right-hand side of the operator
117
+ # @return [Script::String] A string containing both literals
118
+ # separated by `"-"`
119
+ def minus(other)
120
+ Sass::Script::String.new("#{self.to_s}-#{other.to_s}")
121
+ end
122
+
123
+ # The SassScript `/` operation.
124
+ #
125
+ # @param other [Literal] The right-hand side of the operator
126
+ # @return [Script::String] A string containing both literals
127
+ # separated by `"/"`
128
+ def div(other)
129
+ Sass::Script::String.new("#{self.to_s}/#{other.to_s}")
130
+ end
131
+
132
+ # The SassScript unary `-` operation (e.g. `-!a`).
133
+ #
134
+ # @param other [Literal] The right-hand side of the operator
135
+ # @return [Script::String] A string containing the literal
136
+ # preceded by `"-"`
137
+ def unary_minus
138
+ Sass::Script::String.new("-#{self.to_s}")
139
+ end
140
+
141
+ # The SassScript unary `/` operation (e.g. `/!a`).
142
+ #
143
+ # @param other [Literal] The right-hand side of the operator
144
+ # @return [Script::String] A string containing the literal
145
+ # preceded by `"/"`
146
+ def unary_div
147
+ Sass::Script::String.new("/#{self.to_s}")
148
+ end
149
+
150
+ # @return [String] A readable representation of the literal
151
+ def inspect
152
+ value.inspect
153
+ end
154
+
155
+ # @return [Boolean] `true` (the Ruby boolean value)
156
+ def to_bool
157
+ true
158
+ end
159
+
160
+ # Compares this object with another.
161
+ #
162
+ # @param other [Object] The object to compare with
163
+ # @return [Boolean] Whether or not this literal is equivalent to `other`
164
+ def ==(other)
165
+ eq(other).to_bool
166
+ end
167
+
168
+ # @return [Fixnum] The integer value of this literal
169
+ # @raise [Sass::SyntaxError] if this literal isn't an integer
170
+ def to_i
171
+ raise Sass::SyntaxError.new("#{self.inspect} is not an integer.")
172
+ end
173
+
174
+ # @raise [Sass::SyntaxError] if this literal isn't an integer
175
+ def assert_int!; to_i; end
176
+ end
177
+ end
@@ -0,0 +1,14 @@
1
+ module Sass::Script
2
+ # The abstract superclass for SassScript parse tree nodes.
3
+ #
4
+ # Use \{#perform} to evaluate a parse tree.
5
+ class Node
6
+ # Evaluates the node.
7
+ #
8
+ # @param environment [Sass::Environment] The environment in which to evaluate the SassScript
9
+ # @return [Literal] The SassScript object that is the value of the SassScript
10
+ def perform(environment)
11
+ raise NotImplementedError.new("All subclasses of Sass::Script::Node must override #perform.")
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,381 @@
1
+ require 'sass/script/literal'
2
+
3
+ module Sass::Script
4
+ # A SassScript object representing a number.
5
+ # SassScript numbers can have decimal values,
6
+ # and can also have units.
7
+ # For example, `12`, `1px`, and `10.45em`
8
+ # are all valid values.
9
+ #
10
+ # Numbers can also have more complex units, such as `1px*em/in`.
11
+ # These cannot be inputted directly in Sass code at the moment.
12
+ class Number < Literal
13
+ # The Ruby value of the number.
14
+ #
15
+ # @return [Numeric]
16
+ attr_reader :value
17
+
18
+ # A list of units in the numerator of the number.
19
+ # For example, `1px*em/in*cm` would return `["px", "em"]`
20
+ # @return [Array<String>]
21
+ attr_reader :numerator_units
22
+
23
+ # A list of units in the denominator of the number.
24
+ # For example, `1px*em/in*cm` would return `["in", "cm"]`
25
+ # @return [Array<String>]
26
+ attr_reader :denominator_units
27
+
28
+ # The precision with which numbers will be printed to CSS files.
29
+ # For example, if this is `1000.0`,
30
+ # `3.1415926` will be printed as `3.142`.
31
+ PRECISION = 1000.0
32
+
33
+ # @param value [Numeric] The value of the number
34
+ # @param numerator_units [Array<String>] See \{#numerator\_units}
35
+ # @param denominator_units [Array<String>] See \{#denominator\_units}
36
+ def initialize(value, numerator_units = [], denominator_units = [])
37
+ super(value)
38
+ @numerator_units = numerator_units
39
+ @denominator_units = denominator_units
40
+ normalize!
41
+ end
42
+
43
+ # The SassScript `+` operation.
44
+ # Its functionality depends on the type of its argument:
45
+ #
46
+ # {Number}
47
+ # : Adds the two numbers together, converting units if possible.
48
+ #
49
+ # {Color}
50
+ # : Adds this number to each of the RGB color channels.
51
+ #
52
+ # {Literal}
53
+ # : See {Literal#plus}.
54
+ #
55
+ # @param other [Literal] The right-hand side of the operator
56
+ # @return [Literal] The result of the operation
57
+ # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
58
+ def plus(other)
59
+ if other.is_a? Number
60
+ operate(other, :+)
61
+ elsif other.is_a?(Color)
62
+ other.plus(self)
63
+ else
64
+ super
65
+ end
66
+ end
67
+
68
+ # The SassScript binary `-` operation (e.g. `!a - !b`).
69
+ # Its functionality depends on the type of its argument:
70
+ #
71
+ # {Number}
72
+ # : Subtracts this number from the other, converting units if possible.
73
+ #
74
+ # {Literal}
75
+ # : See {Literal#minus}.
76
+ #
77
+ # @param other [Literal] The right-hand side of the operator
78
+ # @return [Literal] The result of the operation
79
+ # @raise [Sass::UnitConversionError] if `other` is a number with incompatible units
80
+ def minus(other)
81
+ if other.is_a? Number
82
+ operate(other, :-)
83
+ else
84
+ super
85
+ end
86
+ end
87
+
88
+ # The SassScript unary `-` operation (e.g. `-!a`).
89
+ #
90
+ # @return [Number] The negative value of this number
91
+ def unary_minus
92
+ Number.new(-value, numerator_units, denominator_units)
93
+ end
94
+
95
+ # The SassScript `*` operation.
96
+ # Its functionality depends on the type of its argument:
97
+ #
98
+ # {Number}
99
+ # : Multiplies the two numbers together, converting units appropriately.
100
+ #
101
+ # {Color}
102
+ # : Multiplies each of the RGB color channels by this number.
103
+ #
104
+ # @param other [Number, Color] The right-hand side of the operator
105
+ # @return [Number, Color] The result of the operation
106
+ # @raise [NoMethodError] if `other` is an invalid type
107
+ def times(other)
108
+ if other.is_a? Number
109
+ self.operate(other, :*)
110
+ elsif other.is_a? Color
111
+ other.times(self)
112
+ else
113
+ raise NoMethodError.new(nil, :times)
114
+ end
115
+ end
116
+
117
+ # The SassScript `/` operation.
118
+ # Its functionality depends on the type of its argument:
119
+ #
120
+ # {Number}
121
+ # : Divides this number by the other, converting units appropriately.
122
+ #
123
+ # {Literal}
124
+ # : See {Literal#div}.
125
+ #
126
+ # @param other [Literal] The right-hand side of the operator
127
+ # @return [Literal] The result of the operation
128
+ def div(other)
129
+ if other.is_a? Number
130
+ operate(other, :/)
131
+ else
132
+ super
133
+ end
134
+ end
135
+
136
+ # The SassScript `%` operation.
137
+ #
138
+ # @param other [Number] The right-hand side of the operator
139
+ # @return [Number] This number modulo the other
140
+ # @raise [NoMethodError] if `other` is an invalid type
141
+ # @raise [Sass::UnitConversionError] if `other` has any units
142
+ def mod(other)
143
+ if other.is_a?(Number)
144
+ unless other.unitless?
145
+ raise Sass::UnitConversionError.new("Cannot modulo by a number with units: #{other.inspect}.")
146
+ end
147
+ operate(other, :%)
148
+ else
149
+ raise NoMethodError.new(nil, :mod)
150
+ end
151
+ end
152
+
153
+ # The SassScript `==` operation.
154
+ #
155
+ # @param other [Literal] The right-hand side of the operator
156
+ # @return [Boolean] Whether this number is equal to the other object
157
+ def eq(other)
158
+ return Sass::Script::Bool.new(false) unless other.is_a?(Sass::Script::Number)
159
+ this = self
160
+ begin
161
+ if unitless?
162
+ this = this.coerce(other.numerator_units, other.denominator_units)
163
+ else
164
+ other = other.coerce(numerator_units, denominator_units)
165
+ end
166
+ rescue Sass::UnitConversionError
167
+ return Sass::Script::Bool.new(false)
168
+ end
169
+
170
+ Sass::Script::Bool.new(this.value == other.value)
171
+ end
172
+
173
+ # The SassScript `>` operation.
174
+ #
175
+ # @param other [Number] The right-hand side of the operator
176
+ # @return [Boolean] Whether this number is greater than the other
177
+ # @raise [NoMethodError] if `other` is an invalid type
178
+ def gt(other)
179
+ raise NoMethodError.new(nil, :gt) unless other.is_a?(Number)
180
+ operate(other, :>)
181
+ end
182
+
183
+ # The SassScript `>=` operation.
184
+ #
185
+ # @param other [Number] The right-hand side of the operator
186
+ # @return [Boolean] Whether this number is greater than or equal to the other
187
+ # @raise [NoMethodError] if `other` is an invalid type
188
+ def gte(other)
189
+ raise NoMethodError.new(nil, :gte) unless other.is_a?(Number)
190
+ operate(other, :>=)
191
+ end
192
+
193
+ # The SassScript `<` operation.
194
+ #
195
+ # @param other [Number] The right-hand side of the operator
196
+ # @return [Boolean] Whether this number is less than the other
197
+ # @raise [NoMethodError] if `other` is an invalid type
198
+ def lt(other)
199
+ raise NoMethodError.new(nil, :lt) unless other.is_a?(Number)
200
+ operate(other, :<)
201
+ end
202
+
203
+ # The SassScript `<=` operation.
204
+ #
205
+ # @param other [Number] The right-hand side of the operator
206
+ # @return [Boolean] Whether this number is less than or equal to the other
207
+ # @raise [NoMethodError] if `other` is an invalid type
208
+ def lte(other)
209
+ raise NoMethodError.new(nil, :lte) unless other.is_a?(Number)
210
+ operate(other, :<=)
211
+ end
212
+
213
+ # @return [String] The CSS representation of this number
214
+ # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS
215
+ # (e.g. `px*in`)
216
+ def to_s
217
+ raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units?
218
+ inspect
219
+ end
220
+
221
+ # Returns a readable representation of this number.
222
+ #
223
+ # This representation is valid CSS (and valid SassScript)
224
+ # as long as there is only one unit.
225
+ #
226
+ # @return [String] The representation
227
+ def inspect
228
+ value =
229
+ if self.value.is_a?(Float) && (self.value.infinite? || self.value.nan?)
230
+ self.value
231
+ elsif int?
232
+ self.value.to_i
233
+ else
234
+ (self.value * PRECISION).round / PRECISION
235
+ end
236
+ "#{value}#{unit_str}"
237
+ end
238
+
239
+ # @return [Fixnum] The integer value of the number
240
+ # @raise [Sass::SyntaxError] if the number isn't an integer
241
+ def to_i
242
+ super unless int?
243
+ return value
244
+ end
245
+
246
+ # @return [Boolean] Whether or not this number is an integer.
247
+ def int?
248
+ value % 1 == 0.0
249
+ end
250
+
251
+ # @return [Boolean] Whether or not this number has no units.
252
+ def unitless?
253
+ numerator_units.empty? && denominator_units.empty?
254
+ end
255
+
256
+ # @return [Boolean] Whether or not this number has units that can be represented in CSS
257
+ # (that is, zero or one \{#numerator\_units}).
258
+ def legal_units?
259
+ (numerator_units.empty? || numerator_units.size == 1) && denominator_units.empty?
260
+ end
261
+
262
+ # Returns this number converted to other units.
263
+ # The conversion takes into account the relationship between e.g. mm and cm,
264
+ # as well as between e.g. in and cm.
265
+ #
266
+ # If this number has no units, it will simply return itself
267
+ # with the given units.
268
+ #
269
+ # An incompatible coercion, e.g. between px and cm, will raise an error.
270
+ #
271
+ # @param num_units [Array<String>] The numerator units to coerce this number into.
272
+ # See {\#numerator\_units}
273
+ # @param den_units [Array<String>] The denominator units to coerce this number into.
274
+ # See {\#denominator\_units}
275
+ # @return [Number] The number with the new units
276
+ # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's
277
+ # current units
278
+ def coerce(num_units, den_units)
279
+ Number.new(if unitless?
280
+ self.value
281
+ else
282
+ self.value * coercion_factor(self.numerator_units, num_units) /
283
+ coercion_factor(self.denominator_units, den_units)
284
+ end, num_units, den_units)
285
+ end
286
+
287
+ protected
288
+
289
+ def operate(other, operation)
290
+ this = self
291
+ if [:+, :-, :<=, :<, :>, :>=].include?(operation)
292
+ if unitless?
293
+ this = this.coerce(other.numerator_units, other.denominator_units)
294
+ else
295
+ other = other.coerce(numerator_units, denominator_units)
296
+ end
297
+ end
298
+ # avoid integer division
299
+ value = (:/ == operation) ? this.value.to_f : this.value
300
+ result = value.send(operation, other.value)
301
+
302
+ if result.is_a?(Numeric)
303
+ Number.new(result, *compute_units(this, other, operation))
304
+ else # Boolean op
305
+ Bool.new(result)
306
+ end
307
+ end
308
+
309
+ def coercion_factor(from_units, to_units)
310
+ # get a list of unmatched units
311
+ from_units, to_units = sans_common_units(from_units, to_units)
312
+
313
+ if from_units.size != to_units.size || !convertable?(from_units | to_units)
314
+ raise Sass::UnitConversionError.new("Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.")
315
+ end
316
+
317
+ from_units.zip(to_units).inject(1) {|m,p| m * conversion_factor(p[0], p[1]) }
318
+ end
319
+
320
+ def compute_units(this, other, operation)
321
+ case operation
322
+ when :*
323
+ [this.numerator_units + other.numerator_units, this.denominator_units + other.denominator_units]
324
+ when :/
325
+ [this.numerator_units + other.denominator_units, this.denominator_units + other.numerator_units]
326
+ else
327
+ [this.numerator_units, this.denominator_units]
328
+ end
329
+ end
330
+
331
+ def unit_str
332
+ rv = numerator_units.join("*")
333
+ if denominator_units.any?
334
+ rv << "/"
335
+ rv << denominator_units.join("*")
336
+ end
337
+ rv
338
+ end
339
+
340
+ def normalize!
341
+ return if unitless?
342
+ @numerator_units, @denominator_units = sans_common_units(numerator_units, denominator_units)
343
+
344
+ @denominator_units.each_with_index do |d, i|
345
+ if convertable?(d) && (u = @numerator_units.detect(&method(:convertable?)))
346
+ @value /= conversion_factor(d, u)
347
+ @denominator_units.delete_at(i)
348
+ @numerator_units.delete_at(@numerator_units.index(u))
349
+ end
350
+ end
351
+ end
352
+
353
+ # A hash of unit names to their index in the conversion table
354
+ CONVERTABLE_UNITS = {"in" => 0, "cm" => 1, "pc" => 2, "mm" => 3, "pt" => 4}
355
+ CONVERSION_TABLE = [[ 1, 2.54, 6, 25.4, 72 ], # in
356
+ [ nil, 1, 2.36220473, 10, 28.3464567], # cm
357
+ [ nil, nil, 1, 4.23333333, 12 ], # pc
358
+ [ nil, nil, nil, 1, 2.83464567], # mm
359
+ [ nil, nil, nil, nil, 1 ]] # pt
360
+
361
+ def conversion_factor(from_unit, to_unit)
362
+ res = CONVERSION_TABLE[CONVERTABLE_UNITS[from_unit]][CONVERTABLE_UNITS[to_unit]]
363
+ return 1.0 / conversion_factor(to_unit, from_unit) if res.nil?
364
+ res
365
+ end
366
+
367
+ def convertable?(units)
368
+ Array(units).all?(&CONVERTABLE_UNITS.method(:include?))
369
+ end
370
+
371
+ def sans_common_units(units1, units2)
372
+ units2 = units2.dup
373
+ # Can't just use -, because we want px*px to coerce properly to px*mm
374
+ return units1.map do |u|
375
+ next u unless j = units2.index(u)
376
+ units2.delete_at(j)
377
+ nil
378
+ end.compact, units2
379
+ end
380
+ end
381
+ end