sassmagic 0.1.0

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.
@@ -0,0 +1,1594 @@
1
+ # sass文件重写,主要修改了属性解析函数,编译文件函数,文件涉及:sass.rb,to_css.rb,base.rb,sass_convert.rb,sass_scss.rb
2
+ module Sass::Script::Functions::UserFunctions
3
+ def option(name)
4
+ Sass::Script::Value::String.new(@options[name.value.to_sym].to_s)
5
+ end
6
+
7
+ def set_a_variable(name, value)
8
+ environment.set_var(name.value, value)
9
+ return Sass::Script::Value::Null.new
10
+ end
11
+
12
+ def set_a_global_variable(name, value)
13
+ environment.set_global_var(name.value, value)
14
+ return Sass::Script::Value::Null.new
15
+ end
16
+
17
+ def get_a_variable(name)
18
+ environment.var(name.value) || Sass::Script::Value::String.new("undefined")
19
+ end
20
+ end
21
+
22
+ module Sass::Script::Functions
23
+ include Sass::Script::Functions::UserFunctions
24
+ def reverse(string)
25
+ assert_type string, :String
26
+ Sass::Script::Value::String.new(string.value.reverse)
27
+ end
28
+ declare :reverse, [:string]
29
+ def pxtorem(string)
30
+ assert_type string, :String
31
+ Sass::Script::Value::String.new(string+'rem')
32
+ end
33
+ declare :pxtorem, [:string]
34
+ # Dynamically calls a function. This can call user-defined
35
+ # functions, built-in functions, or plain CSS functions. It will
36
+ # pass along all arguments, including keyword arguments, to the
37
+ # called function.
38
+ #
39
+ # @example
40
+ # call(rgb, 10, 100, 255) => #0a64ff
41
+ # call(scale-color, #0a64ff, $lightness: -10%) => #0058ef
42
+ #
43
+ # $fn: nth;
44
+ # call($fn, (a b c), 2) => b
45
+ #
46
+ # @overload call($name, $args...)
47
+ # @param $name [String] The name of the function to call.
48
+ def call(name, *args)
49
+ assert_type name, :String, :name
50
+ kwargs = args.last.is_a?(Hash) ? args.pop : {}
51
+ funcall = Sass::Script::Tree::Funcall.new(
52
+ name.value,
53
+ args.map {|a| Sass::Script::Tree::Literal.new(a)},
54
+ Sass::Util.map_vals(kwargs) {|v| Sass::Script::Tree::Literal.new(v)},
55
+ nil,
56
+ nil)
57
+ funcall.options = options
58
+ perform(funcall)
59
+ end
60
+ declare :call, [:name], :var_args => true, :var_kwargs => true
61
+ end
62
+
63
+
64
+
65
+ # 重写编译
66
+ module Sass
67
+ class << self
68
+ # @private
69
+ attr_accessor :tests_running
70
+ end
71
+
72
+ # The global load paths for Sass files. This is meant for plugins and
73
+ # libraries to register the paths to their Sass stylesheets to that they may
74
+ # be `@imported`. This load path is used by every instance of {Sass::Engine}.
75
+ # They are lower-precedence than any load paths passed in via the
76
+ # {file:SASS_REFERENCE.md#load_paths-option `:load_paths` option}.
77
+ #
78
+ # If the `SASS_PATH` environment variable is set,
79
+ # the initial value of `load_paths` will be initialized based on that.
80
+ # The variable should be a colon-separated list of path names
81
+ # (semicolon-separated on Windows).
82
+ #
83
+ # Note that files on the global load path are never compiled to CSS
84
+ # themselves, even if they aren't partials. They exist only to be imported.
85
+ #
86
+ # @example
87
+ # Sass.load_paths << File.dirname(__FILE__ + '/sass')
88
+ # @return [Array<String, Pathname, Sass::Importers::Base>]
89
+ def self.load_paths
90
+ @load_paths ||= if ENV['SASS_PATH']
91
+ ENV['SASS_PATH'].split(Sass::Util.windows? ? ';' : ':')
92
+ else
93
+ []
94
+ end
95
+ end
96
+
97
+ # Compile a Sass or SCSS string to CSS.
98
+ # Defaults to SCSS.
99
+ #
100
+ # @param contents [String] The contents of the Sass file.
101
+ # @param options [{Symbol => Object}] An options hash;
102
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
103
+ # @raise [Sass::SyntaxError] if there's an error in the document
104
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
105
+ # cannot be converted to UTF-8
106
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
107
+ def self.compile(contents, options = {})
108
+ options[:syntax] ||= :scss
109
+ Engine.new(contents, options).to_css
110
+ end
111
+
112
+ # Compile a file on disk to CSS.
113
+ #
114
+ # @raise [Sass::SyntaxError] if there's an error in the document
115
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
116
+ # cannot be converted to UTF-8
117
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
118
+ #
119
+ # @overload compile_file(filename, options = {})
120
+ # Return the compiled CSS rather than writing it to a file.
121
+ #
122
+ # @param filename [String] The path to the Sass, SCSS, or CSS file on disk.
123
+ # @param options [{Symbol => Object}] An options hash;
124
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
125
+ # @return [String] The compiled CSS.
126
+ #
127
+ # @overload compile_file(filename, css_filename, options = {})
128
+ # Write the compiled CSS to a file.
129
+ #
130
+ # @param filename [String] The path to the Sass, SCSS, or CSS file on disk.
131
+ # @param options [{Symbol => Object}] An options hash;
132
+ # see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
133
+ # @param css_filename [String] The location to which to write the compiled CSS.
134
+ def self.compile_file(filename, *args)
135
+ # debugger
136
+ # puts filename
137
+ # puts args
138
+ # Sass.logger(filename, args)
139
+ ctx = Sass::Script::Functions::EvaluationContext.new(Sass::Environment.new(nil, {}))
140
+
141
+ # print 'star compile file:'
142
+ #加载json
143
+ # 获取设置
144
+ $configHash = ctx.load_json(File.expand_path("#{File.dirname(filename)}/sassmagic.json")) || {}
145
+ # puts $configHash
146
+ options = args.last.is_a?(Hash) ? args.pop : {}
147
+ options = options.merge $configHash
148
+ css_filename = args.shift
149
+ # debugger
150
+ #是否需要额外输出样式表
151
+ if options.has_key?"outputExtra"
152
+ options["outputExtra"] ||= []
153
+ options["outputExtra"].each {|v|
154
+
155
+ extra_filename = css_filename + ''
156
+ #替换成1x 2x 3x
157
+ extra_filename.gsub!(/\.css$/ , '.'+v+'.css')
158
+ if extra_filename
159
+ options[:css_filename] = extra_filename
160
+ options["multiple"] = v
161
+ result = Sass::Engine.for_file(filename, options).render
162
+ # open(css_filename, "w") {|css_file| css_file.write(result)}
163
+ File.open(extra_filename, 'w') {|css_file| css_file.write(result)}
164
+ nil
165
+ else
166
+ result = Sass::Engine.for_file(filename, options).render
167
+ result
168
+ end
169
+ }
170
+ end
171
+
172
+ options.delete("multiple")
173
+ result = Sass::Engine.for_file(filename, options).render
174
+ if css_filename
175
+ options[:css_filename] ||= css_filename
176
+ open(css_filename, "w") {|css_file| css_file.write(result)}
177
+ nil
178
+ else
179
+ result
180
+ end
181
+ end
182
+
183
+
184
+ end
185
+
186
+
187
+ #重写属性解析to_css.rb
188
+ # A visitor for converting a Sass tree into CSS.
189
+ class Sass::Tree::Visitors::ToCss < Sass::Tree::Visitors::Base
190
+ # The source mapping for the generated CSS file. This is only set if
191
+ # `build_source_mapping` is passed to the constructor and \{Sass::Engine#render} has been
192
+ # run.
193
+ attr_reader :source_mapping
194
+
195
+ # @param build_source_mapping [Boolean] Whether to build a
196
+ # \{Sass::Source::Map} while creating the CSS output. The mapping will
197
+ # be available from \{#source\_mapping} after the visitor has completed.
198
+ def initialize(build_source_mapping = false)
199
+ @tabs = 0
200
+ @line = 1
201
+ @offset = 1
202
+ @result = ""
203
+ @source_mapping = Sass::Source::Map.new if build_source_mapping
204
+ end
205
+
206
+ # Runs the visitor on `node`.
207
+ #
208
+ # @param node [Sass::Tree::Node] The root node of the tree to convert to CSS>
209
+ # @return [String] The CSS output.
210
+ def visit(node)
211
+ super
212
+ rescue Sass::SyntaxError => e
213
+ e.modify_backtrace(:filename => node.filename, :line => node.line)
214
+ raise e
215
+ end
216
+
217
+ protected
218
+
219
+ def with_tabs(tabs)
220
+ old_tabs, @tabs = @tabs, tabs
221
+ yield
222
+ ensure
223
+ @tabs = old_tabs
224
+ end
225
+
226
+ # Associate all output produced in a block with a given node. Used for source
227
+ # mapping.
228
+ def for_node(node, attr_prefix = nil)
229
+ return yield unless @source_mapping
230
+ start_pos = Sass::Source::Position.new(@line, @offset)
231
+ yield
232
+
233
+ range_attr = attr_prefix ? :"#{attr_prefix}_source_range" : :source_range
234
+ return if node.invisible? || !node.send(range_attr)
235
+ source_range = node.send(range_attr)
236
+ target_end_pos = Sass::Source::Position.new(@line, @offset)
237
+ target_range = Sass::Source::Range.new(start_pos, target_end_pos, nil)
238
+ @source_mapping.add(source_range, target_range)
239
+ end
240
+
241
+ # Move the output cursor back `chars` characters.
242
+ def erase!(chars)
243
+ return if chars == 0
244
+ str = @result.slice!(-chars..-1)
245
+ newlines = str.count("\n")
246
+ if newlines > 0
247
+ @line -= newlines
248
+ @offset = @result[@result.rindex("\n") || 0..-1].size
249
+ else
250
+ @offset -= chars
251
+ end
252
+ end
253
+
254
+ # Avoid allocating lots of new strings for `#output`. This is important
255
+ # because `#output` is called all the time.
256
+ NEWLINE = "\n"
257
+
258
+ # Add `s` to the output string and update the line and offset information
259
+ # accordingly.
260
+ def output(s)
261
+ if @lstrip
262
+ s = s.gsub(/\A\s+/, "")
263
+ @lstrip = false
264
+ end
265
+
266
+ newlines = s.count(NEWLINE)
267
+ if newlines > 0
268
+ @line += newlines
269
+ @offset = s[s.rindex(NEWLINE)..-1].size
270
+ else
271
+ @offset += s.size
272
+ end
273
+
274
+ @result << s
275
+ end
276
+
277
+ # Strip all trailing whitespace from the output string.
278
+ def rstrip!
279
+ erase! @result.length - 1 - (@result.rindex(/[^\s]/) || -1)
280
+ end
281
+
282
+ # lstrip the first output in the given block.
283
+ def lstrip
284
+ old_lstrip = @lstrip
285
+ @lstrip = true
286
+ yield
287
+ ensure
288
+ @lstrip = @lstrip && old_lstrip
289
+ end
290
+
291
+ # Prepend `prefix` to the output string.
292
+ def prepend!(prefix)
293
+ @result.insert 0, prefix
294
+ return unless @source_mapping
295
+
296
+ line_delta = prefix.count("\n")
297
+ offset_delta = prefix.gsub(/.*\n/, '').size
298
+ @source_mapping.shift_output_offsets(offset_delta)
299
+ @source_mapping.shift_output_lines(line_delta)
300
+ end
301
+
302
+ def visit_root(node)
303
+ #输出样式表
304
+ # debugger
305
+ node.children.each do |child|
306
+ next if child.invisible?
307
+ visit(child)
308
+ unless node.style == :compressed
309
+ output "\n"
310
+ if child.is_a?(Sass::Tree::DirectiveNode) && child.has_children && !child.bubbles?
311
+ output "\n"
312
+ end
313
+ end
314
+ end
315
+ rstrip!
316
+ return "" if @result.empty?
317
+
318
+ output "\n"
319
+
320
+ unless Sass::Util.ruby1_8? || @result.ascii_only?
321
+ if node.style == :compressed
322
+ # A byte order mark is sufficient to tell browsers that this
323
+ # file is UTF-8 encoded, and will override any other detection
324
+ # methods as per http://encoding.spec.whatwg.org/#decode-and-encode.
325
+ prepend! "\uFEFF"
326
+ else
327
+ prepend! "@charset \"UTF-8\";\n"
328
+ end
329
+ end
330
+
331
+ @result
332
+ rescue Sass::SyntaxError => e
333
+ e.sass_template ||= node.template
334
+ raise e
335
+ end
336
+
337
+ def visit_charset(node)
338
+ for_node(node) {output("@charset \"#{node.name}\";")}
339
+ end
340
+
341
+ def visit_comment(node)
342
+ return if node.invisible?
343
+ spaces = (' ' * [@tabs - node.resolved_value[/^ */].size, 0].max)
344
+
345
+ content = node.resolved_value.gsub(/^/, spaces)
346
+ if node.type == :silent
347
+ content.gsub!(%r{^(\s*)//(.*)$}) {|md| "#{$1}/*#{$2} */"}
348
+ end
349
+ if (node.style == :compact || node.style == :compressed) && node.type != :loud
350
+ content.gsub!(/\n +(\* *(?!\/))?/, ' ')
351
+ end
352
+ for_node(node) {output(content)}
353
+ end
354
+
355
+ # @comment
356
+ # rubocop:disable MethodLength
357
+ def visit_directive(node)
358
+ was_in_directive = @in_directive
359
+ tab_str = ' ' * @tabs
360
+ if !node.has_children || node.children.empty?
361
+ output(tab_str)
362
+ for_node(node) {output(node.resolved_value)}
363
+ output(!node.has_children ? ";" : " {}")
364
+ return
365
+ end
366
+
367
+ @in_directive = @in_directive || !node.is_a?(Sass::Tree::MediaNode)
368
+ output(tab_str) if node.style != :compressed
369
+ for_node(node) {output(node.resolved_value)}
370
+ output(node.style == :compressed ? "{" : " {")
371
+ output(node.style == :compact ? ' ' : "\n") if node.style != :compressed
372
+
373
+ was_prop = false
374
+ first = true
375
+ node.children.each do |child|
376
+ next if child.invisible?
377
+ if node.style == :compact
378
+ if child.is_a?(Sass::Tree::PropNode)
379
+ with_tabs(first || was_prop ? 0 : @tabs + 1) do
380
+ visit(child)
381
+ output(' ')
382
+ end
383
+ else
384
+ if was_prop
385
+ erase! 1
386
+ output "\n"
387
+ end
388
+
389
+ if first
390
+ lstrip {with_tabs(@tabs + 1) {visit(child)}}
391
+ else
392
+ with_tabs(@tabs + 1) {visit(child)}
393
+ end
394
+
395
+ rstrip!
396
+ output "\n"
397
+ end
398
+ was_prop = child.is_a?(Sass::Tree::PropNode)
399
+ first = false
400
+ elsif node.style == :compressed
401
+ output(was_prop ? ";" : "")
402
+ with_tabs(0) {visit(child)}
403
+ was_prop = child.is_a?(Sass::Tree::PropNode)
404
+ else
405
+ with_tabs(@tabs + 1) {visit(child)}
406
+ output "\n"
407
+ end
408
+ end
409
+ rstrip!
410
+ if node.style == :expanded
411
+ output("\n#{tab_str}")
412
+ elsif node.style != :compressed
413
+ output(" ")
414
+ end
415
+ output("}")
416
+ ensure
417
+ @in_directive = was_in_directive
418
+ end
419
+ # @comment
420
+ # rubocop:enable MethodLength
421
+
422
+ def visit_media(node)
423
+ with_tabs(@tabs + node.tabs) {visit_directive(node)}
424
+ output("\n") if node.style != :compressed && node.group_end
425
+ end
426
+
427
+ def visit_supports(node)
428
+ visit_media(node)
429
+ end
430
+
431
+ def visit_cssimport(node)
432
+ visit_directive(node)
433
+ end
434
+ #属性重写开始
435
+ def visit_prop(node)
436
+ opt = node.options
437
+ if $configHash == nil || $configHash == {}
438
+ #读取配置文件
439
+ ctx = Sass::Script::Functions::EvaluationContext.new(Sass::Environment.new(nil, {}))
440
+ #加载json
441
+ # 获取设置
442
+ filename = opt[:filename] || ''
443
+ $configHash = ctx.load_json(File.expand_path("#{File.dirname(filename)}/sassmagic.json")) || {}
444
+ opt = opt.merge $configHash
445
+ else
446
+ opt = opt.merge $configHash
447
+ end
448
+
449
+ multiple = opt["multiple"] || false
450
+ devicePixelRatio = opt["devicePixelRatio"] || 1
451
+ # debugger
452
+ #干预输出
453
+ if multiple
454
+ # 过滤掉不需要处理的name
455
+ isNeedChangeMultiple = true;
456
+ opt["ignore"].each{|v|
457
+ # debugger
458
+ if $classignore
459
+ isNeedChangeMultiple = false
460
+ end
461
+ }
462
+ if isNeedChangeMultiple
463
+ treated_prop = pxttodpr(node.resolved_value,devicePixelRatio,multiple)
464
+ else
465
+ treated_prop = node.resolved_value
466
+ end
467
+
468
+ else
469
+ if opt['isNeedPxToRem']
470
+ if opt["browserDefaultFontSize"].to_i != 0
471
+ configRem = opt["browserDefaultFontSize"].to_i
472
+ else
473
+ configRem = 75
474
+ end
475
+ treated_prop = pxtorem(node,node.resolved_value,configRem,opt["ignore"])
476
+ else
477
+ treated_prop = node.resolved_value
478
+ end
479
+ end
480
+
481
+
482
+
483
+ #输出属性resolved_name和值resolved_value
484
+ return if node.resolved_value.empty?
485
+ tab_str = ' ' * (@tabs + node.tabs)
486
+ output(tab_str)
487
+ for_node(node, :name) {output(node.resolved_name)}
488
+ if node.style == :compressed
489
+ output(":")
490
+ for_node(node, :value) {output(treated_prop)}
491
+ else
492
+ output(": ")
493
+ for_node(node, :value) {output(treated_prop)}
494
+ output(";")
495
+ end
496
+ end
497
+
498
+ def pxtorem(node,str,rem,ignore)
499
+ ret = str
500
+ ignore = ignore || []
501
+ # 过滤掉不需要处理的name
502
+ isNeedChangeValue = true;
503
+ ignore.each{|v|
504
+ # debugger
505
+ if $classignore && (node.resolved_name.include? v)
506
+ isNeedChangeValue = false
507
+ end
508
+ }
509
+ if isNeedChangeValue
510
+ #字符串替换px
511
+ #'0 1px.jpg 1px 2px;3pxabc 4px'.scan(/([\.\d]+)+(px)+([;\s]+|$)/)
512
+ #=> [["1", "px", " "], ["2", "px", ";"], ["4", "px", ""]]
513
+ str.scan(/([\.\d]+)+(px)+([;\s]+|$)/i){ |c,v|
514
+ if !(ignore.include? c+v) && (c != '1')
515
+ ret = ret.gsub(c.to_s,(format("%.3f",c.to_f/rem).to_f).to_s).gsub(v,'rem')
516
+ end
517
+ }
518
+ end
519
+ return ret
520
+ end
521
+
522
+ def pxttodpr(str,dpr,multiple)
523
+ ret = str
524
+ #字符串替换px
525
+ #'0 1px.jpg 1px 2px;3pxabc 4px'.scan(/([\.\d]+)+(px)+([;\s]+|$)/)
526
+ #=> [["1", "px", " "], ["2", "px", ";"], ["4", "px", ""]]
527
+ str.scan(/([\.\d]+)+(px)+([;\s]+|$)/i){ |c,v|
528
+ if c != '1'
529
+ ret = ret.gsub(c.to_s,((c.to_i/dpr.to_i) * multiple.to_i).to_s)
530
+ end
531
+ }
532
+ return ret
533
+ end
534
+
535
+ #属性重写结束
536
+ # @comment
537
+ # rubocop:disable MethodLength
538
+ def visit_rule(node)
539
+ # debugger
540
+ opt = node.options
541
+ if $configHash == nil || $configHash == {}
542
+ #读取配置文件
543
+ ctx = Sass::Script::Functions::EvaluationContext.new(Sass::Environment.new(nil, {}))
544
+ #加载json
545
+ # 获取设置
546
+ filename = opt[:filename] || ''
547
+ $configHash = ctx.load_json(File.expand_path("#{File.dirname(filename)}/sassmagic.json")) || {}
548
+ opt = opt.merge $configHash
549
+ else
550
+ opt = opt.merge $configHash
551
+ end
552
+ # 判断是否不需要rem进行自动处理,通过class
553
+ $classignore = false
554
+ opt["ignore"].each{|v|
555
+ # debugger
556
+ if (node.resolved_rules.to_s).include? v
557
+ $classignore = true
558
+ end
559
+ }
560
+
561
+ with_tabs(@tabs + node.tabs) do
562
+ rule_separator = node.style == :compressed ? ',' : ', '
563
+ line_separator =
564
+ case node.style
565
+ when :nested, :expanded; "\n"
566
+ when :compressed; ""
567
+ else; " "
568
+ end
569
+ rule_indent = ' ' * @tabs
570
+ per_rule_indent, total_indent = if [:nested, :expanded].include?(node.style)
571
+ [rule_indent, '']
572
+ else
573
+ ['', rule_indent]
574
+ end
575
+
576
+ joined_rules = node.resolved_rules.members.map do |seq|
577
+ next if seq.has_placeholder?
578
+ rule_part = seq.to_s
579
+ if node.style == :compressed
580
+ rule_part.gsub!(/([^,])\s*\n\s*/m, '\1 ')
581
+ rule_part.gsub!(/\s*([,+>])\s*/m, '\1')
582
+ rule_part.strip!
583
+ end
584
+ rule_part
585
+ end.compact.join(rule_separator)
586
+
587
+ joined_rules.lstrip!
588
+ joined_rules.gsub!(/\s*\n\s*/, "#{line_separator}#{per_rule_indent}")
589
+
590
+ old_spaces = ' ' * @tabs
591
+ if node.style != :compressed
592
+ if node.options[:debug_info] && !@in_directive
593
+ visit(debug_info_rule(node.debug_info, node.options))
594
+ output "\n"
595
+ elsif node.options[:trace_selectors]
596
+ output("#{old_spaces}/* ")
597
+ output(node.stack_trace.gsub("\n", "\n #{old_spaces}"))
598
+ output(" */\n")
599
+ elsif node.options[:line_comments]
600
+ output("#{old_spaces}/* line #{node.line}")
601
+
602
+ if node.filename
603
+ relative_filename =
604
+ if node.options[:css_filename]
605
+ begin
606
+ Sass::Util.relative_path_from(
607
+ node.filename, File.dirname(node.options[:css_filename])).to_s
608
+ rescue ArgumentError
609
+ nil
610
+ end
611
+ end
612
+ relative_filename ||= node.filename
613
+ output(", #{relative_filename}")
614
+ end
615
+
616
+ output(" */\n")
617
+ end
618
+ end
619
+
620
+ end_props, trailer, tabs = '', '', 0
621
+ if node.style == :compact
622
+ separator, end_props, bracket = ' ', ' ', ' { '
623
+ trailer = "\n" if node.group_end
624
+ elsif node.style == :compressed
625
+ separator, bracket = ';', '{'
626
+ else
627
+ tabs = @tabs + 1
628
+ separator, bracket = "\n", " {\n"
629
+ trailer = "\n" if node.group_end
630
+ end_props = (node.style == :expanded ? "\n" + old_spaces : ' ')
631
+ end
632
+ output(total_indent + per_rule_indent)
633
+ for_node(node, :selector) {output(joined_rules)}
634
+ output(bracket)
635
+
636
+ with_tabs(tabs) do
637
+ node.children.each_with_index do |child, i|
638
+ output(separator) if i > 0
639
+ visit(child)
640
+ end
641
+ end
642
+
643
+ output(end_props)
644
+ output("}" + trailer)
645
+ end
646
+ end
647
+ # @comment
648
+ # rubocop:enable MethodLength
649
+
650
+ def visit_keyframerule(node)
651
+ visit_directive(node)
652
+ end
653
+
654
+ private
655
+
656
+ def debug_info_rule(debug_info, options)
657
+ node = Sass::Tree::DirectiveNode.resolved("@media -sass-debug-info")
658
+ Sass::Util.hash_to_a(debug_info.map {|k, v| [k.to_s, v.to_s]}).each do |k, v|
659
+ rule = Sass::Tree::RuleNode.new([""])
660
+ rule.resolved_rules = Sass::Selector::CommaSequence.new(
661
+ [Sass::Selector::Sequence.new(
662
+ [Sass::Selector::SimpleSequence.new(
663
+ [Sass::Selector::Element.new(k.to_s.gsub(/[^\w-]/, "\\\\\\0"), nil)],
664
+ false)
665
+ ])
666
+ ])
667
+ prop = Sass::Tree::PropNode.new([""], Sass::Script::Value::String.new(''), :new)
668
+ prop.resolved_name = "font-family"
669
+ prop.resolved_value = Sass::SCSS::RX.escape_ident(v.to_s)
670
+ rule << prop
671
+ node << rule
672
+ end
673
+ node.options = options.merge(:debug_info => false,
674
+ :line_comments => false,
675
+ :style => :compressed)
676
+ node
677
+ end
678
+ end
679
+
680
+ require 'optparse'
681
+
682
+ # 重写base.rb 去掉require
683
+ module Sass::Exec
684
+ # The abstract base class for Sass executables.
685
+ class Base
686
+ # @param args [Array<String>] The command-line arguments
687
+ def initialize(args)
688
+ @args = args
689
+ @options = {}
690
+ end
691
+
692
+ # Parses the command-line arguments and runs the executable.
693
+ # Calls `Kernel#exit` at the end, so it never returns.
694
+ #
695
+ # @see #parse
696
+ def parse!
697
+ # rubocop:disable RescueException
698
+ begin
699
+ parse
700
+ rescue Exception => e
701
+ # Exit code 65 indicates invalid data per
702
+ # http://www.freebsd.org/cgi/man.cgi?query=sysexits. Setting it via
703
+ # at_exit is a bit of a hack, but it allows us to rethrow when --trace
704
+ # is active and get both the built-in exception formatting and the
705
+ # correct exit code.
706
+ at_exit {exit 65} if e.is_a?(Sass::SyntaxError)
707
+
708
+ raise e if @options[:trace] || e.is_a?(SystemExit)
709
+
710
+ if e.is_a?(Sass::SyntaxError)
711
+ $stderr.puts e.sass_backtrace_str("standard input")
712
+ else
713
+ $stderr.print "#{e.class}: " unless e.class == RuntimeError
714
+ $stderr.puts e.message.to_s
715
+ end
716
+ $stderr.puts " Use --trace for backtrace."
717
+
718
+ exit 1
719
+ end
720
+ exit 0
721
+ # rubocop:enable RescueException
722
+ end
723
+
724
+ # Parses the command-line arguments and runs the executable.
725
+ # This does not handle exceptions or exit the program.
726
+ #
727
+ # @see #parse!
728
+ def parse
729
+ @opts = OptionParser.new(&method(:set_opts))
730
+
731
+ @opts.parse!(@args)
732
+
733
+ process_result
734
+
735
+ @options
736
+ end
737
+
738
+ # @return [String] A description of the executable
739
+ def to_s
740
+ @opts.to_s
741
+ end
742
+
743
+ protected
744
+
745
+ # Finds the line of the source template
746
+ # on which an exception was raised.
747
+ #
748
+ # @param exception [Exception] The exception
749
+ # @return [String] The line number
750
+ def get_line(exception)
751
+ # SyntaxErrors have weird line reporting
752
+ # when there's trailing whitespace
753
+ if exception.is_a?(::SyntaxError)
754
+ return (exception.message.scan(/:(\d+)/).first || ["??"]).first
755
+ end
756
+ (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
757
+ end
758
+
759
+ # Tells optparse how to parse the arguments
760
+ # available for all executables.
761
+ #
762
+ # This is meant to be overridden by subclasses
763
+ # so they can add their own options.
764
+ #
765
+ # @param opts [OptionParser]
766
+ def set_opts(opts)
767
+ Sass::Util.abstract(this)
768
+ end
769
+
770
+ # Set an option for specifying `Encoding.default_external`.
771
+ #
772
+ # @param opts [OptionParser]
773
+ def encoding_option(opts)
774
+ encoding_desc = if Sass::Util.ruby1_8?
775
+ 'Does not work in Ruby 1.8.'
776
+ else
777
+ 'Specify the default encoding for input files.'
778
+ end
779
+ opts.on('-E', '--default-encoding ENCODING', encoding_desc) do |encoding|
780
+ if Sass::Util.ruby1_8?
781
+ $stderr.puts "Specifying the encoding is not supported in ruby 1.8."
782
+ exit 1
783
+ else
784
+ Encoding.default_external = encoding
785
+ end
786
+ end
787
+ end
788
+
789
+ # Processes the options set by the command-line arguments. In particular,
790
+ # sets `@options[:input]` and `@options[:output]` to appropriate IO streams.
791
+ #
792
+ # This is meant to be overridden by subclasses
793
+ # so they can run their respective programs.
794
+ def process_result
795
+ input, output = @options[:input], @options[:output]
796
+ args = @args.dup
797
+ input ||=
798
+ begin
799
+ filename = args.shift
800
+ @options[:filename] = filename
801
+ open_file(filename) || $stdin
802
+ end
803
+ @options[:output_filename] = args.shift
804
+ output ||= @options[:output_filename] || $stdout
805
+ @options[:input], @options[:output] = input, output
806
+ end
807
+
808
+ COLORS = {:red => 31, :green => 32, :yellow => 33}
809
+
810
+ # Prints a status message about performing the given action,
811
+ # colored using the given color (via terminal escapes) if possible.
812
+ #
813
+ # @param name [#to_s] A short name for the action being performed.
814
+ # Shouldn't be longer than 11 characters.
815
+ # @param color [Symbol] The name of the color to use for this action.
816
+ # Can be `:red`, `:green`, or `:yellow`.
817
+ def puts_action(name, color, arg)
818
+ return if @options[:for_engine][:quiet]
819
+ printf color(color, "%11s %s\n"), name, arg
820
+ STDOUT.flush
821
+ end
822
+
823
+ # Same as `Kernel.puts`, but doesn't print anything if the `--quiet` option is set.
824
+ #
825
+ # @param args [Array] Passed on to `Kernel.puts`
826
+ def puts(*args)
827
+ return if @options[:for_engine][:quiet]
828
+ Kernel.puts(*args)
829
+ end
830
+
831
+ # Wraps the given string in terminal escapes
832
+ # causing it to have the given color.
833
+ # If terminal esapes aren't supported on this platform,
834
+ # just returns the string instead.
835
+ #
836
+ # @param color [Symbol] The name of the color to use.
837
+ # Can be `:red`, `:green`, or `:yellow`.
838
+ # @param str [String] The string to wrap in the given color.
839
+ # @return [String] The wrapped string.
840
+ def color(color, str)
841
+ raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
842
+
843
+ # Almost any real Unix terminal will support color,
844
+ # so we just filter for Windows terms (which don't set TERM)
845
+ # and not-real terminals, which aren't ttys.
846
+ return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
847
+ "\e[#{COLORS[color]}m#{str}\e[0m"
848
+ end
849
+
850
+ def write_output(text, destination)
851
+ if destination.is_a?(String)
852
+ open_file(destination, 'w') {|file| file.write(text)}
853
+ else
854
+ destination.write(text)
855
+ end
856
+ end
857
+
858
+ private
859
+
860
+ def open_file(filename, flag = 'r')
861
+ return if filename.nil?
862
+ flag = 'wb' if @options[:unix_newlines] && flag == 'w'
863
+ file = File.open(filename, flag)
864
+ return file unless block_given?
865
+ yield file
866
+ file.close
867
+ end
868
+
869
+ def handle_load_error(err)
870
+ dep = err.message[/^no such file to load -- (.*)/, 1]
871
+ raise err if @options[:trace] || dep.nil? || dep.empty?
872
+ $stderr.puts <<MESSAGE
873
+ Required dependency #{dep} not found!
874
+ Run "gem install #{dep}" to get it.
875
+ Use --trace for backtrace.
876
+ MESSAGE
877
+ exit 1
878
+ end
879
+ end
880
+ end
881
+
882
+ # 重写sass_convert.rb 去掉require
883
+ module Sass::Exec
884
+ # The `sass-convert` executable.
885
+ class SassConvert < Base
886
+ # @param args [Array<String>] The command-line arguments
887
+ def initialize(args)
888
+ super
889
+ # require 'sass'
890
+ @options[:for_tree] = {}
891
+ @options[:for_engine] = {:cache => false, :read_cache => true}
892
+ end
893
+
894
+ # Tells optparse how to parse the arguments.
895
+ #
896
+ # @param opts [OptionParser]
897
+ def set_opts(opts)
898
+ opts.banner = <<END
899
+ Usage: sass-convert [options] [INPUT] [OUTPUT]
900
+
901
+ Description:
902
+ Converts between CSS, indented syntax, and SCSS files. For example,
903
+ this can convert from the indented syntax to SCSS, or from CSS to
904
+ SCSS (adding appropriate nesting).
905
+ END
906
+
907
+ common_options(opts)
908
+ style(opts)
909
+ input_and_output(opts)
910
+ miscellaneous(opts)
911
+ end
912
+
913
+ # Processes the options set by the command-line arguments,
914
+ # and runs the CSS compiler appropriately.
915
+ def process_result
916
+ # require 'sass'
917
+
918
+ if @options[:recursive]
919
+ process_directory
920
+ return
921
+ end
922
+
923
+ super
924
+ input = @options[:input]
925
+ if File.directory?(input)
926
+ raise "Error: '#{input.path}' is a directory (did you mean to use --recursive?)"
927
+ end
928
+ output = @options[:output]
929
+ output = input if @options[:in_place]
930
+ process_file(input, output)
931
+ end
932
+
933
+ private
934
+
935
+ def common_options(opts)
936
+ opts.separator ''
937
+ opts.separator 'Common Options:'
938
+
939
+ opts.on('-F', '--from FORMAT',
940
+ 'The format to convert from. Can be css, scss, sass.',
941
+ 'By default, this is inferred from the input filename.',
942
+ 'If there is none, defaults to css.') do |name|
943
+ @options[:from] = name.downcase.to_sym
944
+ raise "sass-convert no longer supports LessCSS." if @options[:from] == :less
945
+ unless [:css, :scss, :sass].include?(@options[:from])
946
+ raise "Unknown format for sass-convert --from: #{name}"
947
+ end
948
+ end
949
+
950
+ opts.on('-T', '--to FORMAT',
951
+ 'The format to convert to. Can be scss or sass.',
952
+ 'By default, this is inferred from the output filename.',
953
+ 'If there is none, defaults to sass.') do |name|
954
+ @options[:to] = name.downcase.to_sym
955
+ unless [:scss, :sass].include?(@options[:to])
956
+ raise "Unknown format for sass-convert --to: #{name}"
957
+ end
958
+ end
959
+
960
+ opts.on('-i', '--in-place',
961
+ 'Convert a file to its own syntax.',
962
+ 'This can be used to update some deprecated syntax.') do
963
+ @options[:in_place] = true
964
+ end
965
+
966
+ opts.on('-R', '--recursive',
967
+ 'Convert all the files in a directory. Requires --from and --to.') do
968
+ @options[:recursive] = true
969
+ end
970
+
971
+ opts.on("-?", "-h", "--help", "Show this help message.") do
972
+ puts opts
973
+ exit
974
+ end
975
+
976
+ opts.on("-v", "--version", "Print the Sass version.") do
977
+ puts("Sass #{Sass.version[:string]}")
978
+ exit
979
+ end
980
+ end
981
+
982
+ def style(opts)
983
+ opts.separator ''
984
+ opts.separator 'Style:'
985
+
986
+ opts.on('--dasherize', 'Convert underscores to dashes.') do
987
+ @options[:for_tree][:dasherize] = true
988
+ end
989
+
990
+ opts.on('--indent NUM',
991
+ 'How many spaces to use for each level of indentation. Defaults to 2.',
992
+ '"t" means use hard tabs.') do |indent|
993
+
994
+ if indent == 't'
995
+ @options[:for_tree][:indent] = "\t"
996
+ else
997
+ @options[:for_tree][:indent] = " " * indent.to_i
998
+ end
999
+ end
1000
+
1001
+ opts.on('--old', 'Output the old-style ":prop val" property syntax.',
1002
+ 'Only meaningful when generating Sass.') do
1003
+ @options[:for_tree][:old] = true
1004
+ end
1005
+ end
1006
+
1007
+ def input_and_output(opts)
1008
+ opts.separator ''
1009
+ opts.separator 'Input and Output:'
1010
+
1011
+ opts.on('-s', '--stdin', :NONE,
1012
+ 'Read input from standard input instead of an input file.',
1013
+ 'This is the default if no input file is specified. Requires --from.') do
1014
+ @options[:input] = $stdin
1015
+ end
1016
+
1017
+ encoding_option(opts)
1018
+
1019
+ opts.on('--unix-newlines', 'Use Unix-style newlines in written files.',
1020
+ ('Always true on Unix.' unless Sass::Util.windows?)) do
1021
+ @options[:unix_newlines] = true if Sass::Util.windows?
1022
+ end
1023
+ end
1024
+
1025
+ def miscellaneous(opts)
1026
+ opts.separator ''
1027
+ opts.separator 'Miscellaneous:'
1028
+
1029
+ opts.on('--cache-location PATH',
1030
+ 'The path to save parsed Sass files. Defaults to .sass-cache.') do |loc|
1031
+ @options[:for_engine][:cache_location] = loc
1032
+ end
1033
+
1034
+ opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
1035
+ @options[:for_engine][:read_cache] = false
1036
+ end
1037
+
1038
+ opts.on('--trace', :NONE, 'Show a full Ruby stack trace on error') do
1039
+ @options[:trace] = true
1040
+ end
1041
+ end
1042
+
1043
+ def process_directory
1044
+ unless @options[:input] = @args.shift
1045
+ raise "Error: directory required when using --recursive."
1046
+ end
1047
+
1048
+ output = @options[:output] = @args.shift
1049
+ raise "Error: --from required when using --recursive." unless @options[:from]
1050
+ raise "Error: --to required when using --recursive." unless @options[:to]
1051
+ unless File.directory?(@options[:input])
1052
+ raise "Error: '#{@options[:input]}' is not a directory"
1053
+ end
1054
+ if @options[:output] && File.exist?(@options[:output]) &&
1055
+ !File.directory?(@options[:output])
1056
+ raise "Error: '#{@options[:output]}' is not a directory"
1057
+ end
1058
+ @options[:output] ||= @options[:input]
1059
+
1060
+ if @options[:to] == @options[:from] && !@options[:in_place]
1061
+ fmt = @options[:from]
1062
+ raise "Error: converting from #{fmt} to #{fmt} without --in-place"
1063
+ end
1064
+
1065
+ ext = @options[:from]
1066
+ Sass::Util.glob("#{@options[:input]}/**/*.#{ext}") do |f|
1067
+ output =
1068
+ if @options[:in_place]
1069
+ f
1070
+ elsif @options[:output]
1071
+ output_name = f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
1072
+ output_name[0...@options[:input].size] = @options[:output]
1073
+ output_name
1074
+ else
1075
+ f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
1076
+ end
1077
+
1078
+ unless File.directory?(File.dirname(output))
1079
+ puts_action :directory, :green, File.dirname(output)
1080
+ FileUtils.mkdir_p(File.dirname(output))
1081
+ end
1082
+ puts_action :convert, :green, f
1083
+ if File.exist?(output)
1084
+ puts_action :overwrite, :yellow, output
1085
+ else
1086
+ puts_action :create, :green, output
1087
+ end
1088
+
1089
+ process_file(f, output)
1090
+ end
1091
+ end
1092
+
1093
+ def process_file(input, output)
1094
+ input_path, output_path = path_for(input), path_for(output)
1095
+ if input_path
1096
+ @options[:from] ||=
1097
+ case input_path
1098
+ when /\.scss$/; :scss
1099
+ when /\.sass$/; :sass
1100
+ when /\.less$/; raise "sass-convert no longer supports LessCSS."
1101
+ when /\.css$/; :css
1102
+ end
1103
+ elsif @options[:in_place]
1104
+ raise "Error: the --in-place option requires a filename."
1105
+ end
1106
+
1107
+ if output_path
1108
+ @options[:to] ||=
1109
+ case output_path
1110
+ when /\.scss$/; :scss
1111
+ when /\.sass$/; :sass
1112
+ end
1113
+ end
1114
+
1115
+ @options[:from] ||= :css
1116
+ @options[:to] ||= :sass
1117
+ @options[:for_engine][:syntax] = @options[:from]
1118
+
1119
+ out =
1120
+ Sass::Util.silence_sass_warnings do
1121
+ if @options[:from] == :css
1122
+ require 'sass/css'
1123
+ Sass::CSS.new(input.read, @options[:for_tree]).render(@options[:to])
1124
+ else
1125
+ if input_path
1126
+ Sass::Engine.for_file(input_path, @options[:for_engine])
1127
+ else
1128
+ Sass::Engine.new(input.read, @options[:for_engine])
1129
+ end.to_tree.send("to_#{@options[:to]}", @options[:for_tree])
1130
+ end
1131
+ end
1132
+
1133
+ output = input_path if @options[:in_place]
1134
+ write_output(out, output)
1135
+ rescue Sass::SyntaxError => e
1136
+ raise e if @options[:trace]
1137
+ file = " of #{e.sass_filename}" if e.sass_filename
1138
+ raise "Error on line #{e.sass_line}#{file}: #{e.message}\n Use --trace for backtrace"
1139
+ rescue LoadError => err
1140
+ handle_load_error(err)
1141
+ end
1142
+
1143
+ def path_for(file)
1144
+ return file.path if file.is_a?(File)
1145
+ return file if file.is_a?(String)
1146
+ end
1147
+ end
1148
+ end
1149
+
1150
+ # 重写sass_scss.rb 去掉require
1151
+ module Sass::Exec
1152
+ # The `sass` and `scss` executables.
1153
+ class SassScss < Base
1154
+ attr_reader :default_syntax
1155
+
1156
+ # @param args [Array<String>] The command-line arguments
1157
+ def initialize(args, default_syntax)
1158
+ super(args)
1159
+ @options[:sourcemap] = :auto
1160
+ @options[:for_engine] = {
1161
+ :load_paths => default_sass_path
1162
+ }
1163
+ @default_syntax = default_syntax
1164
+ end
1165
+
1166
+ protected
1167
+
1168
+ # Tells optparse how to parse the arguments.
1169
+ #
1170
+ # @param opts [OptionParser]
1171
+ def set_opts(opts)
1172
+ opts.banner = <<END
1173
+ Usage: #{default_syntax} [options] [INPUT] [OUTPUT]
1174
+
1175
+ Description:
1176
+ Converts SCSS or Sass files to CSS.
1177
+ END
1178
+
1179
+ common_options(opts)
1180
+ watching_and_updating(opts)
1181
+ input_and_output(opts)
1182
+ miscellaneous(opts)
1183
+ end
1184
+
1185
+ # Processes the options set by the command-line arguments,
1186
+ # and runs the Sass compiler appropriately.
1187
+ def process_result
1188
+ # require 'sass'
1189
+
1190
+ if !@options[:update] && !@options[:watch] &&
1191
+ @args.first && colon_path?(@args.first)
1192
+ if @args.size == 1
1193
+ @args = split_colon_path(@args.first)
1194
+ else
1195
+ @options[:update] = true
1196
+ end
1197
+ end
1198
+ load_compass if @options[:compass]
1199
+ return interactive if @options[:interactive]
1200
+ return watch_or_update if @options[:watch] || @options[:update]
1201
+ super
1202
+
1203
+ if @options[:sourcemap] != :none && @options[:output_filename]
1204
+ @options[:sourcemap_filename] = Sass::Util.sourcemap_name(@options[:output_filename])
1205
+ end
1206
+
1207
+ @options[:for_engine][:filename] = @options[:filename]
1208
+ @options[:for_engine][:css_filename] = @options[:output] if @options[:output].is_a?(String)
1209
+ @options[:for_engine][:sourcemap_filename] = @options[:sourcemap_filename]
1210
+ @options[:for_engine][:sourcemap] = @options[:sourcemap]
1211
+
1212
+ run
1213
+ end
1214
+
1215
+ private
1216
+
1217
+ def common_options(opts)
1218
+ opts.separator ''
1219
+ opts.separator 'Common Options:'
1220
+
1221
+ opts.on('-I', '--load-path PATH', 'Specify a Sass import path.') do |path|
1222
+ (@options[:for_engine][:load_paths] ||= []) << path
1223
+ end
1224
+
1225
+ opts.on('-r', '--require LIB', 'Require a Ruby library before running Sass.') do |lib|
1226
+ require lib
1227
+ end
1228
+
1229
+ opts.on('--compass', 'Make Compass imports available and load project configuration.') do
1230
+ @options[:compass] = true
1231
+ end
1232
+
1233
+ opts.on('-t', '--style NAME', 'Output style. Can be nested (default), compact, ' \
1234
+ 'compressed, or expanded.') do |name|
1235
+ @options[:for_engine][:style] = name.to_sym
1236
+ end
1237
+
1238
+ opts.on("-?", "-h", "--help", "Show this help message.") do
1239
+ puts opts
1240
+ exit
1241
+ end
1242
+
1243
+ opts.on("-v", "--version", "Print the Sass version.") do
1244
+ puts("Sass #{Sass.version[:string]}")
1245
+ exit
1246
+ end
1247
+ end
1248
+
1249
+ def watching_and_updating(opts)
1250
+ opts.separator ''
1251
+ opts.separator 'Watching and Updating:'
1252
+
1253
+ opts.on('--watch', 'Watch files or directories for changes.',
1254
+ 'The location of the generated CSS can be set using a colon:',
1255
+ " #{@default_syntax} --watch input.#{@default_syntax}:output.css",
1256
+ " #{@default_syntax} --watch input-dir:output-dir") do
1257
+ @options[:watch] = true
1258
+ end
1259
+
1260
+ # Polling is used by default on Windows.
1261
+ unless Sass::Util.windows?
1262
+ opts.on('--poll', 'Check for file changes manually, rather than relying on the OS.',
1263
+ 'Only meaningful for --watch.') do
1264
+ @options[:poll] = true
1265
+ end
1266
+ end
1267
+
1268
+ opts.on('--update', 'Compile files or directories to CSS.',
1269
+ 'Locations are set like --watch.') do
1270
+ @options[:update] = true
1271
+ end
1272
+
1273
+ opts.on('-f', '--force', 'Recompile every Sass file, even if the CSS file is newer.',
1274
+ 'Only meaningful for --update.') do
1275
+ @options[:force] = true
1276
+ end
1277
+
1278
+ opts.on('--stop-on-error', 'If a file fails to compile, exit immediately.',
1279
+ 'Only meaningful for --watch and --update.') do
1280
+ @options[:stop_on_error] = true
1281
+ end
1282
+ end
1283
+
1284
+ def input_and_output(opts)
1285
+ opts.separator ''
1286
+ opts.separator 'Input and Output:'
1287
+
1288
+ if @default_syntax == :sass
1289
+ opts.on('--scss',
1290
+ 'Use the CSS-superset SCSS syntax.') do
1291
+ @options[:for_engine][:syntax] = :scss
1292
+ end
1293
+ else
1294
+ opts.on('--sass',
1295
+ 'Use the indented Sass syntax.') do
1296
+ @options[:for_engine][:syntax] = :sass
1297
+ end
1298
+ end
1299
+
1300
+ # This is optional for backwards-compatibility with Sass 3.3, which didn't
1301
+ # enable sourcemaps by default and instead used "--sourcemap" to do so.
1302
+ opts.on(:OPTIONAL, '--sourcemap=TYPE',
1303
+ 'How to link generated output to the source files.',
1304
+ ' auto (default): relative paths where possible, file URIs elsewhere',
1305
+ ' file: always absolute file URIs',
1306
+ ' inline: include the source text in the sourcemap',
1307
+ ' none: no sourcemaps') do |type|
1308
+ if type && !%w[auto file inline none].include?(type)
1309
+ $stderr.puts "Unknown sourcemap type #{type}.\n\n"
1310
+ $stderr.puts opts
1311
+ exit
1312
+ elsif type.nil?
1313
+ Sass::Util.sass_warn <<MESSAGE.rstrip
1314
+ DEPRECATION WARNING: Passing --sourcemap without a value is deprecated.
1315
+ Sourcemaps are now generated by default, so this flag has no effect.
1316
+ MESSAGE
1317
+ end
1318
+
1319
+ @options[:sourcemap] = (type || :auto).to_sym
1320
+ end
1321
+
1322
+ opts.on('-s', '--stdin', :NONE,
1323
+ 'Read input from standard input instead of an input file.',
1324
+ 'This is the default if no input file is specified.') do
1325
+ @options[:input] = $stdin
1326
+ end
1327
+
1328
+ encoding_option(opts)
1329
+
1330
+ opts.on('--unix-newlines', 'Use Unix-style newlines in written files.',
1331
+ ('Always true on Unix.' unless Sass::Util.windows?)) do
1332
+ @options[:unix_newlines] = true if Sass::Util.windows?
1333
+ end
1334
+
1335
+ opts.on('-g', '--debug-info',
1336
+ 'Emit output that can be used by the FireSass Firebug plugin.') do
1337
+ @options[:for_engine][:debug_info] = true
1338
+ end
1339
+
1340
+ opts.on('-l', '--line-numbers', '--line-comments',
1341
+ 'Emit comments in the generated CSS indicating the corresponding source line.') do
1342
+ @options[:for_engine][:line_numbers] = true
1343
+ end
1344
+ end
1345
+
1346
+ def miscellaneous(opts)
1347
+ opts.separator ''
1348
+ opts.separator 'Miscellaneous:'
1349
+
1350
+ opts.on('-i', '--interactive',
1351
+ 'Run an interactive SassScript shell.') do
1352
+ @options[:interactive] = true
1353
+ end
1354
+
1355
+ opts.on('-c', '--check', "Just check syntax, don't evaluate.") do
1356
+ require 'stringio'
1357
+ @options[:check_syntax] = true
1358
+ @options[:output] = StringIO.new
1359
+ end
1360
+
1361
+ opts.on('--precision NUMBER_OF_DIGITS', Integer,
1362
+ "How many digits of precision to use when outputting decimal numbers.",
1363
+ "Defaults to #{Sass::Script::Value::Number.precision}.") do |precision|
1364
+ Sass::Script::Value::Number.precision = precision
1365
+ end
1366
+
1367
+ opts.on('--cache-location PATH',
1368
+ 'The path to save parsed Sass files. Defaults to .sass-cache.') do |loc|
1369
+ @options[:for_engine][:cache_location] = loc
1370
+ end
1371
+
1372
+ opts.on('-C', '--no-cache', "Don't cache parsed Sass files.") do
1373
+ @options[:for_engine][:cache] = false
1374
+ end
1375
+
1376
+ opts.on('--trace', :NONE, 'Show a full Ruby stack trace on error.') do
1377
+ @options[:trace] = true
1378
+ end
1379
+
1380
+ opts.on('-q', '--quiet', 'Silence warnings and status messages during compilation.') do
1381
+ @options[:for_engine][:quiet] = true
1382
+ end
1383
+ end
1384
+
1385
+ def load_compass
1386
+ begin
1387
+ require 'compass'
1388
+ rescue LoadError
1389
+ require 'rubygems'
1390
+ begin
1391
+ require 'compass'
1392
+ rescue LoadError
1393
+ puts "ERROR: Cannot load compass."
1394
+ exit 1
1395
+ end
1396
+ end
1397
+ Compass.add_project_configuration
1398
+ Compass.configuration.project_path ||= Dir.pwd
1399
+ @options[:for_engine][:load_paths] ||= []
1400
+ @options[:for_engine][:load_paths] += Compass.configuration.sass_load_paths
1401
+ end
1402
+
1403
+ def interactive
1404
+ require 'sass/repl'
1405
+ Sass::Repl.new(@options).run
1406
+ end
1407
+
1408
+ # @comment
1409
+ # rubocop:disable MethodLength
1410
+ def watch_or_update
1411
+ require 'sass/plugin'
1412
+ Sass::Plugin.options.merge! @options[:for_engine]
1413
+ Sass::Plugin.options[:unix_newlines] = @options[:unix_newlines]
1414
+ Sass::Plugin.options[:poll] = @options[:poll]
1415
+ Sass::Plugin.options[:sourcemap] = @options[:sourcemap]
1416
+
1417
+ if @options[:force]
1418
+ raise "The --force flag may only be used with --update." unless @options[:update]
1419
+ Sass::Plugin.options[:always_update] = true
1420
+ end
1421
+
1422
+ raise <<MSG if @args.empty?
1423
+ What files should I watch? Did you mean something like:
1424
+ #{@default_syntax} --watch input.#{@default_syntax}:output.css
1425
+ #{@default_syntax} --watch input-dir:output-dir
1426
+ MSG
1427
+
1428
+ if !colon_path?(@args[0]) && probably_dest_dir?(@args[1])
1429
+ flag = @options[:update] ? "--update" : "--watch"
1430
+ err =
1431
+ if !File.exist?(@args[1])
1432
+ "doesn't exist"
1433
+ elsif @args[1] =~ /\.css$/
1434
+ "is a CSS file"
1435
+ end
1436
+ raise <<MSG if err
1437
+ File #{@args[1]} #{err}.
1438
+ Did you mean: #{@default_syntax} #{flag} #{@args[0]}:#{@args[1]}
1439
+ MSG
1440
+ end
1441
+
1442
+ # Watch the working directory for changes without adding it to the load
1443
+ # path. This preserves the pre-3.4 behavior when the working directory was
1444
+ # on the load path. We should remove this when we can look for directories
1445
+ # to watch by traversing the import graph.
1446
+ class << Sass::Plugin.compiler
1447
+ # We have to use a class var to make this visible to #watched_file? and
1448
+ # #watched_paths.
1449
+ # rubocop:disable ClassVars
1450
+ @@working_directory = Sass::Util.realpath('.').to_s
1451
+ # rubocop:ensable ClassVars
1452
+
1453
+ def watched_file?(file)
1454
+ super(file) ||
1455
+ (file =~ /\.s[ac]ss$/ && file.start_with?(@@working_directory + File::SEPARATOR))
1456
+ end
1457
+
1458
+ def watched_paths
1459
+ @watched_paths ||= super + [@@working_directory]
1460
+ end
1461
+ end
1462
+
1463
+ dirs, files = @args.map {|name| split_colon_path(name)}.
1464
+ partition {|i, _| File.directory? i}
1465
+ files.map! do |from, to|
1466
+ to ||= from.gsub(/\.[^.]*?$/, '.css')
1467
+ sourcemap = Sass::Util.sourcemap_name(to) if @options[:sourcemap]
1468
+ [from, to, sourcemap]
1469
+ end
1470
+ dirs.map! {|from, to| [from, to || from]}
1471
+ Sass::Plugin.options[:template_location] = dirs
1472
+
1473
+ Sass::Plugin.on_updated_stylesheet do |_, css, sourcemap|
1474
+ [css, sourcemap].each do |file|
1475
+ next unless file
1476
+ puts_action :write, :green, file
1477
+ end
1478
+ end
1479
+
1480
+ had_error = false
1481
+ Sass::Plugin.on_creating_directory {|dirname| puts_action :directory, :green, dirname}
1482
+ Sass::Plugin.on_deleting_css {|filename| puts_action :delete, :yellow, filename}
1483
+ Sass::Plugin.on_deleting_sourcemap {|filename| puts_action :delete, :yellow, filename}
1484
+ Sass::Plugin.on_compilation_error do |error, _, _|
1485
+ if error.is_a?(SystemCallError) && !@options[:stop_on_error]
1486
+ had_error = true
1487
+ puts_action :error, :red, error.message
1488
+ STDOUT.flush
1489
+ next
1490
+ end
1491
+
1492
+ raise error unless error.is_a?(Sass::SyntaxError) && !@options[:stop_on_error]
1493
+ had_error = true
1494
+ puts_action :error, :red,
1495
+ "#{error.sass_filename} (Line #{error.sass_line}: #{error.message})"
1496
+ STDOUT.flush
1497
+ end
1498
+
1499
+ if @options[:update]
1500
+ Sass::Plugin.update_stylesheets(files)
1501
+ exit 1 if had_error
1502
+ return
1503
+ end
1504
+
1505
+ puts ">>> Sass is watching for changes. Press Ctrl-C to stop."
1506
+
1507
+ Sass::Plugin.on_template_modified do |template|
1508
+ puts ">>> Change detected to: #{template}"
1509
+ STDOUT.flush
1510
+ end
1511
+ Sass::Plugin.on_template_created do |template|
1512
+ puts ">>> New template detected: #{template}"
1513
+ STDOUT.flush
1514
+ end
1515
+ Sass::Plugin.on_template_deleted do |template|
1516
+ puts ">>> Deleted template detected: #{template}"
1517
+ STDOUT.flush
1518
+ end
1519
+
1520
+ Sass::Plugin.watch(files)
1521
+ end
1522
+ # @comment
1523
+ # rubocop:enable MethodLength
1524
+
1525
+ def run
1526
+ input = @options[:input]
1527
+ output = @options[:output]
1528
+
1529
+ @options[:for_engine][:syntax] ||= :scss if input.is_a?(File) && input.path =~ /\.scss$/
1530
+ @options[:for_engine][:syntax] ||= @default_syntax
1531
+ engine =
1532
+ if input.is_a?(File) && !@options[:check_syntax]
1533
+ Sass::Engine.for_file(input.path, @options[:for_engine])
1534
+ else
1535
+ # We don't need to do any special handling of @options[:check_syntax] here,
1536
+ # because the Sass syntax checking happens alongside evaluation
1537
+ # and evaluation doesn't actually evaluate any code anyway.
1538
+ Sass::Engine.new(input.read, @options[:for_engine])
1539
+ end
1540
+
1541
+ input.close if input.is_a?(File)
1542
+
1543
+ if @options[:sourcemap] != :none && @options[:sourcemap_filename]
1544
+ relative_sourcemap_path = Sass::Util.relative_path_from(
1545
+ @options[:sourcemap_filename], Sass::Util.pathname(@options[:output_filename]).dirname)
1546
+ rendered, mapping = engine.render_with_sourcemap(relative_sourcemap_path.to_s)
1547
+ write_output(rendered, output)
1548
+ write_output(mapping.to_json(
1549
+ :type => @options[:sourcemap],
1550
+ :css_path => @options[:output_filename],
1551
+ :sourcemap_path => @options[:sourcemap_filename]) + "\n",
1552
+ @options[:sourcemap_filename])
1553
+ else
1554
+ write_output(engine.render, output)
1555
+ end
1556
+ rescue Sass::SyntaxError => e
1557
+ write_output(Sass::SyntaxError.exception_to_css(e), output) if output.is_a?(String)
1558
+ raise e
1559
+ ensure
1560
+ output.close if output.is_a? File
1561
+ end
1562
+
1563
+ def colon_path?(path)
1564
+ !split_colon_path(path)[1].nil?
1565
+ end
1566
+
1567
+ def split_colon_path(path)
1568
+ one, two = path.split(':', 2)
1569
+ if one && two && Sass::Util.windows? &&
1570
+ one =~ /\A[A-Za-z]\Z/ && two =~ /\A[\/\\]/
1571
+ # If we're on Windows and we were passed a drive letter path,
1572
+ # don't split on that colon.
1573
+ one2, two = two.split(':', 2)
1574
+ one = one + ':' + one2
1575
+ end
1576
+ return one, two
1577
+ end
1578
+
1579
+ # Whether path is likely to be meant as the destination
1580
+ # in a source:dest pair.
1581
+ def probably_dest_dir?(path)
1582
+ return false unless path
1583
+ return false if colon_path?(path)
1584
+ Sass::Util.glob(File.join(path, "*.s[ca]ss")).empty?
1585
+ end
1586
+
1587
+ def default_sass_path
1588
+ return unless ENV['SASS_PATH']
1589
+ # The select here prevents errors when the environment's
1590
+ # load paths specified do not exist.
1591
+ ENV['SASS_PATH'].split(File::PATH_SEPARATOR).select {|d| File.directory?(d)}
1592
+ end
1593
+ end
1594
+ end