sass 3.3.0 → 3.4.25

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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +3 -1
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/CONTRIBUTING.md +148 -0
  5. data/MIT-LICENSE +1 -1
  6. data/README.md +76 -62
  7. data/Rakefile +104 -24
  8. data/VERSION +1 -1
  9. data/VERSION_DATE +1 -1
  10. data/VERSION_NAME +1 -1
  11. data/bin/sass +1 -1
  12. data/bin/scss +1 -1
  13. data/extra/sass-spec-ref.sh +32 -0
  14. data/extra/update_watch.rb +1 -1
  15. data/lib/sass/cache_stores/filesystem.rb +9 -5
  16. data/lib/sass/cache_stores/memory.rb +4 -5
  17. data/lib/sass/callbacks.rb +2 -2
  18. data/lib/sass/css.rb +12 -13
  19. data/lib/sass/deprecation.rb +55 -0
  20. data/lib/sass/engine.rb +106 -70
  21. data/lib/sass/environment.rb +39 -19
  22. data/lib/sass/error.rb +17 -20
  23. data/lib/sass/exec/base.rb +199 -0
  24. data/lib/sass/exec/sass_convert.rb +283 -0
  25. data/lib/sass/exec/sass_scss.rb +440 -0
  26. data/lib/sass/exec.rb +5 -771
  27. data/lib/sass/features.rb +9 -2
  28. data/lib/sass/importers/base.rb +8 -3
  29. data/lib/sass/importers/filesystem.rb +30 -38
  30. data/lib/sass/logger/base.rb +8 -2
  31. data/lib/sass/logger/delayed.rb +50 -0
  32. data/lib/sass/logger.rb +8 -3
  33. data/lib/sass/media.rb +1 -4
  34. data/lib/sass/plugin/compiler.rb +224 -90
  35. data/lib/sass/plugin/configuration.rb +38 -22
  36. data/lib/sass/plugin/merb.rb +2 -2
  37. data/lib/sass/plugin/rack.rb +3 -3
  38. data/lib/sass/plugin/rails.rb +1 -1
  39. data/lib/sass/plugin/staleness_checker.rb +4 -4
  40. data/lib/sass/plugin.rb +6 -5
  41. data/lib/sass/script/css_lexer.rb +1 -1
  42. data/lib/sass/script/css_parser.rb +2 -3
  43. data/lib/sass/script/css_variable_warning.rb +52 -0
  44. data/lib/sass/script/functions.rb +739 -318
  45. data/lib/sass/script/lexer.rb +134 -54
  46. data/lib/sass/script/parser.rb +252 -56
  47. data/lib/sass/script/tree/funcall.rb +13 -6
  48. data/lib/sass/script/tree/interpolation.rb +127 -4
  49. data/lib/sass/script/tree/list_literal.rb +31 -4
  50. data/lib/sass/script/tree/literal.rb +4 -0
  51. data/lib/sass/script/tree/node.rb +21 -3
  52. data/lib/sass/script/tree/operation.rb +54 -1
  53. data/lib/sass/script/tree/selector.rb +26 -0
  54. data/lib/sass/script/tree/string_interpolation.rb +59 -38
  55. data/lib/sass/script/tree/variable.rb +1 -1
  56. data/lib/sass/script/tree.rb +1 -0
  57. data/lib/sass/script/value/base.rb +17 -14
  58. data/lib/sass/script/value/bool.rb +0 -5
  59. data/lib/sass/script/value/color.rb +78 -42
  60. data/lib/sass/script/value/helpers.rb +119 -2
  61. data/lib/sass/script/value/list.rb +0 -15
  62. data/lib/sass/script/value/map.rb +1 -1
  63. data/lib/sass/script/value/null.rb +0 -5
  64. data/lib/sass/script/value/number.rb +112 -31
  65. data/lib/sass/script/value/string.rb +102 -13
  66. data/lib/sass/script/value.rb +0 -1
  67. data/lib/sass/script.rb +3 -3
  68. data/lib/sass/scss/css_parser.rb +24 -4
  69. data/lib/sass/scss/parser.rb +290 -383
  70. data/lib/sass/scss/rx.rb +17 -9
  71. data/lib/sass/scss/static_parser.rb +306 -4
  72. data/lib/sass/scss.rb +0 -2
  73. data/lib/sass/selector/abstract_sequence.rb +35 -18
  74. data/lib/sass/selector/comma_sequence.rb +114 -19
  75. data/lib/sass/selector/pseudo.rb +266 -0
  76. data/lib/sass/selector/sequence.rb +146 -40
  77. data/lib/sass/selector/simple.rb +22 -33
  78. data/lib/sass/selector/simple_sequence.rb +122 -39
  79. data/lib/sass/selector.rb +57 -197
  80. data/lib/sass/shared.rb +2 -2
  81. data/lib/sass/source/map.rb +31 -14
  82. data/lib/sass/source/position.rb +4 -4
  83. data/lib/sass/stack.rb +2 -8
  84. data/lib/sass/supports.rb +10 -13
  85. data/lib/sass/tree/at_root_node.rb +1 -0
  86. data/lib/sass/tree/charset_node.rb +1 -1
  87. data/lib/sass/tree/comment_node.rb +1 -1
  88. data/lib/sass/tree/css_import_node.rb +9 -1
  89. data/lib/sass/tree/directive_node.rb +8 -2
  90. data/lib/sass/tree/error_node.rb +18 -0
  91. data/lib/sass/tree/extend_node.rb +1 -1
  92. data/lib/sass/tree/function_node.rb +9 -0
  93. data/lib/sass/tree/import_node.rb +6 -5
  94. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  95. data/lib/sass/tree/node.rb +5 -3
  96. data/lib/sass/tree/prop_node.rb +6 -7
  97. data/lib/sass/tree/rule_node.rb +26 -11
  98. data/lib/sass/tree/visitors/check_nesting.rb +56 -32
  99. data/lib/sass/tree/visitors/convert.rb +59 -44
  100. data/lib/sass/tree/visitors/cssize.rb +34 -30
  101. data/lib/sass/tree/visitors/deep_copy.rb +6 -1
  102. data/lib/sass/tree/visitors/extend.rb +15 -13
  103. data/lib/sass/tree/visitors/perform.rb +87 -50
  104. data/lib/sass/tree/visitors/set_options.rb +15 -1
  105. data/lib/sass/tree/visitors/to_css.rb +72 -43
  106. data/lib/sass/util/multibyte_string_scanner.rb +0 -2
  107. data/lib/sass/util/normalized_map.rb +0 -1
  108. data/lib/sass/util/subset_map.rb +2 -3
  109. data/lib/sass/util.rb +334 -154
  110. data/lib/sass/version.rb +7 -7
  111. data/lib/sass.rb +10 -8
  112. data/test/sass/cache_test.rb +62 -20
  113. data/test/sass/callbacks_test.rb +1 -1
  114. data/test/sass/compiler_test.rb +24 -11
  115. data/test/sass/conversion_test.rb +241 -50
  116. data/test/sass/css2sass_test.rb +73 -5
  117. data/test/sass/css_variable_test.rb +132 -0
  118. data/test/sass/encoding_test.rb +219 -0
  119. data/test/sass/engine_test.rb +343 -260
  120. data/test/sass/exec_test.rb +12 -2
  121. data/test/sass/extend_test.rb +333 -44
  122. data/test/sass/functions_test.rb +353 -260
  123. data/test/sass/importer_test.rb +40 -21
  124. data/test/sass/logger_test.rb +1 -1
  125. data/test/sass/more_results/more_import.css +1 -1
  126. data/test/sass/more_templates/more1.sass +10 -10
  127. data/test/sass/more_templates/more_import.sass +2 -2
  128. data/test/sass/plugin_test.rb +24 -21
  129. data/test/sass/results/compact.css +1 -1
  130. data/test/sass/results/complex.css +4 -4
  131. data/test/sass/results/expanded.css +1 -1
  132. data/test/sass/results/import.css +1 -1
  133. data/test/sass/results/import_charset_ibm866.css +2 -2
  134. data/test/sass/results/mixins.css +17 -17
  135. data/test/sass/results/nested.css +1 -1
  136. data/test/sass/results/parent_ref.css +2 -2
  137. data/test/sass/results/script.css +5 -5
  138. data/test/sass/results/scss_import.css +1 -1
  139. data/test/sass/script_conversion_test.rb +71 -39
  140. data/test/sass/script_test.rb +714 -123
  141. data/test/sass/scss/css_test.rb +213 -30
  142. data/test/sass/scss/rx_test.rb +8 -4
  143. data/test/sass/scss/scss_test.rb +766 -22
  144. data/test/sass/source_map_test.rb +263 -95
  145. data/test/sass/superselector_test.rb +210 -0
  146. data/test/sass/templates/_partial.sass +1 -1
  147. data/test/sass/templates/basic.sass +10 -10
  148. data/test/sass/templates/bork1.sass +1 -1
  149. data/test/sass/templates/bork5.sass +1 -1
  150. data/test/sass/templates/compact.sass +10 -10
  151. data/test/sass/templates/complex.sass +187 -187
  152. data/test/sass/templates/compressed.sass +10 -10
  153. data/test/sass/templates/expanded.sass +10 -10
  154. data/test/sass/templates/import.sass +2 -2
  155. data/test/sass/templates/importee.sass +3 -3
  156. data/test/sass/templates/mixins.sass +22 -22
  157. data/test/sass/templates/multiline.sass +4 -4
  158. data/test/sass/templates/nested.sass +13 -13
  159. data/test/sass/templates/parent_ref.sass +12 -12
  160. data/test/sass/templates/script.sass +70 -70
  161. data/test/sass/templates/scss_import.scss +2 -1
  162. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +1 -1
  163. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +2 -2
  164. data/test/sass/templates/subdir/subdir.sass +3 -3
  165. data/test/sass/templates/units.sass +10 -10
  166. data/test/sass/test_helper.rb +1 -1
  167. data/test/sass/util/multibyte_string_scanner_test.rb +11 -3
  168. data/test/sass/util/normalized_map_test.rb +1 -1
  169. data/test/sass/util/subset_map_test.rb +2 -2
  170. data/test/sass/util_test.rb +46 -45
  171. data/test/sass/value_helpers_test.rb +5 -7
  172. data/test/sass-spec.yml +3 -0
  173. data/test/test_helper.rb +7 -6
  174. data/vendor/listen/CHANGELOG.md +1 -228
  175. data/vendor/listen/Gemfile +5 -15
  176. data/vendor/listen/README.md +111 -77
  177. data/vendor/listen/Rakefile +0 -42
  178. data/vendor/listen/lib/listen/adapter.rb +195 -82
  179. data/vendor/listen/lib/listen/adapters/bsd.rb +27 -64
  180. data/vendor/listen/lib/listen/adapters/darwin.rb +21 -58
  181. data/vendor/listen/lib/listen/adapters/linux.rb +23 -55
  182. data/vendor/listen/lib/listen/adapters/polling.rb +25 -34
  183. data/vendor/listen/lib/listen/adapters/windows.rb +50 -46
  184. data/vendor/listen/lib/listen/directory_record.rb +96 -61
  185. data/vendor/listen/lib/listen/listener.rb +135 -37
  186. data/vendor/listen/lib/listen/turnstile.rb +9 -5
  187. data/vendor/listen/lib/listen/version.rb +1 -1
  188. data/vendor/listen/lib/listen.rb +33 -19
  189. data/vendor/listen/listen.gemspec +6 -0
  190. data/vendor/listen/spec/listen/adapter_spec.rb +43 -77
  191. data/vendor/listen/spec/listen/adapters/polling_spec.rb +8 -8
  192. data/vendor/listen/spec/listen/directory_record_spec.rb +81 -56
  193. data/vendor/listen/spec/listen/listener_spec.rb +128 -39
  194. data/vendor/listen/spec/listen_spec.rb +15 -21
  195. data/vendor/listen/spec/spec_helper.rb +4 -0
  196. data/vendor/listen/spec/support/adapter_helper.rb +52 -15
  197. data/vendor/listen/spec/support/directory_record_helper.rb +7 -5
  198. data/vendor/listen/spec/support/listeners_helper.rb +30 -7
  199. metadata +310 -300
  200. data/CONTRIBUTING +0 -3
  201. data/ext/mkrf_conf.rb +0 -27
  202. data/lib/sass/script/value/deprecated_false.rb +0 -55
  203. data/lib/sass/scss/script_lexer.rb +0 -15
  204. data/lib/sass/scss/script_parser.rb +0 -25
  205. data/vendor/listen/lib/listen/dependency_manager.rb +0 -126
  206. data/vendor/listen/lib/listen/multi_listener.rb +0 -143
  207. data/vendor/listen/spec/listen/dependency_manager_spec.rb +0 -107
  208. data/vendor/listen/spec/listen/multi_listener_spec.rb +0 -174
