sass 3.3.0.alpha.16 → 3.3.0.alpha.50

Sign up to get free protection for your applications and to get access to all the features.
data/lib/sass/util.rb CHANGED
@@ -788,6 +788,100 @@ MSG
788
788
  inject_values(str, vals)
789
789
  end
790
790
 
791
+ # Builds a sourcemap file name given the generated CSS file name.
792
+ #
793
+ # @param css [String] The generated CSS file name.
794
+ # @return [String] The source map file name.
795
+ def sourcemap_name(css)
796
+ css + ".map"
797
+ end
798
+
799
+ # Escapes certain characters so that the result can be used
800
+ # as the JSON string value. Returns the original string if
801
+ # no escaping is necessary.
802
+ #
803
+ # @param s [String] The string to be escaped
804
+ # @return [String] The escaped string
805
+ def json_escape_string(s)
806
+ return s if s !~ /["\\\/\b\f\n\r\t]/
807
+
808
+ result = ""
809
+ s.split("").each do |c|
810
+ case c
811
+ when '"', "\\", "/"
812
+ result << "\\" << c
813
+ when "\n" then result << "\\n"
814
+ when "\t" then result << "\\t"
815
+ when "\r" then result << "\\r"
816
+ when "\f" then result << "\\f"
817
+ when "\b" then result << "\\b"
818
+ else
819
+ result << c
820
+ end
821
+ end
822
+ result
823
+ end
824
+
825
+ # Converts the argument into a valid JSON value.
826
+ #
827
+ # @param v [Fixnum, String, Array, Boolean, nil]
828
+ # @return [String]
829
+ def json_value_of(v)
830
+ case v
831
+ when Fixnum
832
+ v.to_s
833
+ when String
834
+ "\"" + json_escape_string(v) + "\""
835
+ when Array
836
+ "[" + v.map {|x| json_value_of(x)}.join(",") + "]"
837
+ when NilClass
838
+ "null"
839
+ when TrueClass
840
+ "true"
841
+ when FalseClass
842
+ "false"
843
+ else
844
+ raise ArgumentError.new("Unknown type: #{v.class.name}")
845
+ end
846
+ end
847
+
848
+ VLQ_BASE_SHIFT = 5
849
+ VLQ_BASE = 1 << VLQ_BASE_SHIFT
850
+ VLQ_BASE_MASK = VLQ_BASE - 1
851
+ VLQ_CONTINUATION_BIT = VLQ_BASE
852
+
853
+ BASE64_DIGITS = ('A'..'Z').to_a + ('a'..'z').to_a + ('0'..'9').to_a + ['+', '/']
854
+ BASE64_DIGIT_MAP = begin
855
+ map = {}
856
+ Sass::Util.enum_with_index(BASE64_DIGITS).map do |digit, i|
857
+ map[digit] = i
858
+ end
859
+ map
860
+ end
861
+
862
+ # Encodes `value` as VLQ (http://en.wikipedia.org/wiki/VLQ).
863
+ #
864
+ # @param value [Fixnum]
865
+ # @return [String] The encoded value
866
+ def encode_vlq(value)
867
+ if value < 0
868
+ value = ((-value) << 1) | 1
869
+ else
870
+ value <<= 1
871
+ end
872
+
873
+ result = String.new
874
+ begin
875
+ digit = value & VLQ_BASE_MASK
876
+ value >>= VLQ_BASE_SHIFT
877
+ if value > 0
878
+ digit |= VLQ_CONTINUATION_BIT
879
+ end
880
+ result << BASE64_DIGITS[digit]
881
+ end while value > 0
882
+ result
883
+ end
884
+
791
885
  ## Static Method Stuff
792
886
 
793
887
  # The context in which the ERB for \{#def\_static\_method} will be run.
@@ -0,0 +1,837 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ require File.dirname(__FILE__) + '/../test_helper'
4
+ require File.dirname(__FILE__) + '/test_helper'
5
+
6
+ class SourcemapTest < Test::Unit::TestCase
7
+ def test_simple_mapping_scss
8
+ assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON
9
+ a {
10
+ foo: bar;
11
+ /* SOME COMMENT */
12
+ font-size: 12px;
13
+ }
14
+ SCSS
15
+ a {
16
+ foo: bar;
17
+ /* SOME COMMENT */
18
+ font-size: 12px; }
19
+
20
+ /*@ sourceMappingURL=test.css.map */
21
+ CSS
22
+ {
23
+ "version": "3",
24
+ "mappings": ";EACE,GAAG,EAAE,GAAG;;EAER,SAAS,EAAE,IAAI",
25
+ "sources": ["test_simple_mapping_scss_inline.scss"],
26
+ "file": "test.css"
27
+ }
28
+ JSON
29
+ end
30
+
31
+ def test_simple_mapping_sass
32
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, :syntax => :sass
33
+ a
34
+ foo: bar
35
+ /* SOME COMMENT */
36
+ :font-size 12px
37
+ SASS
38
+ a {
39
+ foo: bar;
40
+ /* SOME COMMENT */
41
+ font-size: 12px; }
42
+
43
+ /*@ sourceMappingURL=test.css.map */
44
+ CSS
45
+ {
46
+ "version": "3",
47
+ "mappings": ";EACE,GAAG,EAAE,GAAG;;EAEP,SAAS,EAAC,IAAI",
48
+ "sources": ["test_simple_mapping_sass_inline.sass"],
49
+ "file": "test.css"
50
+ }
51
+ JSON
52
+ end
53
+
54
+ def test_mapping_with_directory_scss
55
+ options = {:filename => "scss/style.scss", :output => "css/style.css"}
56
+ assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON, options
57
+ a {
58
+ foo: bar;
59
+ /* SOME COMMENT */
60
+ font-size: 12px;
61
+ }
62
+ SCSS
63
+ a {
64
+ foo: bar;
65
+ /* SOME COMMENT */
66
+ font-size: 12px; }
67
+
68
+ /*@ sourceMappingURL=style.css.map */
69
+ CSS
70
+ {
71
+ "version": "3",
72
+ "mappings": ";EACE,GAAG,EAAE,GAAG;;EAER,SAAS,EAAE,IAAI",
73
+ "sources": ["..\\/scss\\/style.scss"],
74
+ "file": "style.css"
75
+ }
76
+ JSON
77
+ end
78
+
79
+ def test_mapping_with_directory_sass
80
+ options = {:filename => "sass/style.sass", :output => "css/style.css", :syntax => :sass}
81
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, options
82
+ a
83
+ foo: bar
84
+ /* SOME COMMENT */
85
+ :font-size 12px
86
+ SASS
87
+ a {
88
+ foo: bar;
89
+ /* SOME COMMENT */
90
+ font-size: 12px; }
91
+
92
+ /*@ sourceMappingURL=style.css.map */
93
+ CSS
94
+ {
95
+ "version": "3",
96
+ "mappings": ";EACE,GAAG,EAAE,GAAG;;EAEP,SAAS,EAAC,IAAI",
97
+ "sources": ["..\\/sass\\/style.sass"],
98
+ "file": "style.css"
99
+ }
100
+ JSON
101
+ end
102
+
103
+ unless Sass::Util.ruby1_8?
104
+ def test_simple_charset_mapping_scss
105
+ assert_parses_with_sourcemap <<SCSS, <<CSS, <<JSON
106
+ a {
107
+ fóó: bár;
108
+ }
109
+ SCSS
110
+ @charset "UTF-8";
111
+ a {
112
+ fóó: bár; }
113
+
114
+ /*@ sourceMappingURL=test.css.map */
115
+ CSS
116
+ {
117
+ "version": "3",
118
+ "mappings": ";;EACE,GAAG,EAAE,GAAG",
119
+ "sources": ["test_simple_charset_mapping_scss_inline.scss"],
120
+ "file": "test.css"
121
+ }
122
+ JSON
123
+ end
124
+
125
+ def test_simple_charset_mapping_sass
126
+ assert_parses_with_sourcemap <<SASS, <<CSS, <<JSON, :syntax => :sass
127
+ a
128
+ fóó: bár
129
+ SASS
130
+ @charset "UTF-8";
131
+ a {
132
+ fóó: bár; }
133
+
134
+ /*@ sourceMappingURL=test.css.map */
135
+ CSS
136
+ {
137
+ "version": "3",
138
+ "mappings": ";;EACE,GAAG,EAAE,GAAG",
139
+ "sources": ["test_simple_charset_mapping_sass_inline.sass"],
140
+ "file": "test.css"
141
+ }
142
+ JSON
143
+ end
144
+
145
+ def test_different_charset_than_encoding_scss
146
+ assert_parses_with_sourcemap(<<SCSS.force_encoding("IBM866"), <<CSS.force_encoding("IBM866"), <<JSON)
147
+ @charset "IBM866";
148
+ f\x86\x86 {
149
+ \x86: b;
150
+ }
151
+ SCSS
152
+ @charset "IBM866";
153
+ f\x86\x86 {
154
+ \x86: b; }
155
+
156
+ /*@ sourceMappingURL=test.css.map */
157
+ CSS
158
+ {
159
+ "version": "3",
160
+ "mappings": ";;EAEE,CAAC,EAAE,CAAC",
161
+ "sources": ["test_different_charset_than_encoding_scss_inline.scss"],
162
+ "file": "test.css"
163
+ }
164
+ JSON
165
+ end
166
+
167
+ def test_different_charset_than_encoding_sass
168
+ assert_parses_with_sourcemap(<<SASS.force_encoding("IBM866"), <<CSS.force_encoding("IBM866"), <<JSON, :syntax => :sass)
169
+ @charset "IBM866"
170
+ f\x86\x86
171
+ \x86: b
172
+ SASS
173
+ @charset "IBM866";
174
+ f\x86\x86 {
175
+ \x86: b; }
176
+
177
+ /*@ sourceMappingURL=test.css.map */
178
+ CSS
179
+ {
180
+ "version": "3",
181
+ "mappings": ";;EAEE,CAAC,EAAE,CAAC",
182
+ "sources": ["test_different_charset_than_encoding_sass_inline.sass"],
183
+ "file": "test.css"
184
+ }
185
+ JSON
186
+ end
187
+ end
188
+
189
+ def test_import_sourcemap_scss
190
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
191
+ @import {{1}}url(foo){{/1}},{{2}}url(moo) {{/2}}, {{3}}url(bar) {{/3}};
192
+ @import {{4}}url(baz) screen print{{/4}};
193
+ SCSS
194
+ {{1}}@import url(foo){{/1}};
195
+ {{2}}@import url(moo){{/2}};
196
+ {{3}}@import url(bar){{/3}};
197
+ {{4}}@import url(baz) screen print{{/4}};
198
+
199
+ /*@ sourceMappingURL=test.css.map */
200
+ CSS
201
+ end
202
+
203
+ def test_import_sourcemap_sass
204
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
205
+ @import {{1}}foo.css{{/1}},{{2}}moo.css{{/2}}, {{3}}bar.css{{/3}}
206
+ @import {{4}}url(baz.css){{/4}}
207
+ @import {{5}}url(qux.css) screen print{{/5}}
208
+ SASS
209
+ {{1}}@import url(foo.css){{/1}};
210
+ {{2}}@import url(moo.css){{/2}};
211
+ {{3}}@import url(bar.css){{/3}};
212
+ {{4}}@import url(baz.css){{/4}};
213
+ {{5}}@import url(qux.css) screen print{{/5}};
214
+
215
+ /*@ sourceMappingURL=test.css.map */
216
+ CSS
217
+ end
218
+
219
+ def test_interpolation_and_vars_sourcemap_scss
220
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
221
+ $te: "te";
222
+ $teal: {{4}}teal{{/4}};
223
+ p {
224
+ {{1}}con#{$te}nt{{/1}}: {{2}}"I a#{$te} #{5 + 10} pies!"{{/2}};
225
+ {{3}}color{{/3}}: $teal;
226
+ }
227
+
228
+ $name: foo;
229
+ $attr: border;
230
+ p.#{$name} {
231
+ {{5}}#{$attr}-color{{/5}}: {{6}}blue{{/6}};
232
+ $font-size: 12px;
233
+ $line-height: 30px;
234
+ {{7}}font{{/7}}: {{8}}#{$font-size}/#{$line-height}{{/8}};
235
+ }
236
+ SCSS
237
+ p {
238
+ {{1}}content{{/1}}: {{2}}"I ate 15 pies!"{{/2}};
239
+ {{3}}color{{/3}}: {{4}}teal{{/4}}; }
240
+
241
+ p.foo {
242
+ {{5}}border-color{{/5}}: {{6}}blue{{/6}};
243
+ {{7}}font{{/7}}: {{8}}12px/30px{{/8}}; }
244
+
245
+ /*@ sourceMappingURL=test.css.map */
246
+ CSS
247
+ end
248
+
249
+ def test_interpolation_and_vars_sourcemap_sass
250
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
251
+ $te: "te"
252
+ $teal: {{4}}teal{{/4}}
253
+ p
254
+ {{1}}con#{$te}nt{{/1}}: {{2}}"I a#{$te} #{5 + 10} pies!"{{/2}}
255
+ {{3}}color{{/3}}: $teal
256
+
257
+ $name: foo
258
+ $attr: border
259
+ p.#{$name}
260
+ {{5}}#{$attr}-color{{/5}}: {{6}}blue{{/6}}
261
+ $font-size: 12px
262
+ $line-height: 30px
263
+ {{7}}font{{/7}}: {{8}}#{$font-size}/#{$line-height}{{/8}}
264
+ SASS
265
+ p {
266
+ {{1}}content{{/1}}: {{2}}"I ate 15 pies!"{{/2}};
267
+ {{3}}color{{/3}}: {{4}}teal{{/4}}; }
268
+
269
+ p.foo {
270
+ {{5}}border-color{{/5}}: {{6}}blue{{/6}};
271
+ {{7}}font{{/7}}: {{8}}12px/30px{{/8}}; }
272
+
273
+ /*@ sourceMappingURL=test.css.map */
274
+ CSS
275
+ end
276
+
277
+ def test_selectors_properties_sourcemap_scss
278
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
279
+ $width: 2px;
280
+ $translucent-red: rgba(255, 0, 0, 0.5);
281
+ a {
282
+ .special {
283
+ {{7}}color{{/7}}: {{8}}red{{/8}};
284
+ &:hover {
285
+ {{9}}foo{{/9}}: {{10}}bar{{/10}};
286
+ {{11}}cursor{{/11}}: {{12}}e + -resize{{/12}};
287
+ {{13}}color{{/13}}: {{14}}opacify($translucent-red, 0.3){{/14}};
288
+ }
289
+ &:after {
290
+ {{15}}content{{/15}}: {{16}}"I ate #{5 + 10} pies #{$width} thick!"{{/16}};
291
+ }
292
+ }
293
+ &:active {
294
+ {{17}}color{{/17}}: {{18}}#010203 + #040506{{/18}};
295
+ {{19}}border{{/19}}: {{20}}$width solid black{{/20}};
296
+ }
297
+ /* SOME COMMENT */
298
+ {{1}}font{{/1}}: {{2}}2px/3px {{/2}}{
299
+ {{3}}family{{/3}}: {{4}}fantasy{{/4}};
300
+ {{5}}size{{/5}}: {{6}}1em + (2em * 3){{/6}};
301
+ }
302
+ }
303
+ SCSS
304
+ a {
305
+ /* SOME COMMENT */
306
+ {{1}}font{{/1}}: {{2}}2px/3px{{/2}};
307
+ {{3}}font-family{{/3}}: {{4}}fantasy{{/4}};
308
+ {{5}}font-size{{/5}}: {{6}}7em{{/6}}; }
309
+ a .special {
310
+ {{7}}color{{/7}}: {{8}}red{{/8}}; }
311
+ a .special:hover {
312
+ {{9}}foo{{/9}}: {{10}}bar{{/10}};
313
+ {{11}}cursor{{/11}}: {{12}}e-resize{{/12}};
314
+ {{13}}color{{/13}}: {{14}}rgba(255, 0, 0, 0.8){{/14}}; }
315
+ a .special:after {
316
+ {{15}}content{{/15}}: {{16}}"I ate 15 pies 2px thick!"{{/16}}; }
317
+ a:active {
318
+ {{17}}color{{/17}}: {{18}}#050709{{/18}};
319
+ {{19}}border{{/19}}: {{20}}2px solid black{{/20}}; }
320
+
321
+ /*@ sourceMappingURL=test.css.map */
322
+ CSS
323
+ end
324
+
325
+ def test_selectors_properties_sourcemap_sass
326
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
327
+ $width: 2px
328
+ $translucent-red: rgba(255, 0, 0, 0.5)
329
+ a
330
+ .special
331
+ {{7}}color{{/7}}: {{8}}red{{/8}}
332
+ &:hover
333
+ {{9}}foo{{/9}}: {{10}}bar{{/10}}
334
+ {{11}}cursor{{/11}}: {{12}}e + -resize{{/12}}
335
+ {{13}}color{{/13}}: {{14}}opacify($translucent-red, 0.3){{/14}}
336
+ &:after
337
+ {{15}}content{{/15}}: {{16}}"I ate #{5 + 10} pies #{$width} thick!"{{/16}}
338
+ &:active
339
+ {{17}}color{{/17}}: {{18}}#010203 + #040506{{/18}}
340
+ {{19}}border{{/19}}: {{20}}$width solid black{{/20}}
341
+
342
+ /* SOME COMMENT */
343
+ {{1}}font{{/1}}: {{2}}2px/3px{{/2}}
344
+ {{3}}family{{/3}}: {{4}}fantasy{{/4}}
345
+ {{5}}size{{/5}}: {{6}}1em + (2em * 3){{/6}}
346
+ SASS
347
+ a {
348
+ /* SOME COMMENT */
349
+ {{1}}font{{/1}}: {{2}}2px/3px{{/2}};
350
+ {{3}}font-family{{/3}}: {{4}}fantasy{{/4}};
351
+ {{5}}font-size{{/5}}: {{6}}7em{{/6}}; }
352
+ a .special {
353
+ {{7}}color{{/7}}: {{8}}red{{/8}}; }
354
+ a .special:hover {
355
+ {{9}}foo{{/9}}: {{10}}bar{{/10}};
356
+ {{11}}cursor{{/11}}: {{12}}e-resize{{/12}};
357
+ {{13}}color{{/13}}: {{14}}rgba(255, 0, 0, 0.8){{/14}}; }
358
+ a .special:after {
359
+ {{15}}content{{/15}}: {{16}}"I ate 15 pies 2px thick!"{{/16}}; }
360
+ a:active {
361
+ {{17}}color{{/17}}: {{18}}#050709{{/18}};
362
+ {{19}}border{{/19}}: {{20}}2px solid black{{/20}}; }
363
+
364
+ /*@ sourceMappingURL=test.css.map */
365
+ CSS
366
+ end
367
+
368
+ def test_extend_sourcemap_scss
369
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
370
+ .error {
371
+ {{1}}border{{/1}}: {{2}}1px #f00{{/2}};
372
+ {{3}}background-color{{/3}}: {{4}}#fdd{{/4}};
373
+ }
374
+ .seriousError {
375
+ @extend .error;
376
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}};
377
+ }
378
+ SCSS
379
+ .error, .seriousError {
380
+ {{1}}border{{/1}}: {{2}}1px #f00{{/2}};
381
+ {{3}}background-color{{/3}}: {{4}}#fdd{{/4}}; }
382
+
383
+ .seriousError {
384
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}}; }
385
+
386
+ /*@ sourceMappingURL=test.css.map */
387
+ CSS
388
+ end
389
+
390
+ def test_extend_sourcemap_sass
391
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
392
+ .error
393
+ {{1}}border{{/1}}: {{2}}1px #f00{{/2}}
394
+ {{3}}background-color{{/3}}: {{4}}#fdd{{/4}}
395
+
396
+ .seriousError
397
+ @extend .error
398
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}}
399
+ SASS
400
+ .error, .seriousError {
401
+ {{1}}border{{/1}}: {{2}}1px red{{/2}};
402
+ {{3}}background-color{{/3}}: {{4}}#ffdddd{{/4}}; }
403
+
404
+ .seriousError {
405
+ {{5}}border-width{{/5}}: {{6}}3px{{/6}}; }
406
+
407
+ /*@ sourceMappingURL=test.css.map */
408
+ CSS
409
+ end
410
+
411
+ def test_for_sourcemap_scss
412
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
413
+ @for $i from 1 through 3 {
414
+ .item-#{$i} { {{1}}width{{/1}}: {{2}}2em * $i{{/2}}; }
415
+ }
416
+ SCSS
417
+ .item-1 {
418
+ {{1}}width{{/1}}: {{2}}2em{{/2}}; }
419
+
420
+ .item-2 {
421
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
422
+
423
+ .item-3 {
424
+ {{1}}width{{/1}}: {{2}}6em{{/2}}; }
425
+
426
+ /*@ sourceMappingURL=test.css.map */
427
+ CSS
428
+ end
429
+
430
+ def test_for_sourcemap_sass
431
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
432
+ @for $i from 1 through 3
433
+ .item-#{$i}
434
+ {{1}}width{{/1}}: {{2}}2em * $i{{/2}}
435
+ SASS
436
+ .item-1 {
437
+ {{1}}width{{/1}}: {{2}}2em{{/2}}; }
438
+
439
+ .item-2 {
440
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
441
+
442
+ .item-3 {
443
+ {{1}}width{{/1}}: {{2}}6em{{/2}}; }
444
+
445
+ /*@ sourceMappingURL=test.css.map */
446
+ CSS
447
+ end
448
+
449
+ def test_while_sourcemap_scss
450
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
451
+ $i: 6;
452
+ @while $i > 0 {
453
+ .item-#{$i} { {{1}}width{{/1}}: {{2}}2em * $i{{/2}}; }
454
+ $i: $i - 2;
455
+ }
456
+ SCSS
457
+ .item-6 {
458
+ {{1}}width{{/1}}: {{2}}12em{{/2}}; }
459
+
460
+ .item-4 {
461
+ {{1}}width{{/1}}: {{2}}8em{{/2}}; }
462
+
463
+ .item-2 {
464
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
465
+
466
+ /*@ sourceMappingURL=test.css.map */
467
+ CSS
468
+ end
469
+
470
+ def test_while_sourcemap_sass
471
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
472
+ $i: 6
473
+ @while $i > 0
474
+ .item-#{$i}
475
+ {{1}}width{{/1}}: {{2}}2em * $i{{/2}}
476
+ $i: $i - 2
477
+ SASS
478
+ .item-6 {
479
+ {{1}}width{{/1}}: {{2}}12em{{/2}}; }
480
+
481
+ .item-4 {
482
+ {{1}}width{{/1}}: {{2}}8em{{/2}}; }
483
+
484
+ .item-2 {
485
+ {{1}}width{{/1}}: {{2}}4em{{/2}}; }
486
+
487
+ /*@ sourceMappingURL=test.css.map */
488
+ CSS
489
+ end
490
+
491
+ def test_each_sourcemap_scss
492
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
493
+ @each $animal in puma, sea-slug, egret, salamander {
494
+ .#{$animal}-icon {
495
+ {{1}}background-image{{/1}}: {{2}}url('/images/#{$animal}.png'){{/2}};
496
+ }
497
+ }
498
+ SCSS
499
+ .puma-icon {
500
+ {{1}}background-image{{/1}}: {{2}}url("/images/puma.png"){{/2}}; }
501
+
502
+ .sea-slug-icon {
503
+ {{1}}background-image{{/1}}: {{2}}url("/images/sea-slug.png"){{/2}}; }
504
+
505
+ .egret-icon {
506
+ {{1}}background-image{{/1}}: {{2}}url("/images/egret.png"){{/2}}; }
507
+
508
+ .salamander-icon {
509
+ {{1}}background-image{{/1}}: {{2}}url("/images/salamander.png"){{/2}}; }
510
+
511
+ /*@ sourceMappingURL=test.css.map */
512
+ CSS
513
+ end
514
+
515
+ def test_each_sourcemap_sass
516
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
517
+ @each $animal in puma, sea-slug, egret, salamander
518
+ .#{$animal}-icon
519
+ {{1}}background-image{{/1}}: {{2}}url('/images/#{$animal}.png'){{/2}}
520
+ SASS
521
+ .puma-icon {
522
+ {{1}}background-image{{/1}}: {{2}}url("/images/puma.png"){{/2}}; }
523
+
524
+ .sea-slug-icon {
525
+ {{1}}background-image{{/1}}: {{2}}url("/images/sea-slug.png"){{/2}}; }
526
+
527
+ .egret-icon {
528
+ {{1}}background-image{{/1}}: {{2}}url("/images/egret.png"){{/2}}; }
529
+
530
+ .salamander-icon {
531
+ {{1}}background-image{{/1}}: {{2}}url("/images/salamander.png"){{/2}}; }
532
+
533
+ /*@ sourceMappingURL=test.css.map */
534
+ CSS
535
+ end
536
+
537
+ def test_mixin_sourcemap_scss
538
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
539
+ @mixin large-text {
540
+ font: {
541
+ {{1}}size{{/1}}: {{2}}20px{{/2}};
542
+ {{3}}weight{{/3}}: {{4}}bold{{/4}};
543
+ }
544
+ {{5}}color{{/5}}: {{6}}#ff0000{{/6}};
545
+ }
546
+
547
+ .page-title {
548
+ @include large-text;
549
+ {{7}}padding{{/7}}: {{8}}4px{{/8}};
550
+ }
551
+
552
+ @mixin dashed-border($color, $width: {{24}}1in{{/24}}) {
553
+ border: {
554
+ {{9}}color{{/9}}: $color;
555
+ {{11}}width{{/11}}: $width;
556
+ {{13}}style{{/13}}: {{14}}dashed{{/14}};
557
+ }
558
+ }
559
+
560
+ p { @include dashed-border({{10}}blue{{/10}}); }
561
+ h1 { @include dashed-border({{25}}blue{{/25}}, {{26}}2in{{/26}}); }
562
+
563
+ @mixin box-shadow($shadows...) {
564
+ {{18}}-moz-box-shadow{{/18}}: {{19}}$shadows{{/19}};
565
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}$shadows{{/21}};
566
+ {{22}}box-shadow{{/22}}: {{23}}$shadows{{/23}};
567
+ }
568
+
569
+ .shadows {
570
+ @include box-shadow(0px 4px 5px #666, 2px 6px 10px #999);
571
+ }
572
+ SCSS
573
+ .page-title {
574
+ {{1}}font-size{{/1}}: {{2}}20px{{/2}};
575
+ {{3}}font-weight{{/3}}: {{4}}bold{{/4}};
576
+ {{5}}color{{/5}}: {{6}}#ff0000{{/6}};
577
+ {{7}}padding{{/7}}: {{8}}4px{{/8}}; }
578
+
579
+ p {
580
+ {{9}}border-color{{/9}}: {{10}}blue{{/10}};
581
+ {{11}}border-width{{/11}}: {{24}}1in{{/24}};
582
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
583
+
584
+ h1 {
585
+ {{9}}border-color{{/9}}: {{25}}blue{{/25}};
586
+ {{11}}border-width{{/11}}: {{26}}2in{{/26}};
587
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
588
+
589
+ .shadows {
590
+ {{18}}-moz-box-shadow{{/18}}: {{19}}0px 4px 5px #666666, 2px 6px 10px #999999{{/19}};
591
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}0px 4px 5px #666666, 2px 6px 10px #999999{{/21}};
592
+ {{22}}box-shadow{{/22}}: {{23}}0px 4px 5px #666666, 2px 6px 10px #999999{{/23}}; }
593
+
594
+ /*@ sourceMappingURL=test.css.map */
595
+ CSS
596
+ end
597
+
598
+ def test_mixin_sourcemap_sass
599
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
600
+ =large-text
601
+ :font
602
+ {{1}}size{{/1}}: {{2}}20px{{/2}}
603
+ {{3}}weight{{/3}}: {{4}}bold{{/4}}
604
+ {{5}}color{{/5}}: {{6}}#ff0000{{/6}}
605
+
606
+ .page-title
607
+ +large-text
608
+ {{7}}padding{{/7}}: {{8}}4px{{/8}}
609
+
610
+ =dashed-border($color, $width: {{24}}1in{{/24}})
611
+ border:
612
+ {{9}}color{{/9}}: $color
613
+ {{11}}width{{/11}}: $width
614
+ {{13}}style{{/13}}: {{14}}dashed{{/14}}
615
+
616
+ p
617
+ +dashed-border({{10}}blue{{/10}})
618
+
619
+ h1
620
+ +dashed-border({{25}}blue{{/25}}, {{26}}2in{{/26}})
621
+
622
+ =box-shadow($shadows...)
623
+ {{18}}-moz-box-shadow{{/18}}: {{19}}$shadows{{/19}}
624
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}$shadows{{/21}}
625
+ {{22}}box-shadow{{/22}}: {{23}}$shadows{{/23}}
626
+
627
+ .shadows
628
+ +box-shadow(0px 4px 5px #666, 2px 6px 10px #999)
629
+ SASS
630
+ .page-title {
631
+ {{1}}font-size{{/1}}: {{2}}20px{{/2}};
632
+ {{3}}font-weight{{/3}}: {{4}}bold{{/4}};
633
+ {{5}}color{{/5}}: {{6}}red{{/6}};
634
+ {{7}}padding{{/7}}: {{8}}4px{{/8}}; }
635
+
636
+ p {
637
+ {{9}}border-color{{/9}}: {{10}}blue{{/10}};
638
+ {{11}}border-width{{/11}}: {{24}}1in{{/24}};
639
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
640
+
641
+ h1 {
642
+ {{9}}border-color{{/9}}: {{25}}blue{{/25}};
643
+ {{11}}border-width{{/11}}: {{26}}2in{{/26}};
644
+ {{13}}border-style{{/13}}: {{14}}dashed{{/14}}; }
645
+
646
+ .shadows {
647
+ {{18}}-moz-box-shadow{{/18}}: {{19}}0px 4px 5px #666666, 2px 6px 10px #999999{{/19}};
648
+ {{20}}-webkit-box-shadow{{/20}}: {{21}}0px 4px 5px #666666, 2px 6px 10px #999999{{/21}};
649
+ {{22}}box-shadow{{/22}}: {{23}}0px 4px 5px #666666, 2px 6px 10px #999999{{/23}}; }
650
+
651
+ /*@ sourceMappingURL=test.css.map */
652
+ CSS
653
+ end
654
+
655
+ def test_function_sourcemap_scss
656
+ assert_parses_with_mapping <<'SCSS', <<'CSS'
657
+ $grid-width: 20px;
658
+ $gutter-width: 5px;
659
+
660
+ @function grid-width($n) {
661
+ @return $n * $grid-width + ($n - 1) * $gutter-width;
662
+ }
663
+ sidebar { {{1}}width{{/1}}: {{2}}grid-width(5){{/2}}; }
664
+ SCSS
665
+ sidebar {
666
+ {{1}}width{{/1}}: {{2}}120px{{/2}}; }
667
+
668
+ /*@ sourceMappingURL=test.css.map */
669
+ CSS
670
+ end
671
+
672
+ def test_function_sourcemap_sass
673
+ assert_parses_with_mapping <<'SASS', <<'CSS', :syntax => :sass
674
+ $grid-width: 20px
675
+ $gutter-width: 5px
676
+
677
+ @function grid-width($n)
678
+ @return $n * $grid-width + ($n - 1) * $gutter-width
679
+
680
+ sidebar
681
+ {{1}}width{{/1}}: {{2}}grid-width(5){{/2}}
682
+ SASS
683
+ sidebar {
684
+ {{1}}width{{/1}}: {{2}}120px{{/2}}; }
685
+
686
+ /*@ sourceMappingURL=test.css.map */
687
+ CSS
688
+ end
689
+
690
+ # Regression tests
691
+
692
+ def test_properties_sass
693
+ assert_parses_with_mapping <<SASS, <<CSS, :syntax => :sass
694
+ .foo
695
+ :{{1}}name{{/1}} {{2}}value{{/2}}
696
+ {{3}}name{{/3}}: {{4}}value{{/4}}
697
+ :{{5}}name{{/5}} {{6}}value{{/6}}
698
+ {{7}}name{{/7}}: {{8}}value{{/8}}
699
+ SASS
700
+ .foo {
701
+ {{1}}name{{/1}}: {{2}}value{{/2}};
702
+ {{3}}name{{/3}}: {{4}}value{{/4}};
703
+ {{5}}name{{/5}}: {{6}}value{{/6}};
704
+ {{7}}name{{/7}}: {{8}}value{{/8}}; }
705
+
706
+ /*@ sourceMappingURL=test.css.map */
707
+ CSS
708
+ end
709
+
710
+ private
711
+
712
+ ANNOTATION_REGEX = /\{\{(\/?)([^}]+)\}\}/
713
+
714
+ def build_ranges(text, file_name = nil)
715
+ ranges = Hash.new {|h, k| h[k] = []}
716
+ start_positions = {}
717
+ text.split("\n").each_with_index do |line_text, line|
718
+ line += 1 # lines shoud be 1-based
719
+ match_start = 0
720
+ while match = line_text.match(ANNOTATION_REGEX)
721
+ closing = !match[1].empty?
722
+ name = match[2]
723
+ match_offsets = match.offset(0)
724
+ offset = match_offsets[0] + 1 # Offsets are 1-based in source maps.
725
+ assert(!closing || start_positions[name], "Closing annotation #{name} found before opening one.")
726
+ position = Sass::Source::Position.new(line, offset)
727
+ if closing
728
+ ranges[name] << Sass::Source::Range.new(start_positions[name], position, file_name)
729
+ start_positions.delete name
730
+ else
731
+ assert(!start_positions[name], "Overlapping range annotation #{name} encountered on line #{line}")
732
+ start_positions[name] = position
733
+ end
734
+ line_text.slice!(match_offsets[0], match_offsets[1] - match_offsets[0])
735
+ end
736
+ end
737
+ ranges
738
+ end
739
+
740
+ def build_mapping_from_annotations(source, css, source_file_name)
741
+ source_ranges = build_ranges(source, source_file_name)
742
+ target_ranges = build_ranges(css)
743
+ map = Sass::Source::Map.new
744
+ mappings = Sass::Util.flatten(source_ranges.map do |(name, sources)|
745
+ assert(sources.length == 1, "#{sources.length} source ranges encountered for annotation #{name}")
746
+ assert(target_ranges[name], "No target ranges for annotation #{name}")
747
+ target_ranges[name].map {|target_range| [sources.first, target_range]}
748
+ end, 1).
749
+ sort_by {|(source, target)| [target.start_pos.line, target.start_pos.offset]}.
750
+ each {|(source, target)| map.add(source, target)}
751
+ map
752
+ end
753
+
754
+ def assert_parses_with_mapping(source, css, options={})
755
+ options[:syntax] ||= :scss
756
+ input_filename = filename_for_test(options[:syntax])
757
+ mapping = build_mapping_from_annotations(source, css, input_filename)
758
+ source.gsub!(ANNOTATION_REGEX, "")
759
+ css.gsub!(ANNOTATION_REGEX, "")
760
+ rendered, sourcemap = render_with_sourcemap(source, options)
761
+ assert_equal css.rstrip, rendered.rstrip
762
+ assert_sourcemaps_equal source, css, mapping, sourcemap
763
+ end
764
+
765
+ def assert_positions_equal(expected, actual, lines, message = nil)
766
+ prefix = message ? message + ": " : ""
767
+ assert_equal(expected.line, actual.line, prefix +
768
+ "Expected #{expected.inspect} but was #{actual.inspect}")
769
+ assert_equal(expected.offset, actual.offset, prefix +
770
+ "Expected #{expected.inspect} but was #{actual.inspect}\n" +
771
+ lines[actual.line - 1] + "\n" + ("-" * (actual.offset - 1)) + "^")
772
+ end
773
+
774
+ def assert_ranges_equal(expected, actual, lines, prefix)
775
+ assert_positions_equal(expected.start_pos, actual.start_pos, lines, prefix + " start position")
776
+ assert_positions_equal(expected.end_pos, actual.end_pos, lines, prefix + " end position")
777
+ assert_equal(expected.file, actual.file)
778
+ end
779
+
780
+ def assert_sourcemaps_equal(source, css, expected, actual)
781
+ assert_equal(expected.data.length, actual.data.length, <<MESSAGE)
782
+ Wrong number of mappings. Expected:
783
+ #{dump_sourcemap_as_expectation(source, css, expected).gsub(/^/, '| ')}
784
+
785
+ Actual:
786
+ #{dump_sourcemap_as_expectation(source, css, actual).gsub(/^/, '| ')}
787
+ MESSAGE
788
+ source_lines = source.split("\n")
789
+ css_lines = css.split("\n")
790
+ expected.data.zip(actual.data) do |expected_mapping, actual_mapping|
791
+ assert_ranges_equal(expected_mapping.input, actual_mapping.input, source_lines, "Input")
792
+ assert_ranges_equal(expected_mapping.output, actual_mapping.output, css_lines, "Output")
793
+ end
794
+ end
795
+
796
+ def assert_parses_with_sourcemap(source, css, sourcemap_json, options={})
797
+ rendered, sourcemap = render_with_sourcemap(source, options)
798
+ assert_equal css.rstrip, rendered.rstrip
799
+ assert_equal sourcemap_json.rstrip, sourcemap.to_json(options[:output] || "test.css")
800
+ end
801
+
802
+ def render_with_sourcemap(source, options={})
803
+ options[:syntax] ||= :scss
804
+ munge_filename options
805
+ engine = Sass::Engine.new(source, options)
806
+ engine.options[:cache] = false
807
+ sourcemap_path = Sass::Util.sourcemap_name(options[:output] || "test.css")
808
+ engine.render_with_sourcemap File.basename(sourcemap_path)
809
+ end
810
+
811
+ def dump_sourcemap_as_expectation(source, css, sourcemap)
812
+ mappings_to_annotations(source, sourcemap.data.map {|d| d.input}) + "\n\n" +
813
+ "=" * 20 + " maps to:\n\n" +
814
+ mappings_to_annotations(css, sourcemap.data.map {|d| d.output})
815
+ end
816
+
817
+ def mappings_to_annotations(source, ranges)
818
+ additional_offsets = Hash.new(0)
819
+ lines = source.split("\n")
820
+
821
+ add_annotation = lambda do |pos, str|
822
+ line_num = pos.line - 1
823
+ line = lines[line_num]
824
+ offset = pos.offset + additional_offsets[line_num] - 1
825
+ line << " " * (offset - line.length) if offset > line.length
826
+ line.insert(offset, str)
827
+ additional_offsets[line_num] += str.length
828
+ end
829
+
830
+ ranges.each_with_index do |range, i|
831
+ add_annotation[range.start_pos, "{{#{i + 1}}}"]
832
+ add_annotation[range.end_pos, "{{/#{i + 1}}}"]
833
+ end
834
+
835
+ return lines.join("\n")
836
+ end
837
+ end