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