data/lib/sass/error.rb CHANGED
@@ -69,14 +69,14 @@ module Sass
69
69
  # The name of the mixin in which the error occurred.
70
70
  # This could be `nil` if the error occurred outside a mixin.
71
71
  #
72
- # @return [Fixnum]
72
+ # @return [String]
73
73
  def sass_mixin
74
74
  sass_backtrace.first[:mixin]
75
75
  end
76
76
 
77
77
  # The line of the Sass template on which the error occurred.
78
78
  #
79
- # @return [Fixnum]
79
+ # @return [Integer]
80
80
  def sass_line
81
81
  sass_backtrace.first[:line]
82
82
  end
@@ -86,7 +86,7 @@ module Sass
86
86
  # @param attrs [{Symbol => Object}] The information in the backtrace entry.
87
87
  # See \{#sass\_backtrace}
88
88
  def add_backtrace(attrs)
89
- sass_backtrace << attrs.reject {|k, v| v.nil?}
89
+ sass_backtrace << attrs.reject {|_k, v| v.nil?}
90
90
  end
91
91
 
92
92
  # Modify the top Sass backtrace entries
@@ -104,12 +104,12 @@ module Sass
104
104
  # @param attrs [{Symbol => Object}] The information to add to the backtrace entry.
105
105
  # See \{#sass\_backtrace}
