sass4 4.0.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.
Files changed (147) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +13 -0
  3. data/AGENTS.md +534 -0
  4. data/CODE_OF_CONDUCT.md +10 -0
  5. data/CONTRIBUTING.md +148 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.md +242 -0
  8. data/VERSION +1 -0
  9. data/VERSION_NAME +1 -0
  10. data/bin/sass +13 -0
  11. data/bin/sass-convert +12 -0
  12. data/bin/scss +13 -0
  13. data/extra/sass-spec-ref.sh +40 -0
  14. data/extra/update_watch.rb +13 -0
  15. data/init.rb +18 -0
  16. data/lib/sass/cache_stores/base.rb +88 -0
  17. data/lib/sass/cache_stores/chain.rb +34 -0
  18. data/lib/sass/cache_stores/filesystem.rb +60 -0
  19. data/lib/sass/cache_stores/memory.rb +46 -0
  20. data/lib/sass/cache_stores/null.rb +25 -0
  21. data/lib/sass/cache_stores.rb +15 -0
  22. data/lib/sass/callbacks.rb +67 -0
  23. data/lib/sass/css.rb +407 -0
  24. data/lib/sass/deprecation.rb +55 -0
  25. data/lib/sass/engine.rb +1236 -0
  26. data/lib/sass/environment.rb +236 -0
  27. data/lib/sass/error.rb +198 -0
  28. data/lib/sass/exec/base.rb +188 -0
  29. data/lib/sass/exec/sass_convert.rb +283 -0
  30. data/lib/sass/exec/sass_scss.rb +436 -0
  31. data/lib/sass/exec.rb +9 -0
  32. data/lib/sass/features.rb +48 -0
  33. data/lib/sass/importers/base.rb +182 -0
  34. data/lib/sass/importers/deprecated_path.rb +51 -0
  35. data/lib/sass/importers/filesystem.rb +221 -0
  36. data/lib/sass/importers.rb +23 -0
  37. data/lib/sass/logger/base.rb +47 -0
  38. data/lib/sass/logger/delayed.rb +50 -0
  39. data/lib/sass/logger/log_level.rb +45 -0
  40. data/lib/sass/logger.rb +17 -0
  41. data/lib/sass/media.rb +210 -0
  42. data/lib/sass/plugin/compiler.rb +552 -0
  43. data/lib/sass/plugin/configuration.rb +134 -0
  44. data/lib/sass/plugin/generic.rb +15 -0
  45. data/lib/sass/plugin/merb.rb +48 -0
  46. data/lib/sass/plugin/rack.rb +60 -0
  47. data/lib/sass/plugin/rails.rb +47 -0
  48. data/lib/sass/plugin/staleness_checker.rb +199 -0
  49. data/lib/sass/plugin.rb +134 -0
  50. data/lib/sass/railtie.rb +10 -0
  51. data/lib/sass/repl.rb +57 -0
  52. data/lib/sass/root.rb +7 -0
  53. data/lib/sass/script/css_lexer.rb +33 -0
  54. data/lib/sass/script/css_parser.rb +36 -0
  55. data/lib/sass/script/functions.rb +3103 -0
  56. data/lib/sass/script/lexer.rb +518 -0
  57. data/lib/sass/script/parser.rb +1164 -0
  58. data/lib/sass/script/tree/funcall.rb +314 -0
  59. data/lib/sass/script/tree/interpolation.rb +220 -0
  60. data/lib/sass/script/tree/list_literal.rb +119 -0
  61. data/lib/sass/script/tree/literal.rb +49 -0
  62. data/lib/sass/script/tree/map_literal.rb +64 -0
  63. data/lib/sass/script/tree/node.rb +119 -0
  64. data/lib/sass/script/tree/operation.rb +149 -0
  65. data/lib/sass/script/tree/selector.rb +26 -0
  66. data/lib/sass/script/tree/string_interpolation.rb +125 -0
  67. data/lib/sass/script/tree/unary_operation.rb +69 -0
  68. data/lib/sass/script/tree/variable.rb +57 -0
  69. data/lib/sass/script/tree.rb +16 -0
  70. data/lib/sass/script/value/arg_list.rb +36 -0
  71. data/lib/sass/script/value/base.rb +258 -0
  72. data/lib/sass/script/value/bool.rb +35 -0
  73. data/lib/sass/script/value/callable.rb +25 -0
  74. data/lib/sass/script/value/color.rb +704 -0
  75. data/lib/sass/script/value/function.rb +19 -0
  76. data/lib/sass/script/value/helpers.rb +298 -0
  77. data/lib/sass/script/value/list.rb +135 -0
  78. data/lib/sass/script/value/map.rb +70 -0
  79. data/lib/sass/script/value/null.rb +44 -0
  80. data/lib/sass/script/value/number.rb +564 -0
  81. data/lib/sass/script/value/string.rb +138 -0
  82. data/lib/sass/script/value.rb +13 -0
  83. data/lib/sass/script.rb +66 -0
  84. data/lib/sass/scss/css_parser.rb +61 -0
  85. data/lib/sass/scss/parser.rb +1343 -0
  86. data/lib/sass/scss/rx.rb +134 -0
  87. data/lib/sass/scss/static_parser.rb +351 -0
  88. data/lib/sass/scss.rb +14 -0
  89. data/lib/sass/selector/abstract_sequence.rb +112 -0
  90. data/lib/sass/selector/comma_sequence.rb +195 -0
  91. data/lib/sass/selector/pseudo.rb +291 -0
  92. data/lib/sass/selector/sequence.rb +661 -0
  93. data/lib/sass/selector/simple.rb +124 -0
  94. data/lib/sass/selector/simple_sequence.rb +348 -0
  95. data/lib/sass/selector.rb +327 -0
  96. data/lib/sass/shared.rb +76 -0
  97. data/lib/sass/source/map.rb +209 -0
  98. data/lib/sass/source/position.rb +39 -0
  99. data/lib/sass/source/range.rb +41 -0
  100. data/lib/sass/stack.rb +140 -0
  101. data/lib/sass/supports.rb +225 -0
  102. data/lib/sass/tree/at_root_node.rb +83 -0
  103. data/lib/sass/tree/charset_node.rb +22 -0
  104. data/lib/sass/tree/comment_node.rb +82 -0
  105. data/lib/sass/tree/content_node.rb +9 -0
  106. data/lib/sass/tree/css_import_node.rb +68 -0
  107. data/lib/sass/tree/debug_node.rb +18 -0
  108. data/lib/sass/tree/directive_node.rb +59 -0
  109. data/lib/sass/tree/each_node.rb +24 -0
  110. data/lib/sass/tree/error_node.rb +18 -0
  111. data/lib/sass/tree/extend_node.rb +43 -0
  112. data/lib/sass/tree/for_node.rb +36 -0
  113. data/lib/sass/tree/function_node.rb +44 -0
  114. data/lib/sass/tree/if_node.rb +52 -0
  115. data/lib/sass/tree/import_node.rb +75 -0
  116. data/lib/sass/tree/keyframe_rule_node.rb +15 -0
  117. data/lib/sass/tree/media_node.rb +48 -0
  118. data/lib/sass/tree/mixin_def_node.rb +38 -0
  119. data/lib/sass/tree/mixin_node.rb +52 -0
  120. data/lib/sass/tree/node.rb +240 -0
  121. data/lib/sass/tree/prop_node.rb +162 -0
  122. data/lib/sass/tree/return_node.rb +19 -0
  123. data/lib/sass/tree/root_node.rb +44 -0
  124. data/lib/sass/tree/rule_node.rb +153 -0
  125. data/lib/sass/tree/supports_node.rb +38 -0
  126. data/lib/sass/tree/trace_node.rb +33 -0
  127. data/lib/sass/tree/variable_node.rb +36 -0
  128. data/lib/sass/tree/visitors/base.rb +72 -0
  129. data/lib/sass/tree/visitors/check_nesting.rb +173 -0
  130. data/lib/sass/tree/visitors/convert.rb +350 -0
  131. data/lib/sass/tree/visitors/cssize.rb +362 -0
  132. data/lib/sass/tree/visitors/deep_copy.rb +107 -0
  133. data/lib/sass/tree/visitors/extend.rb +64 -0
  134. data/lib/sass/tree/visitors/perform.rb +572 -0
  135. data/lib/sass/tree/visitors/set_options.rb +139 -0
  136. data/lib/sass/tree/visitors/to_css.rb +440 -0
  137. data/lib/sass/tree/warn_node.rb +18 -0
  138. data/lib/sass/tree/while_node.rb +18 -0
  139. data/lib/sass/util/multibyte_string_scanner.rb +151 -0
  140. data/lib/sass/util/normalized_map.rb +122 -0
  141. data/lib/sass/util/subset_map.rb +109 -0
  142. data/lib/sass/util/test.rb +9 -0
  143. data/lib/sass/util.rb +1137 -0
  144. data/lib/sass/version.rb +120 -0
  145. data/lib/sass.rb +102 -0
  146. data/rails/init.rb +1 -0
  147. metadata +283 -0
