sassc 2.1.0.pre1-x64-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.gitmodules +3 -0
  4. data/.travis.yml +11 -0
  5. data/CHANGELOG.md +66 -0
  6. data/CODE_OF_CONDUCT.md +10 -0
  7. data/Gemfile +2 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +68 -0
  10. data/Rakefile +30 -0
  11. data/lib/sassc.rb +57 -0
  12. data/lib/sassc/dependency.rb +17 -0
  13. data/lib/sassc/engine.rb +139 -0
  14. data/lib/sassc/error.rb +37 -0
  15. data/lib/sassc/functions_handler.rb +75 -0
  16. data/lib/sassc/import_handler.rb +50 -0
  17. data/lib/sassc/importer.rb +31 -0
  18. data/lib/sassc/native.rb +70 -0
  19. data/lib/sassc/native/lib_c.rb +21 -0
  20. data/lib/sassc/native/native_context_api.rb +147 -0
  21. data/lib/sassc/native/native_functions_api.rb +164 -0
  22. data/lib/sassc/native/sass2scss_api.rb +10 -0
  23. data/lib/sassc/native/sass_input_style.rb +13 -0
  24. data/lib/sassc/native/sass_output_style.rb +12 -0
  25. data/lib/sassc/native/sass_value.rb +97 -0
  26. data/lib/sassc/native/string_list.rb +10 -0
  27. data/lib/sassc/sass_2_scss.rb +9 -0
  28. data/lib/sassc/script.rb +19 -0
  29. data/lib/sassc/script/functions.rb +8 -0
  30. data/lib/sassc/script/value.rb +137 -0
  31. data/lib/sassc/script/value/bool.rb +32 -0
  32. data/lib/sassc/script/value/color.rb +95 -0
  33. data/lib/sassc/script/value/list.rb +136 -0
  34. data/lib/sassc/script/value/map.rb +69 -0
  35. data/lib/sassc/script/value/number.rb +389 -0
  36. data/lib/sassc/script/value/string.rb +96 -0
  37. data/lib/sassc/script/value_conversion.rb +69 -0
  38. data/lib/sassc/script/value_conversion/base.rb +13 -0
  39. data/lib/sassc/script/value_conversion/bool.rb +13 -0
  40. data/lib/sassc/script/value_conversion/color.rb +18 -0
  41. data/lib/sassc/script/value_conversion/list.rb +25 -0
  42. data/lib/sassc/script/value_conversion/map.rb +21 -0
  43. data/lib/sassc/script/value_conversion/number.rb +13 -0
  44. data/lib/sassc/script/value_conversion/string.rb +17 -0
  45. data/lib/sassc/util.rb +231 -0
  46. data/lib/sassc/util/normalized_map.rb +117 -0
  47. data/lib/sassc/version.rb +5 -0
  48. data/sassc.gemspec +57 -0
  49. data/test/custom_importer_test.rb +127 -0
  50. data/test/engine_test.rb +314 -0
  51. data/test/error_test.rb +29 -0
  52. data/test/fixtures/paths.scss +10 -0
  53. data/test/functions_test.rb +303 -0
  54. data/test/native_test.rb +213 -0
  55. data/test/output_style_test.rb +107 -0
  56. data/test/sass_2_scss_test.rb +14 -0
  57. data/test/test_helper.rb +45 -0
  58. metadata +242 -0
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A SassScript object representing a CSS list.
4
+ # This includes both comma-separated lists and space-separated lists.
5
+
6
+ class SassC::Script::Value::List < SassC::Script::Value
7
+
8
+ # The Ruby array containing the contents of the list.
9
+ #
10
+ # @return [Array<Value>]
11
+ attr_reader :value
12
+ alias_method :to_a, :value
13
+
14
+ # The operator separating the values of the list.
15
+ # Either `:comma` or `:space`.
16
+ #
17
+ # @return [Symbol]
18
+ attr_reader :separator
19
+
20
+ # Whether the list is surrounded by square brackets.
21
+ #
22
+ # @return [Boolean]
23
+ attr_reader :bracketed
24
+
25
+ # Creates a new list.
26
+ #
27
+ # @param value [Array<Value>] See \{#value}
28
+ # @param separator [Symbol] See \{#separator}
29
+ # @param bracketed [Boolean] See \{#bracketed}
30
+ def initialize(value, separator: nil, bracketed: false)
31
+ super(value)
32
+ @separator = separator
33
+ @bracketed = bracketed
34
+ end
35
+
36
+ # @see Value#options=
37
+ def options=(options)
38
+ super
39
+ value.each {|v| v.options = options}
40
+ end
41
+
42
+ # @see Value#eq
43
+ def eq(other)
44
+ SassC::Script::Value::Bool.new(
45
+ other.is_a?(List) && value == other.value &&
46
+ separator == other.separator && bracketed == other.bracketed
47
+ )
48
+ end
49
+
50
+ def hash
51
+ @hash ||= [value, separator, bracketed].hash
52
+ end
53
+
54
+ # @see Value#to_s
55
+ def to_s(opts = {})
56
+ if !bracketed && value.empty?
57
+ raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.")
58
+ end
59
+
60
+ members = value.
61
+ reject {|e| e.is_a?(Null) || e.is_a?(List) && e.value.empty?}.
62
+ map {|e| e.to_s(opts)}
63
+
64
+ contents = members.join(sep_str)
65
+ bracketed ? "[#{contents}]" : contents
66
+ end
67
+
68
+ # @see Value#to_sass
69
+ def to_sass(opts = {})
70
+ return bracketed ? "[]" : "()" if value.empty?
71
+ members = value.map do |v|
72
+ if element_needs_parens?(v)
73
+ "(#{v.to_sass(opts)})"
74
+ else
75
+ v.to_sass(opts)
76
+ end
77
+ end
78
+
79
+ if separator == :comma && members.length == 1
80
+ return "#{bracketed ? '[' : '('}#{members.first},#{bracketed ? ']' : ')'}"
81
+ end
82
+
83
+ contents = members.join(sep_str(nil))
84
+ bracketed ? "[#{contents}]" : contents
85
+ end
86
+
87
+ # @see Value#to_h
88
+ def to_h
89
+ return {} if value.empty?
90
+ super
91
+ end
92
+
93
+ # @see Value#inspect
94
+ def inspect
95
+ (bracketed ? '[' : '(') + value.map {|e| e.inspect}.join(sep_str(nil)) + (bracketed ? ']' : ')')
96
+ end
97
+
98
+ # Asserts an index is within the list.
99
+ #
100
+ # @private
101
+ #
102
+ # @param list [Sass::Script::Value::List] The list for which the index should be checked.
103
+ # @param n [Sass::Script::Value::Number] The index being checked.
104
+ def self.assert_valid_index(list, n)
105
+ if !n.int? || n.to_i == 0
106
+ raise ArgumentError.new("List index #{n} must be a non-zero integer")
107
+ elsif list.to_a.size == 0
108
+ raise ArgumentError.new("List index is #{n} but list has no items")
109
+ elsif n.to_i.abs > (size = list.to_a.size)
110
+ raise ArgumentError.new(
111
+ "List index is #{n} but list is only #{size} item#{'s' if size != 1} long")
112
+ end
113
+ end
114
+
115
+ private
116
+
117
+ def element_needs_parens?(element)
118
+ if element.is_a?(List)
119
+ return false if element.value.length < 2
120
+ return false if element.bracketed
121
+ precedence = Sass::Script::Parser.precedence_of(separator || :space)
122
+ return Sass::Script::Parser.precedence_of(element.separator || :space) <= precedence
123
+ end
124
+
125
+ return false unless separator == :space
126
+ return false unless element.is_a?(Sass::Script::Tree::UnaryOperation)
127
+ element.operator == :minus || element.operator == :plus
128
+ end
129
+
130
+ def sep_str(opts = options)
131
+ return ' ' if separator == :space
132
+ return ',' if opts && opts[:style] == :compressed
133
+ ', '
134
+ end
135
+
136
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SassC::Script::Value::Map < SassC::Script::Value
4
+
5
+ # The Ruby hash containing the contents of this map.
6
+ # @return [Hash<Node, Node>]
7
+ attr_reader :value
8
+ alias_method :to_h, :value
9
+
10
+ # Creates a new map.
11
+ #
12
+ # @param hash [Hash<Node, Node>]
13
+ def initialize(hash)
14
+ super(hash)
15
+ end
16
+
17
+ # @see Value#options=
18
+ def options=(options)
19
+ super
20
+ value.each do |k, v|
21
+ k.options = options
22
+ v.options = options
23
+ end
24
+ end
25
+
26
+ # @see Value#separator
27
+ def separator
28
+ :comma unless value.empty?
29
+ end
30
+
31
+ # @see Value#to_a
32
+ def to_a
33
+ value.map do |k, v|
34
+ list = SassC::Script::Value::List.new([k, v], separator: :space)
35
+ list.options = options
36
+ list
37
+ end
38
+ end
39
+
40
+ # @see Value#eq
41
+ def eq(other)
42
+ SassC::Script::Value::Bool.new(other.is_a?(Map) && value == other.value)
43
+ end
44
+
45
+ def hash
46
+ @hash ||= value.hash
47
+ end
48
+
49
+ # @see Value#to_s
50
+ def to_s(opts = {})
51
+ raise SassC::SyntaxError.new("#{inspect} isn't a valid CSS value.")
52
+ end
53
+
54
+ def to_sass(opts = {})
55
+ return "()" if value.empty?
56
+
57
+ to_sass = lambda do |value|
58
+ if value.is_a?(List) && value.separator == :comma
59
+ "(#{value.to_sass(opts)})"
60
+ else
61
+ value.to_sass(opts)
62
+ end
63
+ end
64
+
65
+ "(#{value.map {|(k, v)| "#{to_sass[k]}: #{to_sass[v]}"}.join(', ')})"
66
+ end
67
+ alias_method :inspect, :to_sass
68
+
69
+ end
@@ -0,0 +1,389 @@
1
+ # frozen_string_literal: true
2
+
3
+ # A SassScript object representing a number.
4
+ # SassScript numbers can have decimal values,
5
+ # and can also have units.
6
+ # For example, `12`, `1px`, and `10.45em`
7
+ # are all valid values.
8
+ #
9
+ # Numbers can also have more complex units, such as `1px*em/in`.
10
+ # These cannot be inputted directly in Sass code at the moment.
11
+
12
+ class SassC::Script::Value::Number < SassC::Script::Value
13
+
14
+ # The Ruby value of the number.
15
+ #
16
+ # @return [Numeric]
17
+ attr_reader :value
18
+
19
+ # A list of units in the numerator of the number.
20
+ # For example, `1px*em/in*cm` would return `["px", "em"]`
21
+ # @return [Array<String>]
22
+ attr_reader :numerator_units
23
+
24
+ # A list of units in the denominator of the number.
25
+ # For example, `1px*em/in*cm` would return `["in", "cm"]`
26
+ # @return [Array<String>]
27
+ attr_reader :denominator_units
28
+
29
+ # The original representation of this number.
30
+ # For example, although the result of `1px/2px` is `0.5`,
31
+ # the value of `#original` is `"1px/2px"`.
32
+ #
33
+ # This is only non-nil when the original value should be used as the CSS value,
34
+ # as in `font: 1px/2px`.
35
+ #
36
+ # @return [Boolean, nil]
37
+ attr_accessor :original
38
+
39
+ def self.precision
40
+ Thread.current[:sass_numeric_precision] || Thread.main[:sass_numeric_precision] || 10
41
+ end
42
+
43
+ # Sets the number of digits of precision
44
+ # For example, if this is `3`,
45
+ # `3.1415926` will be printed as `3.142`.
46
+ # The numeric precision is stored as a thread local for thread safety reasons.
47
+ # To set for all threads, be sure to set the precision on the main thread.
48
+ def self.precision=(digits)
49
+ Thread.current[:sass_numeric_precision] = digits.round
50
+ Thread.current[:sass_numeric_precision_factor] = nil
51
+ Thread.current[:sass_numeric_epsilon] = nil
52
+ end
53
+
54
+ # the precision factor used in numeric output
55
+ # it is derived from the `precision` method.
56
+ def self.precision_factor
57
+ Thread.current[:sass_numeric_precision_factor] ||= 10.0**precision
58
+ end
59
+
60
+ # Used in checking equality of floating point numbers. Any
61
+ # numbers within an `epsilon` of each other are considered functionally equal.
62
+ # The value for epsilon is one tenth of the current numeric precision.
63
+ def self.epsilon
64
+ Thread.current[:sass_numeric_epsilon] ||= 1 / (precision_factor * 10)
65
+ end
66
+
67
+ # Used so we don't allocate two new arrays for each new number.
68
+ NO_UNITS = []
69
+
70
+ # @param value [Numeric] The value of the number
71
+ # @param numerator_units [::String, Array<::String>] See \{#numerator\_units}
72
+ # @param denominator_units [::String, Array<::String>] See \{#denominator\_units}
73
+ def initialize(value, numerator_units = NO_UNITS, denominator_units = NO_UNITS)
74
+ numerator_units = [numerator_units] if numerator_units.is_a?(::String)
75
+ denominator_units = [denominator_units] if denominator_units.is_a?(::String)
76
+ super(value)
77
+ @numerator_units = numerator_units
78
+ @denominator_units = denominator_units
79
+ @options = nil
80
+ normalize!
81
+ end
82
+
83
+ def hash
84
+ [value, numerator_units, denominator_units].hash
85
+ end
86
+
87
+ # Hash-equality works differently than `==` equality for numbers.
88
+ # Hash-equality must be transitive, so it just compares the exact value,
89
+ # numerator units, and denominator units.
90
+ def eql?(other)
91
+ basically_equal?(value, other.value) && numerator_units == other.numerator_units &&
92
+ denominator_units == other.denominator_units
93
+ end
94
+
95
+ # @return [String] The CSS representation of this number
96
+ # @raise [Sass::SyntaxError] if this number has units that can't be used in CSS
97
+ # (e.g. `px*in`)
98
+ def to_s(opts = {})
99
+ return original if original
100
+ raise Sass::SyntaxError.new("#{inspect} isn't a valid CSS value.") unless legal_units?
101
+ inspect
102
+ end
103
+
104
+ # Returns a readable representation of this number.
105
+ #
106
+ # This representation is valid CSS (and valid SassScript)
107
+ # as long as there is only one unit.
108
+ #
109
+ # @return [String] The representation
110
+ def inspect(opts = {})
111
+ return original if original
112
+
113
+ value = self.class.round(self.value)
114
+ str = value.to_s
115
+
116
+ # Ruby will occasionally print in scientific notation if the number is
117
+ # small enough. That's technically valid CSS, but it's not well-supported
118
+ # and confusing.
119
+ str = ("%0.#{self.class.precision}f" % value).gsub(/0*$/, '') if str.include?('e')
120
+
121
+ # Sometimes numeric formatting will result in a decimal number with a trailing zero (x.0)
122
+ if str =~ /(.*)\.0$/
123
+ str = $1
124
+ end
125
+
126
+ # We omit a leading zero before the decimal point in compressed mode.
127
+ if @options && options[:style] == :compressed
128
+ str.sub!(/^(-)?0\./, '\1.')
129
+ end
130
+
131
+ unitless? ? str : "#{str}#{unit_str}"
132
+ end
133
+ alias_method :to_sass, :inspect
134
+
135
+ # @return [Integer] The integer value of the number
136
+ # @raise [Sass::SyntaxError] if the number isn't an integer
137
+ def to_i
138
+ super unless int?
139
+ value.to_i
140
+ end
141
+
142
+ # @return [Boolean] Whether or not this number is an integer.
143
+ def int?
144
+ basically_equal?(value % 1, 0.0)
145
+ end
146
+
147
+ # @return [Boolean] Whether or not this number has no units.
148
+ def unitless?
149
+ @numerator_units.empty? && @denominator_units.empty?
150
+ end
151
+
152
+ # Checks whether the number has the numerator unit specified.
153
+ #
154
+ # @example
155
+ # number = Sass::Script::Value::Number.new(10, "px")
156
+ # number.is_unit?("px") => true
157
+ # number.is_unit?(nil) => false
158
+ #
159
+ # @param unit [::String, nil] The unit the number should have or nil if the number
160
+ # should be unitless.
161
+ # @see Number#unitless? The unitless? method may be more readable.
162
+ def is_unit?(unit)
163
+ if unit
164
+ denominator_units.size == 0 && numerator_units.size == 1 && numerator_units.first == unit
165
+ else
166
+ unitless?
167
+ end
168
+ end
169
+
170
+ # @return [Boolean] Whether or not this number has units that can be represented in CSS
171
+ # (that is, zero or one \{#numerator\_units}).
172
+ def legal_units?
173
+ (@numerator_units.empty? || @numerator_units.size == 1) && @denominator_units.empty?
174
+ end
175
+
176
+ # Returns this number converted to other units.
177
+ # The conversion takes into account the relationship between e.g. mm and cm,
178
+ # as well as between e.g. in and cm.
179
+ #
180
+ # If this number has no units, it will simply return itself
181
+ # with the given units.
182
+ #
183
+ # An incompatible coercion, e.g. between px and cm, will raise an error.
184
+ #
185
+ # @param num_units [Array<String>] The numerator units to coerce this number into.
186
+ # See {\#numerator\_units}
187
+ # @param den_units [Array<String>] The denominator units to coerce this number into.
188
+ # See {\#denominator\_units}
189
+ # @return [Number] The number with the new units
190
+ # @raise [Sass::UnitConversionError] if the given units are incompatible with the number's
191
+ # current units
192
+ def coerce(num_units, den_units)
193
+ Number.new(if unitless?
194
+ value
195
+ else
196
+ value * coercion_factor(@numerator_units, num_units) /
197
+ coercion_factor(@denominator_units, den_units)
198
+ end, num_units, den_units)
199
+ end
200
+
201
+ # @param other [Number] A number to decide if it can be compared with this number.
202
+ # @return [Boolean] Whether or not this number can be compared with the other.
203
+ def comparable_to?(other)
204
+ operate(other, :+)
205
+ true
206
+ rescue Sass::UnitConversionError
207
+ false
208
+ end
209
+
210
+ # Returns a human readable representation of the units in this number.
211
+ # For complex units this takes the form of:
212
+ # numerator_unit1 * numerator_unit2 / denominator_unit1 * denominator_unit2
213
+ # @return [String] a string that represents the units in this number
214
+ def unit_str
215
+ rv = @numerator_units.sort.join("*")
216
+ if @denominator_units.any?
217
+ rv << "/"
218
+ rv << @denominator_units.sort.join("*")
219
+ end
220
+ rv
221
+ end
222
+
223
+ private
224
+
225
+ # @private
226
+ # @see Sass::Script::Number.basically_equal?
227
+ def basically_equal?(num1, num2)
228
+ self.class.basically_equal?(num1, num2)
229
+ end
230
+
231
+ # Checks whether two numbers are within an epsilon of each other.
232
+ # @return [Boolean]
233
+ def self.basically_equal?(num1, num2)
234
+ (num1 - num2).abs < epsilon
235
+ end
236
+
237
+ # @private
238
+ def self.round(num)
239
+ if num.is_a?(Float) && (num.infinite? || num.nan?)
240
+ num
241
+ elsif basically_equal?(num % 1, 0.0)
242
+ num.round
243
+ else
244
+ ((num * precision_factor).round / precision_factor).to_f
245
+ end
246
+ end
247
+
248
+ OPERATIONS = [:+, :-, :<=, :<, :>, :>=, :%]
249
+
250
+ def operate(other, operation)
251
+ this = self
252
+ if OPERATIONS.include?(operation)
253
+ if unitless?
254
+ this = this.coerce(other.numerator_units, other.denominator_units)
255
+ else
256
+ other = other.coerce(@numerator_units, @denominator_units)
257
+ end
258
+ end
259
+ # avoid integer division
260
+ value = :/ == operation ? this.value.to_f : this.value
261
+ result = value.send(operation, other.value)
262
+
263
+ if result.is_a?(Numeric)
264
+ Number.new(result, *compute_units(this, other, operation))
265
+ else # Boolean op
266
+ Bool.new(result)
267
+ end
268
+ end
269
+
270
+ def coercion_factor(from_units, to_units)
271
+ # get a list of unmatched units
272
+ from_units, to_units = sans_common_units(from_units, to_units)
273
+
274
+ if from_units.size != to_units.size || !convertable?(from_units | to_units)
275
+ raise Sass::UnitConversionError.new(
276
+ "Incompatible units: '#{from_units.join('*')}' and '#{to_units.join('*')}'.")
277
+ end
278
+
279
+ from_units.zip(to_units).inject(1) {|m, p| m * conversion_factor(p[0], p[1])}
280
+ end
281
+
282
+ def compute_units(this, other, operation)
283
+ case operation
284
+ when :*
285
+ [this.numerator_units + other.numerator_units,
286
+ this.denominator_units + other.denominator_units]
287
+ when :/
288
+ [this.numerator_units + other.denominator_units,
289
+ this.denominator_units + other.numerator_units]
290
+ else
291
+ [this.numerator_units, this.denominator_units]
292
+ end
293
+ end
294
+
295
+ def normalize!
296
+ return if unitless?
297
+ @numerator_units, @denominator_units =
298
+ sans_common_units(@numerator_units, @denominator_units)
299
+
300
+ @denominator_units.each_with_index do |d, i|
301
+ next unless convertable?(d) && (u = @numerator_units.find {|n| convertable?([n, d])})
302
+ @value /= conversion_factor(d, u)
303
+ @denominator_units.delete_at(i)
304
+ @numerator_units.delete_at(@numerator_units.index(u))
305
+ end
306
+ end
307
+
308
+ # This is the source data for all the unit logic. It's pre-processed to make
309
+ # it efficient to figure out whether a set of units is mutually compatible
310
+ # and what the conversion ratio is between two units.
311
+ #
312
+ # These come from http://www.w3.org/TR/2012/WD-css3-values-20120308/.
313
+ relative_sizes = [
314
+ {
315
+ "in" => Rational(1),
316
+ "cm" => Rational(1, 2.54),
317
+ "pc" => Rational(1, 6),
318
+ "mm" => Rational(1, 25.4),
319
+ "q" => Rational(1, 101.6),
320
+ "pt" => Rational(1, 72),
321
+ "px" => Rational(1, 96)
322
+ },
323
+ {
324
+ "deg" => Rational(1, 360),
325
+ "grad" => Rational(1, 400),
326
+ "rad" => Rational(1, 2 * Math::PI),
327
+ "turn" => Rational(1)
328
+ },
329
+ {
330
+ "s" => Rational(1),
331
+ "ms" => Rational(1, 1000)
332
+ },
333
+ {
334
+ "Hz" => Rational(1),
335
+ "kHz" => Rational(1000)
336
+ },
337
+ {
338
+ "dpi" => Rational(1),
339
+ "dpcm" => Rational(254, 100),
340
+ "dppx" => Rational(96)
341
+ }
342
+ ]
343
+
344
+ # A hash from each known unit to the set of units that it's mutually
345
+ # convertible with.
346
+ MUTUALLY_CONVERTIBLE = {}
347
+ relative_sizes.map do |values|
348
+ set = values.keys.to_set
349
+ values.keys.each {|name| MUTUALLY_CONVERTIBLE[name] = set}
350
+ end
351
+
352
+ # A two-dimensional hash from two units to the conversion ratio between
353
+ # them. Multiply `X` by `CONVERSION_TABLE[X][Y]` to convert it to `Y`.
354
+ CONVERSION_TABLE = {}
355
+ relative_sizes.each do |values|
356
+ values.each do |(name1, value1)|
357
+ CONVERSION_TABLE[name1] ||= {}
358
+ values.each do |(name2, value2)|
359
+ value = value1 / value2
360
+ CONVERSION_TABLE[name1][name2] = value.denominator == 1 ? value.to_i : value.to_f
361
+ end
362
+ end
363
+ end
364
+
365
+ def conversion_factor(from_unit, to_unit)
366
+ CONVERSION_TABLE[from_unit][to_unit]
367
+ end
368
+
369
+ def convertable?(units)
370
+ units = Array(units).to_set
371
+ return true if units.empty?
372
+ return false unless (mutually_convertible = MUTUALLY_CONVERTIBLE[units.first])
373
+ units.subset?(mutually_convertible)
374
+ end
375
+
376
+ def sans_common_units(units1, units2)
377
+ units2 = units2.dup
378
+ # Can't just use -, because we want px*px to coerce properly to px*mm
379
+ units1 = units1.map do |u|
380
+ j = units2.index(u)
381
+ next u unless j
382
+ units2.delete_at(j)
383
+ nil
384
+ end
385
+ units1.compact!
386
+ return units1, units2
387
+ end
388
+
389
+ end