106
106
  def modify_backtrace(attrs)
107
- attrs = attrs.reject {|k, v| v.nil?}
107
+ attrs = attrs.reject {|_k, v| v.nil?}
108
108
  # Move backwards through the backtrace
109
- (0...sass_backtrace.size).to_a.reverse.each do |i|
109
+ (0...sass_backtrace.size).to_a.reverse_each do |i|
110
110
  entry = sass_backtrace[i]
111
111
  sass_backtrace[i] = attrs.merge(entry)
112
- attrs.reject! {|k, v| entry.include?(k)}
112
+ attrs.reject! {|k, _v| entry.include?(k)}
113
113
  break if attrs.empty?
114
114
  end
115
115
  end
@@ -127,7 +127,7 @@ module Sass
127
127
  return nil if super.nil?
128
128
  return super if sass_backtrace.all? {|h| h.empty?}
129
129
  sass_backtrace.map do |h|
130
- "#{h[:filename] || "(sass)"}:#{h[:line]}" +
130
+ "#{h[:filename] || '(sass)'}:#{h[:line]}" +
131
131
  (h[:mixin] ? ":in `#{h[:mixin]}'" : "")
132
132
  end + super
133
133
  end
@@ -140,10 +140,10 @@ module Sass
140
140
  def sass_backtrace_str(default_filename = "an unknown file")