@@ -0,0 +1,1236 @@
1
+ require 'set'
2
+ require 'digest/sha1'
3
+ require 'sass/cache_stores'
4
+ require 'sass/deprecation'
5
+ require 'sass/source/position'
6
+ require 'sass/source/range'
7
+ require 'sass/source/map'
8
+ require 'sass/tree/node'
9
+ require 'sass/tree/root_node'
10
+ require 'sass/tree/rule_node'
11
+ require 'sass/tree/comment_node'
12
+ require 'sass/tree/prop_node'
13
+ require 'sass/tree/directive_node'
14
+ require 'sass/tree/media_node'
15
+ require 'sass/tree/supports_node'
16
+ require 'sass/tree/css_import_node'
17
+ require 'sass/tree/variable_node'
18
+ require 'sass/tree/mixin_def_node'
19
+ require 'sass/tree/mixin_node'
20
+ require 'sass/tree/trace_node'
21
+ require 'sass/tree/content_node'
22
+ require 'sass/tree/function_node'
23
+ require 'sass/tree/return_node'
24
+ require 'sass/tree/extend_node'
25
+ require 'sass/tree/if_node'
26
+ require 'sass/tree/while_node'
27
+ require 'sass/tree/for_node'
28
+ require 'sass/tree/each_node'
29
+ require 'sass/tree/debug_node'
30
+ require 'sass/tree/warn_node'
31
+ require 'sass/tree/import_node'
32
+ require 'sass/tree/charset_node'
33
+ require 'sass/tree/at_root_node'
34
+ require 'sass/tree/keyframe_rule_node'
35
+ require 'sass/tree/error_node'
36
+ require 'sass/tree/visitors/base'
37
+ require 'sass/tree/visitors/perform'
38
+ require 'sass/tree/visitors/cssize'
39
+ require 'sass/tree/visitors/extend'
40
+ require 'sass/tree/visitors/convert'
41
+ require 'sass/tree/visitors/to_css'
42
+ require 'sass/tree/visitors/deep_copy'
43
+ require 'sass/tree/visitors/set_options'
44
+ require 'sass/tree/visitors/check_nesting'
45
+ require 'sass/selector'
46
+ require 'sass/environment'
47
+ require 'sass/script'
48
+ require 'sass/scss'
49
+ require 'sass/stack'
50
+ require 'sass/error'
51
+ require 'sass/importers'
52
+ require 'sass/shared'
53
+ require 'sass/media'
54
+ require 'sass/supports'
55
+
56
+ module Sass
57
+ # A Sass mixin or function.
58
+ #
59
+ # `name`: `String`
60
+ # : The name of the mixin/function.
61
+ #
62
+ # `args`: `Array<(Script::Tree::Node, Script::Tree::Node)>`
63
+ # : The arguments for the mixin/function.
64
+ # Each element is a tuple containing the variable node of the argument
65
+ # and the parse tree for the default value of the argument.
66
+ #
67
+ # `splat`: `Script::Tree::Node?`
68
+ # : The variable node of the splat argument for this callable, or null.
69
+ #
70
+ # `environment`: {Sass::Environment}
71
+ # : The environment in which the mixin/function was defined.
72
+ # This is captured so that the mixin/function can have access
73
+ # to local variables defined in its scope.
74
+ #
75
+ # `tree`: `Array<Tree::Node>`
76
+ # : The parse tree for the mixin/function.
77
+ #
78
+ # `has_content`: `Boolean`
79
+ # : Whether the callable accepts a content block.
80
+ #
81
+ # `type`: `String`
82
+ # : The user-friendly name of the type of the callable.
83
+ #
84
+ # `origin`: `Symbol`
85
+ # : From whence comes the callable: `:stylesheet`, `:builtin`, `:css`
86
+ # A callable with an origin of `:stylesheet` was defined in the stylesheet itself.
87
+ # A callable with an origin of `:builtin` was defined in ruby.
88
+ # A callable (function) with an origin of `:css` returns a function call with arguments to CSS.
89
+ Callable = Struct.new(:name, :args, :splat, :environment, :tree, :has_content, :type, :origin)
90
+
91
+ # This class handles the parsing and compilation of the Sass template.
92
+ # Example usage:
93
+ #
94
+ # template = File.read('stylesheets/sassy.sass')
95
+ # sass_engine = Sass::Engine.new(template)
96
+ # output = sass_engine.render
97
+ # puts output
98
+ class Engine
99
+ @@old_property_deprecation = Deprecation.new
100
+
101
+ # A line of Sass code.
102
+ #
103
+ # `text`: `String`
104
+ # : The text in the line, without any whitespace at the beginning or end.
105
+ #
106
+ # `tabs`: `Integer`
107
+ # : The level of indentation of the line.
108
+ #
109
+ # `index`: `Integer`
110
+ # : The line number in the original document.
111
+ #
112
+ # `offset`: `Integer`
113
+ # : The number of bytes in on the line that the text begins.
114
+ # This ends up being the number of bytes of leading whitespace.
115
+ #
116
+ # `filename`: `String`
117
+ # : The name of the file in which this line appeared.
118
+ #
119
+ # `children`: `Array<Line>`
120
+ # : The lines nested below this one.
121
+ #
122
+ # `comment_tab_str`: `String?`
123
+ # : The prefix indentation for this comment, if it is a comment.
124
+ class Line < Struct.new(:text, :tabs, :index, :offset, :filename, :children, :comment_tab_str)
125
+ def comment?
126
+ text[0] == COMMENT_CHAR && (text[1] == SASS_COMMENT_CHAR || text[1] == CSS_COMMENT_CHAR)
127
+ end
128
+ end
129
+
130
+ # The character that begins a CSS property.
131
+ PROPERTY_CHAR = ?:
132
+
133
+ # The character that designates the beginning of a comment,
134
+ # either Sass or CSS.
135
+ COMMENT_CHAR = ?/
136
+
137
+ # The character that follows the general COMMENT_CHAR and designates a Sass comment,
138
+ # which is not output as a CSS comment.
139
+ SASS_COMMENT_CHAR = ?/
140
+
141
+ # The character that indicates that a comment allows interpolation
142
+ # and should be preserved even in `:compressed` mode.
143
+ SASS_LOUD_COMMENT_CHAR = ?!
144
+
145
+ # The character that follows the general COMMENT_CHAR and designates a CSS comment,
146
+ # which is embedded in the CSS document.
147
+ CSS_COMMENT_CHAR = ?*
148
+
149
+ # The character used to denote a compiler directive.
150
+ DIRECTIVE_CHAR = ?@
151
+
152
+ # Designates a non-parsed rule.
153
+ ESCAPE_CHAR = ?\\
154
+
155
+ # Designates block as mixin definition rather than CSS rules to output
156
+ MIXIN_DEFINITION_CHAR = ?=
157
+
158
+ # Includes named mixin declared using MIXIN_DEFINITION_CHAR
159
+ MIXIN_INCLUDE_CHAR = ?+
160
+
161
+ # The regex that matches and extracts data from
162
+ # properties of the form `:name prop`.
163
+ PROPERTY_OLD = /^:([^\s=:"]+)\s*(?:\s+|$)(.*)/
164
+
165
+ # The default options for Sass::Engine.
166
+ # @api public
167
+ DEFAULT_OPTIONS = {
168
+ :style => :nested,
169
+ :load_paths => [],
170
+ :cache => true,
171
+ :cache_location => './.sass-cache',
172
+ :syntax => :sass,
173
+ :filesystem_importer => Sass::Importers::Filesystem
174
+ }.freeze
175
+
176
+ # Converts a Sass options hash into a standard form, filling in
177
+ # default values and resolving aliases.
178
+ #
179
+ # @param options [{Symbol => Object}] The options hash;
180
+ # see {file:SASS_REFERENCE.md#Options the Sass options documentation}
181
+ # @return [{Symbol => Object}] The normalized options hash.
182
+ # @private
183
+ def self.normalize_options(options)
184
+ options = DEFAULT_OPTIONS.merge(options.reject {|_k, v| v.nil?})
185
+
186
+ # If the `:filename` option is passed in without an importer,
187
+ # assume it's using the default filesystem importer.
188
+ options[:importer] ||= options[:filesystem_importer].new(".") if options[:filename]
189
+
190
+ # Tracks the original filename of the top-level Sass file
191
+ options[:original_filename] ||= options[:filename]
192
+
193
+ options[:cache_store] ||= Sass::CacheStores::Chain.new(
194
+ Sass::CacheStores::Memory.new, Sass::CacheStores::Filesystem.new(options[:cache_location]))
195
+ # Support both, because the docs said one and the other actually worked
196
+ # for quite a long time.
197
+ options[:line_comments] ||= options[:line_numbers]
198
+
199
+ options[:load_paths] = (options[:load_paths] + Sass.load_paths).map do |p|
200
+ next p unless p.is_a?(String) || (defined?(Pathname) && p.is_a?(Pathname))
201
+ options[:filesystem_importer].new(p.to_s)
202
+ end
203
+
204
+ # Remove any deprecated importers if the location is imported explicitly
205
+ options[:load_paths].reject! do |importer|
206
+ importer.is_a?(Sass::Importers::DeprecatedPath) &&
207
+ options[:load_paths].find do |other_importer|
208
+ other_importer.is_a?(Sass::Importers::Filesystem) &&
209
+ other_importer != importer &&
210
+ other_importer.root == importer.root
211
+ end
212
+ end
213
+
214
+ # Backwards compatibility
215
+ options[:property_syntax] ||= options[:attribute_syntax]
216
+ case options[:property_syntax]
217
+ when :alternate; options[:property_syntax] = :new
218
+ when :normal; options[:property_syntax] = :old
219
+ end
220
+ options[:sourcemap] = :auto if options[:sourcemap] == true
221
+ options[:sourcemap] = :none if options[:sourcemap] == false
222
+
223
+ options
224
+ end
225
+
226
+ # Returns the {Sass::Engine} for the given file.
227
+ # This is preferable to Sass::Engine.new when reading from a file
228
+ # because it properly sets up the Engine's metadata,
229
+ # enables parse-tree caching,
230
+ # and infers the syntax from the filename.
231
+ #
232
+ # @param filename [String] The path to the Sass or SCSS file
233
+ # @param options [{Symbol => Object}] The options hash;
234
+ # See {file:SASS_REFERENCE.md#Options the Sass options documentation}.
235
+ # @return [Sass::Engine] The Engine for the given Sass or SCSS file.
236
+ # @raise [Sass::SyntaxError] if there's an error in the document.
237
+ def self.for_file(filename, options)
238
+ had_syntax = options[:syntax]
239
+
240
+ if had_syntax
241
+ # Use what was explicitly specified
242
+ elsif filename =~ /\.scss$/
243
+ options.merge!(:syntax => :scss)
244
+ elsif filename =~ /\.sass$/
245
+ options.merge!(:syntax => :sass)
246
+ end
247
+
248
+ Sass::Engine.new(File.read(filename), options.merge(:filename => filename))
249
+ end
250
+
251
+ # The options for the Sass engine.
252
+ # See {file:SASS_REFERENCE.md#Options the Sass options documentation}.
253
+ #
254
+ # @return [{Symbol => Object}]
255
+ attr_reader :options
256
+
257
+ # Creates a new Engine. Note that Engine should only be used directly
258
+ # when compiling in-memory Sass code.
259
+ # If you're compiling a single Sass file from the filesystem,
260
+ # use \{Sass::Engine.for\_file}.
261
+ # If you're compiling multiple files from the filesystem,
262
+ # use {Sass::Plugin}.
263
+ #
264
+ # @param template [String] The Sass template.
265
+ # This template can be encoded using any encoding
266
+ # that can be converted to Unicode.
267
+ # If the template contains an `@charset` declaration,
268
+ # that overrides the Ruby encoding
269
+ # (see {file:SASS_REFERENCE.md#Encodings the encoding documentation})
270
+ # @param options [{Symbol => Object}] An options hash.
271
+ # See {file:SASS_REFERENCE.md#Options the Sass options documentation}.
272
+ # @see {Sass::Engine.for_file}
273
+ # @see {Sass::Plugin}
274
+ def initialize(template, options = {})
275
+ @options = self.class.normalize_options(options)
276
+ @template = template
277
+ @checked_encoding = false
278
+ @filename = nil
279
+ @line = nil
280
+ end
281
+
282
+ # Render the template to CSS.
283
+ #
284
+ # @return [String] The CSS
285
+ # @raise [Sass::SyntaxError] if there's an error in the document
286
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
287
+ # cannot be converted to UTF-8
288
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
289
+ def render
290
+ return _to_tree.render unless @options[:quiet]
291
+ Sass::Util.silence_sass_warnings {_to_tree.render}
292
+ end
293
+
294
+ # Render the template to CSS and return the source map.
295
+ #
296
+ # @param sourcemap_uri [String] The sourcemap URI to use in the
297
+ # `@sourceMappingURL` comment. If this is relative, it should be relative
298
+ # to the location of the CSS file.
299
+ # @return [(String, Sass::Source::Map)] The rendered CSS and the associated
300
+ # source map
301
+ # @raise [Sass::SyntaxError] if there's an error in the document, or if the
302
+ # public URL for this document couldn't be determined.
303
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
304
+ # cannot be converted to UTF-8
305
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
306
+ def render_with_sourcemap(sourcemap_uri)
307
+ return _render_with_sourcemap(sourcemap_uri) unless @options[:quiet]
308
+ Sass::Util.silence_sass_warnings {_render_with_sourcemap(sourcemap_uri)}
309
+ end
310
+
311
+ alias_method :to_css, :render
312
+
313
+ # Parses the document into its parse tree. Memoized.
314
+ #
315
+ # @return [Sass::Tree::Node] The root of the parse tree.
316
+ # @raise [Sass::SyntaxError] if there's an error in the document
317
+ def to_tree
318
+ @tree ||= if @options[:quiet]
319
+ Sass::Util.silence_sass_warnings {_to_tree}
320
+ else
321
+ _to_tree
322
+ end
323
+ end
324
+
325
+ # Returns the original encoding of the document.
326
+ #
327
+ # @return [Encoding, nil]
328
+ # @raise [Encoding::UndefinedConversionError] if the source encoding
329
+ # cannot be converted to UTF-8
330
+ # @raise [ArgumentError] if the document uses an unknown encoding with `@charset`
331
+ def source_encoding
332
+ check_encoding!
333
+ @source_encoding
334
+ end
335
+
336
+ # Gets a set of all the documents
337
+ # that are (transitive) dependencies of this document,
338
+ # not including the document itself.
339
+ #
340
+ # @return [[Sass::Engine]] The dependency documents.
341
+ def dependencies
342
+ _dependencies(Set.new, engines = Set.new)
343
+ Sass::Util.array_minus(engines, [self])
344
+ end
345
+
346
+ # Helper for \{#dependencies}.
347
+ #
348
+ # @private
349
+ def _dependencies(seen, engines)
350
+ key = [@options[:filename], @options[:importer]]
351
+ return if seen.include?(key)
352
+ seen << key
353
+ engines << self
354
+ to_tree.grep(Tree::ImportNode) do |n|
355
+ next if n.css_import?
356
+ n.imported_file._dependencies(seen, engines)
357
+ end
358
+ end
359
+
360
+ private
361
+
362
+ def _render_with_sourcemap(sourcemap_uri)
363
+ filename = @options[:filename]
364
+ importer = @options[:importer]
365
+ sourcemap_dir = @options[:sourcemap_filename] &&
366
+ File.dirname(File.expand_path(@options[:sourcemap_filename]))
367
+ if filename.nil?
368
+ raise Sass::SyntaxError.new(<<ERR)
369
+ Error generating source map: couldn't determine public URL for the source stylesheet.
370
+ No filename is available so there's nothing for the source map to link to.
371
+ ERR
372
+ elsif importer.nil?
373
+ raise Sass::SyntaxError.new(<<ERR)
374
+ Error generating source map: couldn't determine public URL for "#{filename}".
375
+ Without a public URL, there's nothing for the source map to link to.
376
+ An importer was not set for this file.
377
+ ERR
378
+ elsif Sass::Util.silence_sass_warnings do
379
+ sourcemap_dir = nil if @options[:sourcemap] == :file
380
+ importer.public_url(filename, sourcemap_dir).nil?
381
+ end
382
+ raise Sass::SyntaxError.new(<<ERR)
383
+ Error generating source map: couldn't determine public URL for "#{filename}".
384
+ Without a public URL, there's nothing for the source map to link to.
385
+ Custom importers should define the #public_url method.
386
+ ERR
387
+ end
388
+
389
+ rendered, sourcemap = _to_tree.render_with_sourcemap
390
+ compressed = @options[:style] == :compressed
391
+ rendered << "\n" if rendered[-1] != ?\n
392
+ rendered << "\n" unless compressed
393
+ rendered << "/*# sourceMappingURL="
394
+ rendered << URI::DEFAULT_PARSER.escape(sourcemap_uri)
395
+ rendered << " */\n"
396
+ return rendered, sourcemap
397
+ end
398
+
399
+ def _to_tree
400
+ check_encoding!
401
+
402
+ if (@options[:cache] || @options[:read_cache]) &&
403
+ @options[:filename] && @options[:importer]
404
+ key = sassc_key
405
+ sha = Digest::SHA1.hexdigest(@template)
406
+
407
+ if (root = @options[:cache_store].retrieve(key, sha))
408
+ root.options = @options
409
+ return root
410
+ end
411
+ end
412
+
413
+ if @options[:syntax] == :scss
414
+ root = Sass::SCSS::Parser.new(@template, @options[:filename], @options[:importer]).parse
415
+ else
416
+ root = Tree::RootNode.new(@template)
417
+ append_children(root, tree(tabulate(@template)).first, true)
418
+ end
419
+
420
+ root.options = @options
421
+ if @options[:cache] && key && sha
422
+ begin
423
+ old_options = root.options
424
+ root.options = {}
425
+ @options[:cache_store].store(key, sha, root)
426
+ ensure
427
+ root.options = old_options
428
+ end
429
+ end
430
+ root
431
+ rescue SyntaxError => e
432
+ e.modify_backtrace(:filename => @options[:filename], :line => @line)
433
+ e.sass_template = @template
434
+ raise e
435
+ end
436
+
437
+ def sassc_key
438
+ @options[:cache_store].key(*@options[:importer].key(@options[:filename], @options))
439
+ end
440
+
441
+ def check_encoding!
442
+ return if @checked_encoding
443
+ @checked_encoding = true
444
+ @template, @source_encoding = Sass::Util.check_sass_encoding(@template)
445
+ end
446
+
447
+ def tabulate(string)
448
+ tab_str = nil
449
+ comment_tab_str = nil
450
+ first = true
451
+ lines = []
452
+ string.scan(/^[^\n]*?$/).each_with_index do |line, index|
453
+ index += (@options[:line] || 1)
454
+ if line.strip.empty?
455
+ lines.last.text << "\n" if lines.last && lines.last.comment?
456
+ next
457
+ end
458
+
459
+ line_tab_str = line[/^\s*/]
460
+ unless line_tab_str.empty?
461
+ if tab_str.nil?
462
+ comment_tab_str ||= line_tab_str
463
+ next if try_comment(line, lines.last, "", comment_tab_str, index)
464
+ comment_tab_str = nil
465
+ end
466
+
467
+ tab_str ||= line_tab_str
468
+
469
+ raise SyntaxError.new("Indenting at the beginning of the document is illegal.",
470
+ :line => index) if first
471
+
472
+ raise SyntaxError.new("Indentation can't use both tabs and spaces.",
473
+ :line => index) if tab_str.include?(?\s) && tab_str.include?(?\t)
474
+ end
475
+ first &&= !tab_str.nil?
476
+ if tab_str.nil?
477
+ lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
478
+ next
479
+ end
480
+
481
+ comment_tab_str ||= line_tab_str
482
+ if try_comment(line, lines.last, tab_str * lines.last.tabs, comment_tab_str, index)
483
+ next
484
+ else
485
+ comment_tab_str = nil
486
+ end
487
+
488
+ line_tabs = line_tab_str.scan(tab_str).size
489
+ if tab_str * line_tabs != line_tab_str
490
+ message = <<END.strip.tr("\n", ' ')
491
+ Inconsistent indentation: #{Sass::Shared.human_indentation line_tab_str, true} used for indentation,
492
+ but the rest of the document was indented using #{Sass::Shared.human_indentation tab_str}.
493
+ END
494
+ raise SyntaxError.new(message, :line => index)
495
+ end
496
+
497
+ lines << Line.new(line.strip, line_tabs, index, line_tab_str.size, @options[:filename], [])
498
+ end
499
+ lines
500
+ end
501
+
502
+ def try_comment(line, last, tab_str, comment_tab_str, index)
503
+ return unless last && last.comment?
504
+ # Nested comment stuff must be at least one whitespace char deeper
505
+ # than the normal indentation
506
+ return unless line =~ /^#{tab_str}\s/
507
+ unless line =~ /^(?:#{comment_tab_str})(.*)$/
508
+ raise SyntaxError.new(<<MSG.strip.tr("\n", " "), :line => index)
509
+ Inconsistent indentation:
510
+ previous line was indented by #{Sass::Shared.human_indentation comment_tab_str},
511
+ but this line was indented by #{Sass::Shared.human_indentation line[/^\s*/]}.
512
+ MSG
513
+ end
514
+
515
+ last.comment_tab_str ||= comment_tab_str
516
+ last.text << "\n" << line
517
+ true
518
+ end
519
+
520
+ def tree(arr, i = 0)
521
+ return [], i if arr[i].nil?
522
+
523
+ base = arr[i].tabs
524
+ nodes = []
525
+ while (line = arr[i]) && line.tabs >= base
526
+ if line.tabs > base
527
+ nodes.last.children, i = tree(arr, i)
528
+ else
529
+ nodes << line
530
+ i += 1
531
+ end
532
+ end
533
+ return nodes, i
534
+ end
535
+
536
+ def build_tree(parent, line, root = false)
537
+ @line = line.index
538
+ @offset = line.offset
539
+ node_or_nodes = parse_line(parent, line, root)
540
+
541
+ Array(node_or_nodes).each do |node|
542
+ # Node is a symbol if it's non-outputting, like a variable assignment
543
+ next unless node.is_a? Tree::Node
544
+
545
+ node.line = line.index
546
+ node.filename = line.filename
547
+
548
+ append_children(node, line.children, false)
549
+ end
550
+
551
+ node_or_nodes
552
+ end
553
+
554
+ def append_children(parent, children, root)
555
+ continued_rule = nil
556
+ continued_comment = nil
557
+ children.each do |line|
558
+ child = build_tree(parent, line, root)
559
+
560
+ if child.is_a?(Tree::RuleNode)
561
+ if child.continued? && child.children.empty?
562
+ if continued_rule
563
+ continued_rule.add_rules child
564
+ else
565
+ continued_rule = child
566
+ end
567
+ next
568
+ elsif continued_rule
569
+ continued_rule.add_rules child
570
+ continued_rule.children = child.children
571
+ continued_rule, child = nil, continued_rule
572
+ end
573
+ elsif continued_rule
574
+ continued_rule = nil
575
+ end
576
+
577
+ if child.is_a?(Tree::CommentNode) && child.type == :silent
578
+ if continued_comment &&
579
+ child.line == continued_comment.line +
580
+ continued_comment.lines + 1
581
+ continued_comment.value.last.sub!(%r{ \*/\Z}, '')
582
+ child.value.first.gsub!(%r{\A/\*}, ' *')
583
+ continued_comment.value += ["\n"] + child.value
584
+ next
585
+ end
586
+
587
+ continued_comment = child
588
+ end
589
+
590
+ check_for_no_children(child)
591
+ validate_and_append_child(parent, child, line, root)
592
+ end
593
+
594
+ parent
595
+ end
596
+
597
+ def validate_and_append_child(parent, child, line, root)
598
+ case child
599
+ when Array
600
+ child.each {|c| validate_and_append_child(parent, c, line, root)}
601
+ when Tree::Node
602
+ parent << child
603
+ end
604
+ end
605
+
606
+ def check_for_no_children(node)
607
+ return unless node.is_a?(Tree::RuleNode) && node.children.empty?
608
+ Sass::Util.sass_warn(<<WARNING.strip)
609
+ WARNING on line #{node.line}#{" of #{node.filename}" if node.filename}:
610
+ This selector doesn't have any properties and will not be rendered.
611
+ WARNING
612
+ end
613
+
614
+ def parse_line(parent, line, root)
615
+ case line.text[0]
616
+ when PROPERTY_CHAR
617
+ if line.text[1] == PROPERTY_CHAR ||
618
+ (@options[:property_syntax] == :new &&
619
+ line.text =~ PROPERTY_OLD && $2.empty?)
620
+ # Support CSS3-style pseudo-elements,
621
+ # which begin with ::,
622
+ # as well as pseudo-classes
623
+ # if we're using the new property syntax
624
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
625
+ else
626
+ name_start_offset = line.offset + 1 # +1 for the leading ':'
627
+ name, value = line.text.scan(PROPERTY_OLD)[0]
628
+ raise SyntaxError.new("Invalid property: \"#{line.text}\".",
629
+ :line => @line) if name.nil? || value.nil?
630
+
631
+ @@old_property_deprecation.warn(@options[:filename], @line, <<WARNING)
632
+ Old-style properties like "#{line.text}" are deprecated and will be an error in future versions of Sass.
633
+ Use "#{name}: #{value}" instead.
634
+ WARNING
635
+
636
+ value_start_offset = name_end_offset = name_start_offset + name.length
637
+ unless value.empty?
638
+ # +1 and -1 both compensate for the leading ':', which is part of line.text
639
+ value_start_offset = name_start_offset + line.text.index(value, name.length + 1) - 1
640
+ end
641
+
642
+ property = parse_property(name, parse_interp(name), value, :old, line, value_start_offset)
643
+ property.name_source_range = Sass::Source::Range.new(
644
+ Sass::Source::Position.new(@line, to_parser_offset(name_start_offset)),
645
+ Sass::Source::Position.new(@line, to_parser_offset(name_end_offset)),
646
+ @options[:filename], @options[:importer])
647
+ property
648
+ end
649
+ when ?$
650
+ parse_variable(line)
651
+ when COMMENT_CHAR
652
+ parse_comment(line)
653
+ when DIRECTIVE_CHAR
654
+ parse_directive(parent, line, root)
655
+ when ESCAPE_CHAR
656
+ Tree::RuleNode.new(parse_interp(line.text[1..-1]), full_line_range(line))
657
+ when MIXIN_DEFINITION_CHAR
658
+ parse_mixin_definition(line)
659
+ when MIXIN_INCLUDE_CHAR
660
+ if line.text[1].nil? || line.text[1] == ?\s
661
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
662
+ else
663
+ parse_mixin_include(line, root)
664
+ end
665
+ else
666
+ parse_property_or_rule(line)
667
+ end
668
+ end
669
+
670
+ def parse_property_or_rule(line)
671
+ scanner = Sass::Util::MultibyteStringScanner.new(line.text)
672
+ hack_char = scanner.scan(/[:\*\.]|\#(?!\{)/)
673
+ offset = line.offset
674
+ offset += hack_char.length if hack_char
675
+ parser = Sass::SCSS::Parser.new(scanner,
676
+ @options[:filename], @options[:importer],
677
+ @line, to_parser_offset(offset))
678
+
679
+ unless (res = parser.parse_interp_ident)
680
+ parsed = parse_interp(line.text, line.offset)
681
+ return Tree::RuleNode.new(parsed, full_line_range(line))
682
+ end
683
+
684
+ ident_range = Sass::Source::Range.new(
685
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset)),
686
+ Sass::Source::Position.new(@line, parser.offset),
687
+ @options[:filename], @options[:importer])
688
+ offset = parser.offset - 1
689
+ res.unshift(hack_char) if hack_char
690
+
691
+ # Handle comments after a property name but before the colon.
692
+ if (comment = scanner.scan(Sass::SCSS::RX::COMMENT))
693
+ res << comment
694
+ offset += comment.length
695
+ end
696
+
697
+ name = line.text[0...scanner.pos]
698
+ could_be_property =
699
+ if name.start_with?('--')
700
+ (scanned = scanner.scan(/\s*:/))
701
+ else
702
+ (scanned = scanner.scan(/\s*:(?:\s+|$)/))
703
+ end
704
+
705
+ if could_be_property # test for a property
706
+ offset += scanned.length
707
+ property = parse_property(name, res, scanner.rest, :new, line, offset)
708
+ property.name_source_range = ident_range
709
+ property
710
+ else
711
+ res.pop if comment
712
+
713
+ if (trailing = (scanner.scan(/\s*#{Sass::SCSS::RX::COMMENT}/) ||
714
+ scanner.scan(/\s*#{Sass::SCSS::RX::SINGLE_LINE_COMMENT}/)))
715
+ trailing.strip!
716
+ end
717
+ interp_parsed = parse_interp(scanner.rest)
718
+ selector_range = Sass::Source::Range.new(
719
+ ident_range.start_pos,
720
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
721
+ @options[:filename], @options[:importer])
722
+ rule = Tree::RuleNode.new(res + interp_parsed, selector_range)
723
+ rule << Tree::CommentNode.new([trailing], :silent) if trailing
724
+ rule
725
+ end
726
+ end
727
+
728
+ def parse_property(name, parsed_name, value, prop, line, start_offset)
729
+
730
+ if name.start_with?('--')
731
+ unless line.children.empty?
732
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath custom properties.",
733
+ :line => @line + 1)
734
+ end
735
+
736
+ parser = Sass::SCSS::Parser.new(value,
737
+ @options[:filename], @options[:importer],
738
+ @line, to_parser_offset(@offset))
739
+ parsed_value = parser.parse_declaration_value
740
+ end_offset = start_offset + value.length
741
+ elsif value.strip.empty?
742
+ parsed_value = [Sass::Script::Tree::Literal.new(Sass::Script::Value::String.new(""))]
743
+ end_offset = start_offset
744
+ else
745
+ expr = parse_script(value, :offset => to_parser_offset(start_offset))
746
+ end_offset = expr.source_range.end_pos.offset - 1
747
+ parsed_value = [expr]
748
+ end
749
+ node = Tree::PropNode.new(parse_interp(name), parsed_value, prop)
750
+ node.value_source_range = Sass::Source::Range.new(
751
+ Sass::Source::Position.new(line.index, to_parser_offset(start_offset)),
752
+ Sass::Source::Position.new(line.index, to_parser_offset(end_offset)),
753
+ @options[:filename], @options[:importer])
754
+ if !node.custom_property? && value.strip.empty? && line.children.empty?
755
+ raise SyntaxError.new(
756
+ "Invalid property: \"#{node.declaration}\" (no value)." +
757
+ node.pseudo_class_selector_message)
758
+ end
759
+
760
+ node
761
+ end
762
+
763
+ def parse_variable(line)
764
+ name, value, flags = line.text.scan(Script::MATCH)[0]
765
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.",
766
+ :line => @line + 1) unless line.children.empty?
767
+ raise SyntaxError.new("Invalid variable: \"#{line.text}\".",
768
+ :line => @line) unless name && value
769
+ flags = flags ? flags.split(/\s+/) : []
770
+ if (invalid_flag = flags.find {|f| f != '!default' && f != '!global'})
771
+ raise SyntaxError.new("Invalid flag \"#{invalid_flag}\".", :line => @line)
772
+ end
773
+
774
+ # This workaround is needed for the case when the variable value is part of the identifier,
775
+ # otherwise we end up with the offset equal to the value index inside the name:
776
+ # $red_color: red;
777
+ var_lhs_length = 1 + name.length # 1 stands for '$'
778
+ index = line.text.index(value, line.offset + var_lhs_length) || 0
779
+ expr = parse_script(value, :offset => to_parser_offset(line.offset + index))
780
+
781
+ Tree::VariableNode.new(name, expr, flags.include?('!default'), flags.include?('!global'))
782
+ end
783
+
784
+ def parse_comment(line)
785
+ if line.text[1] == CSS_COMMENT_CHAR || line.text[1] == SASS_COMMENT_CHAR
786
+ silent = line.text[1] == SASS_COMMENT_CHAR
787
+ loud = !silent && line.text[2] == SASS_LOUD_COMMENT_CHAR
788
+ if silent
789
+ value = [line.text]
790
+ else
791
+ value = self.class.parse_interp(
792
+ line.text, line.index, to_parser_offset(line.offset), :filename => @filename)
793
+ end
794
+ value = Sass::Util.with_extracted_values(value) do |str|
795
+ str = str.gsub(/^#{line.comment_tab_str}/m, '')[2..-1] # get rid of // or /*
796
+ format_comment_text(str, silent)
797
+ end
798
+ type = if silent
799
+ :silent
800
+ elsif loud
801
+ :loud
802
+ else
803
+ :normal
804
+ end
805
+ comment = Tree::CommentNode.new(value, type)
806
+ comment.line = line.index
807
+ text = line.text.rstrip
808
+ if text.include?("\n")
809
+ end_offset = text.length - text.rindex("\n")
810
+ else
811
+ end_offset = to_parser_offset(line.offset + text.length)
812
+ end
813
+ comment.source_range = Sass::Source::Range.new(
814
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset)),
815
+ Sass::Source::Position.new(@line + text.count("\n"), end_offset),
816
+ @options[:filename])
817
+ comment
818
+ else
819
+ Tree::RuleNode.new(parse_interp(line.text), full_line_range(line))
820
+ end
821
+ end
822
+
823
+ DIRECTIVES = Set[:mixin, :include, :function, :return, :debug, :warn, :for,
824
+ :each, :while, :if, :else, :extend, :import, :media, :charset, :content,
825
+ :at_root, :error]
826
+
827
+ def parse_directive(parent, line, root)
828
+ directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
829
+ raise SyntaxError.new("Invalid directive: '@'.") unless directive
830
+ offset = directive.size + whitespace.size + 1 if whitespace
831
+
832
+ directive_name = directive.tr('-', '_').to_sym
833
+ if DIRECTIVES.include?(directive_name)
834
+ return send("parse_#{directive_name}_directive", parent, line, root, value, offset)
835
+ end
836
+
837
+ unprefixed_directive = directive.gsub(/^-[a-z0-9]+-/i, '')
838
+ if unprefixed_directive == 'supports'
839
+ parser = Sass::SCSS::Parser.new(value, @options[:filename], @line)
840
+ return Tree::SupportsNode.new(directive, parser.parse_supports_condition)
841
+ end
842
+
843
+ Tree::DirectiveNode.new(
844
+ value.nil? ? ["@#{directive}"] : ["@#{directive} "] + parse_interp(value, offset))
845
+ end
846
+
847
+ def parse_while_directive(parent, line, root, value, offset)
848
+ raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
849
+ Tree::WhileNode.new(parse_script(value, :offset => offset))
850
+ end
851
+
852
+ def parse_if_directive(parent, line, root, value, offset)
853
+ raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
854
+ Tree::IfNode.new(parse_script(value, :offset => offset))
855
+ end
856
+
857
+ def parse_debug_directive(parent, line, root, value, offset)
858
+ raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
859
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.",
860
+ :line => @line + 1) unless line.children.empty?
861
+ offset = line.offset + line.text.index(value).to_i
862
+ Tree::DebugNode.new(parse_script(value, :offset => offset))
863
+ end
864
+
865
+ def parse_error_directive(parent, line, root, value, offset)
866
+ raise SyntaxError.new("Invalid error directive '@error': expected expression.") unless value
867
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath error directives.",
868
+ :line => @line + 1) unless line.children.empty?
869
+ offset = line.offset + line.text.index(value).to_i
870
+ Tree::ErrorNode.new(parse_script(value, :offset => offset))
871
+ end
872
+
873
+ def parse_extend_directive(parent, line, root, value, offset)
874
+ raise SyntaxError.new("Invalid extend directive '@extend': expected expression.") unless value
875
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath extend directives.",
876
+ :line => @line + 1) unless line.children.empty?
877
+ optional = !!value.gsub!(/\s+#{Sass::SCSS::RX::OPTIONAL}$/, '')
878
+ offset = line.offset + line.text.index(value).to_i
879
+ interp_parsed = parse_interp(value, offset)
880
+ selector_range = Sass::Source::Range.new(
881
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
882
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
883
+ @options[:filename], @options[:importer]
884
+ )
885
+ Tree::ExtendNode.new(interp_parsed, optional, selector_range)
886
+ end
887
+
888
+ def parse_warn_directive(parent, line, root, value, offset)
889
+ raise SyntaxError.new("Invalid warn directive '@warn': expected expression.") unless value
890
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath warn directives.",
891
+ :line => @line + 1) unless line.children.empty?
892
+ offset = line.offset + line.text.index(value).to_i
893
+ Tree::WarnNode.new(parse_script(value, :offset => offset))
894
+ end
895
+
896
+ def parse_return_directive(parent, line, root, value, offset)
897
+ raise SyntaxError.new("Invalid @return: expected expression.") unless value
898
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath return directives.",
899
+ :line => @line + 1) unless line.children.empty?
900
+ offset = line.offset + line.text.index(value).to_i
901
+ Tree::ReturnNode.new(parse_script(value, :offset => offset))
902
+ end
903
+
904
+ def parse_charset_directive(parent, line, root, value, offset)
905
+ name = value && value[/\A(["'])(.*)\1\Z/, 2] # "
906
+ raise SyntaxError.new("Invalid charset directive '@charset': expected string.") unless name
907
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath charset directives.",
908
+ :line => @line + 1) unless line.children.empty?
909
+ Tree::CharsetNode.new(name)
910
+ end
911
+
912
+ def parse_media_directive(parent, line, root, value, offset)
913
+ parser = Sass::SCSS::Parser.new(value,
914
+ @options[:filename], @options[:importer],
915
+ @line, to_parser_offset(@offset))
916
+ offset = line.offset + line.text.index('media').to_i - 1
917
+ parsed_media_query_list = parser.parse_media_query_list.to_a
918
+ node = Tree::MediaNode.new(parsed_media_query_list)
919
+ node.source_range = Sass::Source::Range.new(
920
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
921
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
922
+ @options[:filename], @options[:importer])
923
+ node
924
+ end
925
+
926
+ def parse_at_root_directive(parent, line, root, value, offset)
927
+ return Sass::Tree::AtRootNode.new unless value
928
+
929
+ if value.start_with?('(')
930
+ parser = Sass::SCSS::Parser.new(value,
931
+ @options[:filename], @options[:importer],
932
+ @line, to_parser_offset(@offset))
933
+ offset = line.offset + line.text.index('at-root').to_i - 1
934
+ return Tree::AtRootNode.new(parser.parse_at_root_query)
935
+ end
936
+
937
+ at_root_node = Tree::AtRootNode.new
938
+ parsed = parse_interp(value, offset)
939
+ rule_node = Tree::RuleNode.new(parsed, full_line_range(line))
940
+
941
+ # The caller expects to automatically add children to the returned node
942
+ # and we want it to add children to the rule node instead, so we
943
+ # manually handle the wiring here and return nil so the caller doesn't
944
+ # duplicate our efforts.
945
+ append_children(rule_node, line.children, false)
946
+ at_root_node << rule_node
947
+ parent << at_root_node
948
+ nil
949
+ end
950
+
951
+ def parse_for_directive(parent, line, root, value, offset)
952
+ var, from_expr, to_name, to_expr =
953
+ value.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
954
+
955
+ if var.nil? # scan failed, try to figure out why for error message
956
+ if value !~ /^[^\s]+/
957
+ expected = "variable name"
958
+ elsif value !~ /^[^\s]+\s+from\s+.+/
959
+ expected = "'from <expr>'"
960
+ else
961
+ expected = "'to <expr>' or 'through <expr>'"
962
+ end
963
+ raise SyntaxError.new("Invalid for directive '@for #{value}': expected #{expected}.")
964
+ end
965
+ raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
966
+
967
+ var = var[1..-1]
968
+ parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
969
+ parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
970
+ Tree::ForNode.new(var, parsed_from, parsed_to, to_name == 'to')
971
+ end
972
+
973
+ def parse_each_directive(parent, line, root, value, offset)
974
+ vars, list_expr = value.scan(/^([^\s]+(?:\s*,\s*[^\s]+)*)\s+in\s+(.+)$/).first
975
+
976
+ if vars.nil? # scan failed, try to figure out why for error message
977
+ if value !~ /^[^\s]+/
978
+ expected = "variable name"
979
+ elsif value !~ /^[^\s]+(?:\s*,\s*[^\s]+)*[^\s]+\s+from\s+.+/
980
+ expected = "'in <expr>'"
981
+ end
982
+ raise SyntaxError.new("Invalid each directive '@each #{value}': expected #{expected}.")
983
+ end
984
+
985
+ vars = vars.split(',').map do |var|
986
+ var.strip!
987
+ raise SyntaxError.new("Invalid variable \"#{var}\".") unless var =~ Script::VALIDATE
988
+ var[1..-1]
989
+ end
990
+
991
+ parsed_list = parse_script(list_expr, :offset => line.offset + line.text.index(list_expr))
992
+ Tree::EachNode.new(vars, parsed_list)
993
+ end
994
+
995
+ def parse_else_directive(parent, line, root, value, offset)
996
+ previous = parent.children.last
997
+ raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
998
+
999
+ if value
1000
+ if value !~ /^if\s+(.+)/
1001
+ raise SyntaxError.new("Invalid else directive '@else #{value}': expected 'if <expr>'.")
1002
+ end
1003
+ expr = parse_script($1, :offset => line.offset + line.text.index($1))
1004
+ end
1005
+
1006
+ node = Tree::IfNode.new(expr)
1007
+ append_children(node, line.children, false)
1008
+ previous.add_else node
1009
+ nil
1010
+ end
1011
+
1012
+ def parse_import_directive(parent, line, root, value, offset)
1013
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.",
1014
+ :line => @line + 1) unless line.children.empty?
1015
+
1016
+ scanner = Sass::Util::MultibyteStringScanner.new(value)
1017
+ values = []
1018
+
1019
+ loop do
1020
+ unless (node = parse_import_arg(scanner, offset + scanner.pos))
1021
+ raise SyntaxError.new(
1022
+ "Invalid @import: expected file to import, was #{scanner.rest.inspect}",
1023
+ :line => @line)
1024
+ end
1025
+ values << node
1026
+ break unless scanner.scan(/,\s*/)
1027
+ end
1028
+
1029
+ if scanner.scan(/;/)
1030
+ raise SyntaxError.new("Invalid @import: expected end of line, was \";\".",
1031
+ :line => @line)
1032
+ end
1033
+
1034
+ values
1035
+ end
1036
+
1037
+ def parse_import_arg(scanner, offset)
1038
+ return if scanner.eos?
1039
+
1040
+ if scanner.match?(/url\(/i)
1041
+ script_parser = Sass::Script::Parser.new(scanner, @line, to_parser_offset(offset), @options)
1042
+ str = script_parser.parse_string
1043
+
1044
+ if scanner.eos?
1045
+ end_pos = str.source_range.end_pos
1046
+ node = Tree::CssImportNode.new(str)
1047
+ else
1048
+ supports_parser = Sass::SCSS::Parser.new(scanner,
1049
+ @options[:filename], @options[:importer],
1050
+ @line, str.source_range.end_pos.offset)
1051
+ supports_condition = supports_parser.parse_supports_clause
1052
+
1053
+ if scanner.eos?
1054
+ node = Tree::CssImportNode.new(str, [], supports_condition)
1055
+ else
1056
+ media_parser = Sass::SCSS::Parser.new(scanner,
1057
+ @options[:filename], @options[:importer],
1058
+ @line, str.source_range.end_pos.offset)
1059
+ media = media_parser.parse_media_query_list
1060
+ end_pos = Sass::Source::Position.new(@line, media_parser.offset + 1)
1061
+ node = Tree::CssImportNode.new(str, media.to_a, supports_condition)
1062
+ end
1063
+ end
1064
+
1065
+ node.source_range = Sass::Source::Range.new(
1066
+ str.source_range.start_pos, end_pos,
1067
+ @options[:filename], @options[:importer])
1068
+ return node
1069
+ end
1070
+
1071
+ unless (quoted_val = scanner.scan(Sass::SCSS::RX::STRING))
1072
+ scanned = scanner.scan(/[^,;]+/)
1073
+ node = Tree::ImportNode.new(scanned)
1074
+ start_parser_offset = to_parser_offset(offset)
1075
+ node.source_range = Sass::Source::Range.new(
1076
+ Sass::Source::Position.new(@line, start_parser_offset),
1077
+ Sass::Source::Position.new(@line, start_parser_offset + scanned.length),
1078
+ @options[:filename], @options[:importer])
1079
+ return node
1080
+ end
1081
+
1082
+ start_offset = offset
1083
+ offset += scanner.matched.length
1084
+ val = Sass::Script::Value::String.value(scanner[1] || scanner[2])
1085
+ scanned = scanner.scan(/\s*/)
1086
+ if !scanner.match?(/[,;]|$/)
1087
+ offset += scanned.length if scanned
1088
+ media_parser = Sass::SCSS::Parser.new(scanner,
1089
+ @options[:filename], @options[:importer], @line, offset)
1090
+ media = media_parser.parse_media_query_list
1091
+ node = Tree::CssImportNode.new(quoted_val, media.to_a)
1092
+ node.source_range = Sass::Source::Range.new(
1093
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1094
+ Sass::Source::Position.new(@line, media_parser.offset),
1095
+ @options[:filename], @options[:importer])
1096
+ elsif val =~ %r{^(https?:)?//}
1097
+ node = Tree::CssImportNode.new(quoted_val)
1098
+ node.source_range = Sass::Source::Range.new(
1099
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1100
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
1101
+ @options[:filename], @options[:importer])
1102
+ else
1103
+ node = Tree::ImportNode.new(val)
1104
+ node.source_range = Sass::Source::Range.new(
1105
+ Sass::Source::Position.new(@line, to_parser_offset(start_offset)),
1106
+ Sass::Source::Position.new(@line, to_parser_offset(offset)),
1107
+ @options[:filename], @options[:importer])
1108
+ end
1109
+ node
1110
+ end
1111
+
1112
+ def parse_mixin_directive(parent, line, root, value, offset)
1113
+ parse_mixin_definition(line)
1114
+ end
1115
+
1116
+ MIXIN_DEF_RE = /^(?:=|@mixin)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
1117
+ def parse_mixin_definition(line)
1118
+ name, arg_string = line.text.scan(MIXIN_DEF_RE).first
1119
+ raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".") if name.nil?
1120
+
1121
+ offset = line.offset + line.text.size - arg_string.size
1122
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
1123
+ parse_mixin_definition_arglist
1124
+ Tree::MixinDefNode.new(name, args, splat)
1125
+ end
1126
+
1127
+ CONTENT_RE = /^@content\s*(.+)?$/
1128
+ def parse_content_directive(parent, line, root, value, offset)
1129
+ trailing = line.text.scan(CONTENT_RE).first.first
1130
+ unless trailing.nil?
1131
+ raise SyntaxError.new(
1132
+ "Invalid content directive. Trailing characters found: \"#{trailing}\".")
1133
+ end
1134
+ raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath @content directives.",
1135
+ :line => line.index + 1) unless line.children.empty?
1136
+ Tree::ContentNode.new
1137
+ end
1138
+
1139
+ def parse_include_directive(parent, line, root, value, offset)
1140
+ parse_mixin_include(line, root)
1141
+ end
1142
+
1143
+ MIXIN_INCLUDE_RE = /^(?:\+|@include)\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
1144
+ def parse_mixin_include(line, root)
1145
+ name, arg_string = line.text.scan(MIXIN_INCLUDE_RE).first
1146
+ raise SyntaxError.new("Invalid mixin include \"#{line.text}\".") if name.nil?
1147
+
1148
+ offset = line.offset + line.text.size - arg_string.size
1149
+ args, keywords, splat, kwarg_splat =
1150
+ Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
1151
+ parse_mixin_include_arglist
1152
+ Tree::MixinNode.new(name, args, keywords, splat, kwarg_splat)
1153
+ end
1154
+
1155
+ FUNCTION_RE = /^@function\s*(#{Sass::SCSS::RX::IDENT})(.*)$/
1156
+ def parse_function_directive(parent, line, root, value, offset)
1157
+ name, arg_string = line.text.scan(FUNCTION_RE).first
1158
+ raise SyntaxError.new("Invalid function definition \"#{line.text}\".") if name.nil?
1159
+
1160
+ offset = line.offset + line.text.size - arg_string.size
1161
+ args, splat = Script::Parser.new(arg_string.strip, @line, to_parser_offset(offset), @options).
1162
+ parse_function_definition_arglist
1163
+ Tree::FunctionNode.new(name, args, splat)
1164
+ end
1165
+
1166
+ def parse_script(script, options = {})
1167
+ line = options[:line] || @line
1168
+ offset = options[:offset] || @offset + 1
1169
+ Script.parse(script, line, offset, @options)
1170
+ end
1171
+
1172
+ def format_comment_text(text, silent)
1173
+ content = text.split("\n")
1174
+
1175
+ if content.first && content.first.strip.empty?
1176
+ removed_first = true
1177
+ content.shift
1178
+ end
1179
+
1180
+ return "/* */" if content.empty?
1181
+ content.last.gsub!(%r{ ?\*/ *$}, '')
1182
+ first = content.shift unless removed_first
1183
+ content.map! {|l| l.gsub!(/^\*( ?)/, '\1') || (l.empty? ? "" : " ") + l}
1184
+ content.unshift first unless removed_first
1185
+ if silent
1186
+ "/*" + content.join("\n *") + " */"
1187
+ else
1188
+ # The #gsub fixes the case of a trailing */
1189
+ "/*" + content.join("\n *").gsub(/ \*\Z/, '') + " */"
1190
+ end
1191
+ end
1192
+
1193
+ def parse_interp(text, offset = 0)
1194
+ self.class.parse_interp(text, @line, offset, :filename => @filename)
1195
+ end
1196
+
1197
+ # Parser tracks 1-based line and offset, so our offset should be converted.
1198
+ def to_parser_offset(offset)
1199
+ offset + 1
1200
+ end
1201
+
1202
+ def full_line_range(line)
1203
+ Sass::Source::Range.new(
1204
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset)),
1205
+ Sass::Source::Position.new(@line, to_parser_offset(line.offset) + line.text.length),
1206
+ @options[:filename], @options[:importer])
1207
+ end
1208
+
1209
+ # It's important that this have strings (at least)
1210
+ # at the beginning, the end, and between each Script::Tree::Node.
1211
+ #
1212
+ # @private
1213
+ def self.parse_interp(text, line, offset, options)
1214
+ res = []
1215
+ rest = Sass::Shared.handle_interpolation text do |scan|
1216
+ escapes = scan[2].size
1217
+ res << scan.matched[0...-2 - escapes]
1218
+ if escapes.odd?
1219
+ res << "\\" * (escapes - 1) << '#{'
1220
+ else
1221
+ res << "\\" * [0, escapes - 1].max
1222
+ if scan[1].include?("\n")
1223
+ line += scan[1].count("\n")
1224
+ offset = scan.matched_size - scan[1].rindex("\n")
1225
+ else
1226
+ offset += scan.matched_size
1227
+ end
1228
+ node = Script::Parser.new(scan, line, offset, options).parse_interpolated
1229
+ offset = node.source_range.end_pos.offset
1230
+ res << node
1231
+ end
1232
+ end
1233
+ res << rest
1234
+ end
1235
+ end
1236
+ end