141
141
  lines = message.split("\n")
142
142
  msg = lines[0] + lines[1..-1].
143
- map {|l| "\n" + (" " * "Syntax error: ".size) + l}.join
144
- "Syntax error: #{msg}" +
143
+ map {|l| "\n" + (" " * "Error: ".size) + l}.join
144
+ "Error: #{msg}" +
145
145
  Sass::Util.enum_with_index(sass_backtrace).map do |entry, i|
146
- "\n #{i == 0 ? "on" : "from"} line #{entry[:line]}" +
146
+ "\n #{i == 0 ? 'on' : 'from'} line #{entry[:line]}" +
147
147
  " of #{entry[:filename] || default_filename}" +
148
148
  (entry[:mixin] ? ", in `#{entry[:mixin]}'" : "")
149
149
  end.join
@@ -153,21 +153,19 @@ module Sass
153
153
  # Returns an error report for an exception in CSS format.
154
154
  #
155
155
  # @param e [Exception]
156
- # @param options [{Symbol => Object}] The options passed to {Sass::Engine#initialize}
156
+ # @param line_offset [Integer] The number of the first line of the Sass template.
157
157
  # @return [String] The error report
158
158
  # @raise [Exception] `e`, if the
159
159
  # {file:SASS_REFERENCE.md#full_exception-option `:full_exception`} option
160
160
  # is set to false.
161
- def exception_to_css(e, options)
162
- raise e unless options[:full_exception]
163
-
164
- header = header_string(e, options)
161
+ def exception_to_css(e, line_offset = 1)
162
+ header = header_string(e, line_offset)
165
163
 
166
164
  <<END
167
165
  /*
168
- #{header.gsub("*/", "*\\/")}
166
+ #{header.gsub('*/', '*\\/')}
169
167
 
170
- Backtrace:\n#{e.backtrace.join("\n").gsub("*/", "*\\/")}
168
+ Backtrace:\n#{e.backtrace.join("\n").gsub('*/', '*\\/')}
171
169
  */
172
170
  body:before {
173
171
  white-space: pre;
@@ -178,15 +176,14 @@ END
178
176
 
179
177
  private
180
178
 
181
- def header_string(e, options)
179
+ def header_string(e, line_offset)
182
180
  unless e.is_a?(Sass::SyntaxError) && e.sass_line && e.sass_template
183
181
  return "#{e.class}: #{e.message}"
184
182
  end
185
183
 
186
- line_offset = options[:line] || 1
187
184
  line_num = e.sass_line + 1 - line_offset
188
185
  min = [line_num - 6, 0].max
189
- section = e.sass_template.rstrip.split("\n")[min ... line_num + 5]
186
+ section = e.sass_template.rstrip.split("\n")[min...line_num + 5]
190
187
  return e.sass_backtrace_str if section.nil? || section.empty?
191
188
 
192
189
  e.sass_backtrace_str + "\n\n" + Sass::Util.enum_with_index(section).
@@ -0,0 +1,199 @@
1
+ require 'optparse'
2
+
3
+ module Sass::Exec
4
+ # The abstract base class for Sass executables.
5
+ class Base
6
+ # @param args [Array<String>] The command-line arguments
7
+ def initialize(args)
8
+ @args = args
9
+ @options = {}
10
+ end
11
+
12
+ # Parses the command-line arguments and runs the executable.
13
+ # Calls `Kernel#exit` at the end, so it never returns.
14
+ #
15
+ # @see #parse
16
+ def parse!
17
+ # rubocop:disable RescueException
18
+ begin
19
+ parse
20
+ rescue Exception => e
21
+ # Exit code 65 indicates invalid data per
22
+ # http://www.freebsd.org/cgi/man.cgi?query=sysexits. Setting it via
23
+ # at_exit is a bit of a hack, but it allows us to rethrow when --trace
24
+ # is active and get both the built-in exception formatting and the
25
+ # correct exit code.
26
+ at_exit {exit Sass::Util.windows? ? 13 : 65} if e.is_a?(Sass::SyntaxError)
27
+
28
+ raise e if @options[:trace] || e.is_a?(SystemExit)
29
+
30
+ if e.is_a?(Sass::SyntaxError)
31
+ $stderr.puts e.sass_backtrace_str("standard input")
32
+ else
33
+ $stderr.print "#{e.class}: " unless e.class == RuntimeError
34
+ $stderr.puts e.message.to_s
35
+ end
36
+ $stderr.puts " Use --trace for backtrace."
37
+
38
+ exit 1
39
+ end
40
+ exit 0
41
+ # rubocop:enable RescueException
42
+ end
43
+
44
+ # Parses the command-line arguments and runs the executable.
45
+ # This does not handle exceptions or exit the program.
46
+ #
47
+ # @see #parse!
48
+ def parse
49
+ @opts = OptionParser.new(&method(:set_opts))
50
+ @opts.parse!(@args)
51
+
52
+ process_result
53
+
54
+ @options
55
+ end
56
+
57
+ # @return [String] A description of the executable
58
+ def to_s
59
+ @opts.to_s
60
+ end
61
+
62
+ protected
63
+
64
+ # Finds the line of the source template
65
+ # on which an exception was raised.
66
+ #
67
+ # @param exception [Exception] The exception
68
+ # @return [String] The line number
69
+ def get_line(exception)
70
+ # SyntaxErrors have weird line reporting
71
+ # when there's trailing whitespace
72
+ if exception.is_a?(::SyntaxError)
73
+ return (exception.message.scan(/:(\d+)/).first || ["??"]).first
74
+ end
75
+ (exception.backtrace[0].scan(/:(\d+)/).first || ["??"]).first
76
+ end
77
+
78
+ # Tells optparse how to parse the arguments
79
+ # available for all executables.
80
+ #
81
+ # This is meant to be overridden by subclasses
82
+ # so they can add their own options.
83
+ #
84
+ # @param opts [OptionParser]
85
+ def set_opts(opts)
86
+ Sass::Util.abstract(this)
87
+ end
88
+
89
+ # Set an option for specifying `Encoding.default_external`.
90
+ #
91
+ # @param opts [OptionParser]
92
+ def encoding_option(opts)
93
+ encoding_desc = if Sass::Util.ruby1_8?
94
+ 'Does not work in Ruby 1.8.'
95
+ else
96
+ 'Specify the default encoding for input files.'
97
+ end
98
+ opts.on('-E', '--default-encoding ENCODING', encoding_desc) do |encoding|
99
+ if Sass::Util.ruby1_8?
100
+ $stderr.puts "Specifying the encoding is not supported in ruby 1.8."
101
+ exit 1
102
+ else
103
+ Encoding.default_external = encoding
104
+ end
105
+ end
106
+ end
107
+
108
+ # Processes the options set by the command-line arguments. In particular,
109
+ # sets `@options[:input]` and `@options[:output]` to appropriate IO streams.
110
+ #
111
+ # This is meant to be overridden by subclasses
112
+ # so they can run their respective programs.
113
+ def process_result
114
+ input, output = @options[:input], @options[:output]
115
+ args = @args.dup
116
+ input ||=
117
+ begin
118
+ filename = args.shift
119
+ @options[:filename] = filename
120
+ open_file(filename) || $stdin
121
+ end
122
+ @options[:output_filename] = args.shift
123
+ output ||= @options[:output_filename] || $stdout
124
+ @options[:input], @options[:output] = input, output
125
+ end
126
+
127
+ COLORS = {:red => 31, :green => 32, :yellow => 33}
128
+
129
+ # Prints a status message about performing the given action,
130
+ # colored using the given color (via terminal escapes) if possible.
131
+ #
132
+ # @param name [#to_s] A short name for the action being performed.
133
+ # Shouldn't be longer than 11 characters.
134
+ # @param color [Symbol] The name of the color to use for this action.
135
+ # Can be `:red`, `:green`, or `:yellow`.
136
+ def puts_action(name, color, arg)
137
+ return if @options[:for_engine][:quiet]
138
+ printf color(color, "%11s %s\n"), name, arg
139
+ STDOUT.flush
140
+ end
141
+
142
+ # Same as `Kernel.puts`, but doesn't print anything if the `--quiet` option is set.
143
+ #
144
+ # @param args [Array] Passed on to `Kernel.puts`
145
+ def puts(*args)
146
+ return if @options[:for_engine][:quiet]
147
+ Kernel.puts(*args)
148
+ end
149
+
150
+ # Wraps the given string in terminal escapes
151
+ # causing it to have the given color.
152
+ # If terminal escapes aren't supported on this platform,
153
+ # just returns the string instead.
154
+ #
155
+ # @param color [Symbol] The name of the color to use.
156
+ # Can be `:red`, `:green`, or `:yellow`.
157
+ # @param str [String] The string to wrap in the given color.
158
+ # @return [String] The wrapped string.
159
+ def color(color, str)
160
+ raise "[BUG] Unrecognized color #{color}" unless COLORS[color]
161
+
162
+ # Almost any real Unix terminal will support color,
163
+ # so we just filter for Windows terms (which don't set TERM)
164
+ # and not-real terminals, which aren't ttys.
165
+ return str if ENV["TERM"].nil? || ENV["TERM"].empty? || !STDOUT.tty?
166
+ "\e[#{COLORS[color]}m#{str}\e[0m"
167
+ end
168
+
169
+ def write_output(text, destination)
170
+ if destination.is_a?(String)
171
+ open_file(destination, 'w') {|file| file.write(text)}
172
+ else
173
+ destination.write(text)
174
+ end
175
+ end
176
+
177
+ private
178
+
179
+ def open_file(filename, flag = 'r')
180
+ return if filename.nil?
181
+ flag = 'wb' if @options[:unix_newlines] && flag == 'w'
182
+ file = File.open(filename, flag)
183
+ return file unless block_given?
184
+ yield file
185
+ file.close
186
+ end
187
+
188
+ def handle_load_error(err)
189
+ dep = err.message[/^no such file to load -- (.*)/, 1]
190
+ raise err if @options[:trace] || dep.nil? || dep.empty?
191
+ $stderr.puts <<MESSAGE
192
+ Required dependency #{dep} not found!
193
+ Run "gem install #{dep}" to get it.
194
+ Use --trace for backtrace.
195
+ MESSAGE
196
+ exit 1
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,283 @@
1
+ require 'optparse'
2
+ require 'fileutils'
3
+
4
+ module Sass::Exec
5
+ # The `sass-convert` executable.
6
+ class SassConvert < Base
7
+ # @param args [Array<String>] The command-line arguments
8
+ def initialize(args)
9
+ super
10
+ require 'sass'
11
+ @options[:for_tree] = {}
12
+ @options[:for_engine] = {:cache => false, :read_cache => true}
13
+ end
14
+
15
+ # Tells optparse how to parse the arguments.
16
+ #
17
+ # @param opts [OptionParser]
18
+ def set_opts(opts)
19
+ opts.banner = <<END
20
+ Usage: sass-convert [options] [INPUT] [OUTPUT]
21
+
22
+ Description:
23
+ Converts between CSS, indented syntax, and SCSS files. For example,
24
+ this can convert from the indented syntax to SCSS, or from CSS to
25
+ SCSS (adding appropriate nesting).
26
+ END
27
+
28
+ common_options(opts)
29
+ style(opts)
30
+ input_and_output(opts)
31
+ miscellaneous(opts)
32
+ end
33
+
34
+ # Processes the options set by the command-line arguments,
35
+ # and runs the CSS compiler appropriately.
36
+ def process_result
37
+ require 'sass'
38
+
39
+ if @options[:recursive]
40
+ process_directory
41
+ return
42
+ end
43
+
44
+ super
45
+ input = @options[:input]
46
+ if File.directory?(input)
47
+ raise "Error: '#{input.path}' is a directory (did you mean to use --recursive?)"
48
+ end
49
+ output = @options[:output]
50
+ output = input if @options[:in_place]
51
+ process_file(input, output)
52
+ end
53
+
54
+ private
55
+
56
+ def common_options(opts)
57
+ opts.separator ''
58
+ opts.separator 'Common Options:'
59
+
60
+ opts.on('-F', '--from FORMAT',
61
+ 'The format to convert from. Can be css, scss, sass.',
62
+ 'By default, this is inferred from the input filename.',
63
+ 'If there is none, defaults to css.') do |name|
64
+ @options[:from] = name.downcase.to_sym
65
+ raise "sass-convert no longer supports LessCSS." if @options[:from] == :less
66
+ unless [:css, :scss, :sass].include?(@options[:from])
67
+ raise "Unknown format for sass-convert --from: #{name}"
68
+ end
69
+ end
70
+
71
+ opts.on('-T', '--to FORMAT',
72
+ 'The format to convert to. Can be scss or sass.',
73
+ 'By default, this is inferred from the output filename.',
74
+ 'If there is none, defaults to sass.') do |name|
75
+ @options[:to] = name.downcase.to_sym
76
+ unless [:scss, :sass].include?(@options[:to])
77
+ raise "Unknown format for sass-convert --to: #{name}"
78
+ end
79
+ end
80
+
81
+ opts.on('-i', '--in-place',
82
+ 'Convert a file to its own syntax.',
83
+ 'This can be used to update some deprecated syntax.') do
84
+ @options[:in_place] = true
85
+ end
86
+
87
+ opts.on('-R', '--recursive',
88
+ 'Convert all the files in a directory. Requires --from and --to.') do
89
+ @options[:recursive] = true
90
+ end
91
+
92
+ opts.on("-?", "-h", "--help", "Show this help message.") do
93
+ puts opts
94
+ exit
95
+ end
96
+
97
+ opts.on("-v", "--version", "Print the Sass version.") do
98
+ puts("Sass #{Sass.version[:string]}")
99
+ exit
100
+ end
101
+ end
102
+
103
+ def style(opts)
104
+ opts.separator ''
105
+ opts.separator 'Style:'
106
+
107
+ opts.on('--dasherize', 'Convert underscores to dashes.') do
108
+ @options[:for_tree][:dasherize] = true
109
+ end
110
+
111
+ opts.on(
112
+ '--indent NUM',
113
+ 'How many spaces to use for each level of indentation. Defaults to 2.',
114
+ '"t" means use hard tabs.'
115
+ ) do |indent|
116
+ if indent == 't'
117
+ @options[:for_tree][:indent] = "\t"
118
+ else
119
+ @options[:for_tree][:indent] = " " * indent.to_i
120
+ end
121
+ end
122
+
123
+ opts.on('--old', 'Output the old-style ":prop val" property syntax.',
124
+ 'Only meaningful when generating Sass.') do
125
+ @options[:for_tree][:old] = true
126
+ end
127
+ end
128
+
129
+ def input_and_output(opts)
130
+ opts.separator ''
131
+ opts.separator 'Input and Output:'
132
+
133
+ opts.on('-s', '--stdin', :NONE,
134
+ 'Read input from standard input instead of an input file.',
135
+ 'This is the default if no input file is specified. Requires --from.') do
136
+ @options[:input] = $stdin
137
+ end
138
+
139
+ encoding_option(opts)
140
+
141
+ opts.on('--unix-newlines', 'Use Unix-style newlines in written files.',
142
+ ('Always true on Unix.' unless Sass::Util.windows?)) do
143
+ @options[:unix_newlines] = true if Sass::Util.windows?
144
+ end
145
+ end
146
+
147
+ def miscellaneous(opts)
148
+ opts.separator ''
149
+ opts.separator 'Miscellaneous:'
150
+
151
+ opts.on('--cache-location PATH',
152
+ 'The path to save parsed Sass files. Defaults to .sass-cache.') do |loc|
153
+ @options[:for_engine][:cache_location] = loc
154
+ end
155
+
156
+ opts.on('-C', '--no-cache', "Don't cache to sassc files.") do
157
+ @options[:for_engine][:read_cache] = false
158
+ end
159
+
160
+ opts.on('-q', '--quiet', 'Silence warnings and status messages during conversion.') do |bool|
161
+ @options[:for_engine][:quiet] = bool
162
+ end
163
+
164
+ opts.on('--trace', :NONE, 'Show a full Ruby stack trace on error') do
165
+ @options[:trace] = true
166
+ end
167
+ end
168
+
169
+ def process_directory
170
+ @options[:input] = @args.shift
171
+ unless @options[:input]
172
+ raise "Error: directory required when using --recursive."
173
+ end
174
+
175
+ output = @options[:output] = @args.shift
176
+ raise "Error: --from required when using --recursive." unless @options[:from]
177
+ raise "Error: --to required when using --recursive." unless @options[:to]
178
+ unless File.directory?(@options[:input])
179
+ raise "Error: '#{@options[:input]}' is not a directory"
180
+ end
181
+ if @options[:output] && File.exist?(@options[:output]) &&
182
+ !File.directory?(@options[:output])
183
+ raise "Error: '#{@options[:output]}' is not a directory"
184
+ end
185
+ @options[:output] ||= @options[:input]
186
+
187
+ if @options[:to] == @options[:from] && !@options[:in_place]
188
+ fmt = @options[:from]
189
+ raise "Error: converting from #{fmt} to #{fmt} without --in-place"
190
+ end
191
+
192
+ ext = @options[:from]
193
+ Sass::Util.glob("#{@options[:input]}/**/*.#{ext}") do |f|
194
+ output =
195
+ if @options[:in_place]
196
+ f
197
+ elsif @options[:output]
198
+ output_name = f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
199
+ output_name[0...@options[:input].size] = @options[:output]
200
+ output_name
201
+ else
202
+ f.gsub(/\.(c|sa|sc|le)ss$/, ".#{@options[:to]}")
203
+ end
204
+
205
+ unless File.directory?(File.dirname(output))
206
+ puts_action :directory, :green, File.dirname(output)
207
+ FileUtils.mkdir_p(File.dirname(output))
208
+ end
209
+ puts_action :convert, :green, f
210
+ if File.exist?(output)
211
+ puts_action :overwrite, :yellow, output
212
+ else
213
+ puts_action :create, :green, output
214
+ end
215
+
216
+ process_file(f, output)
217
+ end
218
+ end
219
+
220
+ def process_file(input, output)
221
+ input_path, output_path = path_for(input), path_for(output)
222
+ if input_path
223
+ @options[:from] ||=
224
+ case input_path
225
+ when /\.scss$/; :scss
226
+ when /\.sass$/; :sass
227
+ when /\.less$/; raise "sass-convert no longer supports LessCSS."
228
+ when /\.css$/; :css
229
+ end
230
+ elsif @options[:in_place]
231
+ raise "Error: the --in-place option requires a filename."
232
+ end
233
+
234
+ if output_path
235
+ @options[:to] ||=
236
+ case output_path
237
+ when /\.scss$/; :scss
238
+ when /\.sass$/; :sass
239
+ end
240
+ end
241
+
242
+ @options[:from] ||= :css
243
+ @options[:to] ||= :sass
244
+ @options[:for_engine][:syntax] = @options[:from]
245
+
246
+ out =
247
+ Sass::Util.silence_sass_warnings do
248
+ if @options[:from] == :css
249
+ require 'sass/css'
250
+ Sass::CSS.new(read(input), @options[:for_tree]).render(@options[:to])
251
+ else
252
+ if input_path
253
+ Sass::Engine.for_file(input_path, @options[:for_engine])
254
+ else
255
+ Sass::Engine.new(read(input), @options[:for_engine])
256
+ end.to_tree.send("to_#{@options[:to]}", @options[:for_tree])
257
+ end
258
+ end
259
+
260
+ output = input_path if @options[:in_place]
261
+ write_output(out, output)
262
+ rescue Sass::SyntaxError => e
263
+ raise e if @options[:trace]
264
+ file = " of #{e.sass_filename}" if e.sass_filename
265
+ raise "Error on line #{e.sass_line}#{file}: #{e.message}\n Use --trace for backtrace"
266
+ rescue LoadError => err
267
+ handle_load_error(err)
268
+ end
269
+
270
+ def path_for(file)
271
+ return file.path if file.is_a?(File)
272
+ return file if file.is_a?(String)
273
+ end
274
+
275
+ def read(file)
276
+ if file.respond_to?(:read)
277
+ file.read
278
+ else
279
+ open(file, 'rb') {|f| f.read}
280
+ end
281
+ end
282
+ end
283
+ end