haml 2.0.10 → 2.2.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of haml might be problematic. Click here for more details.
- data/.yardopts +5 -0
- data/MIT-LICENSE +1 -1
- data/README.md +347 -0
- data/Rakefile +124 -19
- data/VERSION +1 -1
- data/VERSION_NAME +1 -0
- data/extra/haml-mode.el +397 -78
- data/extra/sass-mode.el +148 -36
- data/extra/update_watch.rb +13 -0
- data/lib/haml.rb +15 -993
- data/lib/haml/buffer.rb +131 -84
- data/lib/haml/engine.rb +129 -97
- data/lib/haml/error.rb +7 -7
- data/lib/haml/exec.rb +127 -42
- data/lib/haml/filters.rb +107 -42
- data/lib/haml/helpers.rb +210 -156
- data/lib/haml/helpers/action_view_extensions.rb +34 -39
- data/lib/haml/helpers/action_view_mods.rb +132 -139
- data/lib/haml/html.rb +77 -65
- data/lib/haml/precompiler.rb +404 -213
- data/lib/haml/shared.rb +78 -0
- data/lib/haml/template.rb +14 -14
- data/lib/haml/template/patch.rb +2 -2
- data/lib/haml/template/plugin.rb +2 -3
- data/lib/haml/util.rb +211 -6
- data/lib/haml/version.rb +30 -13
- data/lib/sass.rb +7 -856
- data/lib/sass/css.rb +169 -161
- data/lib/sass/engine.rb +344 -328
- data/lib/sass/environment.rb +79 -0
- data/lib/sass/error.rb +33 -11
- data/lib/sass/files.rb +139 -0
- data/lib/sass/plugin.rb +160 -117
- data/lib/sass/plugin/merb.rb +7 -6
- data/lib/sass/plugin/rails.rb +5 -6
- data/lib/sass/repl.rb +58 -0
- data/lib/sass/script.rb +59 -0
- data/lib/sass/script/bool.rb +17 -0
- data/lib/sass/script/color.rb +183 -0
- data/lib/sass/script/funcall.rb +50 -0
- data/lib/sass/script/functions.rb +198 -0
- data/lib/sass/script/lexer.rb +178 -0
- data/lib/sass/script/literal.rb +177 -0
- data/lib/sass/script/node.rb +14 -0
- data/lib/sass/script/number.rb +381 -0
- data/lib/sass/script/operation.rb +45 -0
- data/lib/sass/script/parser.rb +172 -0
- data/lib/sass/script/string.rb +12 -0
- data/lib/sass/script/unary_operation.rb +34 -0
- data/lib/sass/script/variable.rb +31 -0
- data/lib/sass/tree/comment_node.rb +73 -10
- data/lib/sass/tree/debug_node.rb +30 -0
- data/lib/sass/tree/directive_node.rb +42 -17
- data/lib/sass/tree/file_node.rb +41 -0
- data/lib/sass/tree/for_node.rb +48 -0
- data/lib/sass/tree/if_node.rb +54 -0
- data/lib/sass/tree/mixin_def_node.rb +29 -0
- data/lib/sass/tree/mixin_node.rb +48 -0
- data/lib/sass/tree/node.rb +214 -11
- data/lib/sass/tree/prop_node.rb +109 -0
- data/lib/sass/tree/rule_node.rb +178 -51
- data/lib/sass/tree/variable_node.rb +34 -0
- data/lib/sass/tree/while_node.rb +31 -0
- data/test/haml/engine_test.rb +331 -36
- data/test/haml/helper_test.rb +12 -1
- data/test/haml/results/content_for_layout.xhtml +0 -3
- data/test/haml/results/filters.xhtml +2 -0
- data/test/haml/results/list.xhtml +1 -1
- data/test/haml/template_test.rb +7 -2
- data/test/haml/templates/content_for_layout.haml +0 -2
- data/test/haml/templates/list.haml +1 -1
- data/test/haml/util_test.rb +92 -0
- data/test/sass/css2sass_test.rb +69 -24
- data/test/sass/engine_test.rb +586 -64
- data/test/sass/functions_test.rb +125 -0
- data/test/sass/more_results/more1.css +9 -0
- data/test/sass/more_results/more1_with_line_comments.css +26 -0
- data/test/sass/more_results/more_import.css +29 -0
- data/test/sass/more_templates/_more_partial.sass +2 -0
- data/test/sass/more_templates/more1.sass +23 -0
- data/test/sass/more_templates/more_import.sass +11 -0
- data/test/sass/plugin_test.rb +81 -28
- data/test/sass/results/line_numbers.css +49 -0
- data/test/sass/results/{constants.css → script.css} +4 -4
- data/test/sass/results/subdir/subdir.css +2 -0
- data/test/sass/results/units.css +11 -0
- data/test/sass/script_test.rb +258 -0
- data/test/sass/templates/import.sass +1 -1
- data/test/sass/templates/importee.sass +7 -2
- data/test/sass/templates/line_numbers.sass +13 -0
- data/test/sass/templates/{constants.sass → script.sass} +11 -10
- data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
- data/test/sass/templates/subdir/subdir.sass +2 -2
- data/test/sass/templates/units.sass +11 -0
- data/test/test_helper.rb +14 -0
- metadata +77 -19
- data/FAQ +0 -138
- data/README.rdoc +0 -319
- data/lib/sass/constant.rb +0 -216
- data/lib/sass/constant/color.rb +0 -101
- data/lib/sass/constant/literal.rb +0 -54
- data/lib/sass/constant/nil.rb +0 -9
- data/lib/sass/constant/number.rb +0 -87
- data/lib/sass/constant/operation.rb +0 -30
- data/lib/sass/constant/string.rb +0 -22
- data/lib/sass/tree/attr_node.rb +0 -57
- data/lib/sass/tree/value_node.rb +0 -20
data/lib/sass/engine.rb
CHANGED
@@ -1,27 +1,85 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'digest/sha1'
|
1
3
|
require 'sass/tree/node'
|
2
|
-
require 'sass/tree/value_node'
|
3
4
|
require 'sass/tree/rule_node'
|
4
5
|
require 'sass/tree/comment_node'
|
5
|
-
require 'sass/tree/
|
6
|
+
require 'sass/tree/prop_node'
|
6
7
|
require 'sass/tree/directive_node'
|
7
|
-
require 'sass/
|
8
|
+
require 'sass/tree/variable_node'
|
9
|
+
require 'sass/tree/mixin_def_node'
|
10
|
+
require 'sass/tree/mixin_node'
|
11
|
+
require 'sass/tree/if_node'
|
12
|
+
require 'sass/tree/while_node'
|
13
|
+
require 'sass/tree/for_node'
|
14
|
+
require 'sass/tree/debug_node'
|
15
|
+
require 'sass/tree/file_node'
|
16
|
+
require 'sass/environment'
|
17
|
+
require 'sass/script'
|
8
18
|
require 'sass/error'
|
19
|
+
require 'sass/files'
|
20
|
+
require 'haml/shared'
|
9
21
|
|
10
22
|
module Sass
|
11
|
-
#
|
12
|
-
# template is done. It can be directly used by the user by creating a
|
13
|
-
# new instance and calling <tt>render</tt> to render the template. For example:
|
23
|
+
# A Sass mixin.
|
14
24
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
25
|
+
# `name`: `String`
|
26
|
+
# : The name of the mixin.
|
27
|
+
#
|
28
|
+
# `args`: `Array<(String, Script::Node)>`
|
29
|
+
# : The arguments for the mixin.
|
30
|
+
# Each element is a tuple containing the name of the argument
|
31
|
+
# and the parse tree for the default value of the argument.
|
32
|
+
#
|
33
|
+
# `environment`: {Sass::Environment}
|
34
|
+
# : The environment in which the mixin was defined.
|
35
|
+
# This is captured so that the mixin can have access
|
36
|
+
# to local variables defined in its scope.
|
37
|
+
#
|
38
|
+
# `tree`: {Sass::Tree::Node}
|
39
|
+
# : The parse tree for the mixin.
|
40
|
+
Mixin = Struct.new(:name, :args, :environment, :tree)
|
41
|
+
|
42
|
+
# This class handles the parsing and compilation of the Sass template.
|
43
|
+
# Example usage:
|
44
|
+
#
|
45
|
+
# template = File.load('stylesheets/sassy.sass')
|
46
|
+
# sass_engine = Sass::Engine.new(template)
|
47
|
+
# output = sass_engine.render
|
48
|
+
# puts output
|
19
49
|
class Engine
|
20
|
-
|
21
|
-
|
50
|
+
include Haml::Util
|
51
|
+
|
52
|
+
# A line of Sass code.
|
53
|
+
#
|
54
|
+
# `text`: `String`
|
55
|
+
# : The text in the line, without any whitespace at the beginning or end.
|
56
|
+
#
|
57
|
+
# `tabs`: `Fixnum`
|
58
|
+
# : The level of indentation of the line.
|
59
|
+
#
|
60
|
+
# `index`: `Fixnum`
|
61
|
+
# : The line number in the original document.
|
62
|
+
#
|
63
|
+
# `offset`: `Fixnum`
|
64
|
+
# : The number of bytes in on the line that the text begins.
|
65
|
+
# This ends up being the number of bytes of leading whitespace.
|
66
|
+
#
|
67
|
+
# `filename`: `String`
|
68
|
+
# : The name of the file in which this line appeared.
|
69
|
+
#
|
70
|
+
# `children`: `Array<Line>`
|
71
|
+
# : The lines nested below this one.
|
72
|
+
class Line < Struct.new(:text, :tabs, :index, :offset, :filename, :children)
|
73
|
+
def comment?
|
74
|
+
text[0] == COMMENT_CHAR && (text[1] == SASS_COMMENT_CHAR || text[1] == CSS_COMMENT_CHAR)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
# The character that begins a CSS property.
|
79
|
+
PROPERTY_CHAR = ?:
|
22
80
|
|
23
81
|
# The character that designates that
|
24
|
-
#
|
82
|
+
# a property should be assigned to a SassScript expression.
|
25
83
|
SCRIPT_CHAR = ?=
|
26
84
|
|
27
85
|
# The character that designates the beginning of a comment,
|
@@ -48,433 +106,391 @@ module Sass
|
|
48
106
|
# Includes named mixin declared using MIXIN_DEFINITION_CHAR
|
49
107
|
MIXIN_INCLUDE_CHAR = ?+
|
50
108
|
|
51
|
-
# The regex that matches
|
52
|
-
|
53
|
-
ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
|
54
|
-
|
55
|
-
# The regex that matches attributes of the form <tt>name: attr</tt>.
|
56
|
-
ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
|
109
|
+
# The regex that matches properties of the form <tt>name: prop</tt>.
|
110
|
+
PROPERTY_NEW_MATCHER = /^[^\s:"]+\s*[=:](\s|$)/
|
57
111
|
|
58
112
|
# The regex that matches and extracts data from
|
59
|
-
#
|
60
|
-
|
113
|
+
# properties of the form <tt>name: prop</tt>.
|
114
|
+
PROPERTY_NEW = /^([^\s=:"]+)(\s*=|:)(?:\s+|$)(.*)/
|
61
115
|
|
62
|
-
#
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
116
|
+
# The regex that matches and extracts data from
|
117
|
+
# properties of the form <tt>:name prop</tt>.
|
118
|
+
PROPERTY_OLD = /^:([^\s=:"]+)\s*(=?)(?:\s+|$)(.*)/
|
119
|
+
|
120
|
+
# The default options for Sass::Engine.
|
121
|
+
DEFAULT_OPTIONS = {
|
122
|
+
:style => :nested,
|
123
|
+
:load_paths => ['.'],
|
124
|
+
:cache => true,
|
125
|
+
:cache_location => './.sass-cache',
|
126
|
+
}.freeze
|
127
|
+
|
128
|
+
# @param template [String] The Sass template.
|
129
|
+
# @param options [Hash<Symbol, Object>] An options hash;
|
130
|
+
# see {file:SASS_REFERENCE.md#sass_options the Sass options documentation}
|
74
131
|
def initialize(template, options={})
|
75
|
-
@options =
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
@
|
80
|
-
@
|
81
|
-
@
|
82
|
-
@
|
132
|
+
@options = DEFAULT_OPTIONS.merge(options)
|
133
|
+
@template = template
|
134
|
+
|
135
|
+
# Backwards compatibility
|
136
|
+
@options[:property_syntax] ||= @options[:attribute_syntax]
|
137
|
+
case @options[:property_syntax]
|
138
|
+
when :alternate; @options[:property_syntax] = :new
|
139
|
+
when :normal; @options[:property_syntax] = :old
|
140
|
+
end
|
83
141
|
end
|
84
142
|
|
85
|
-
#
|
143
|
+
# Render the template to CSS.
|
144
|
+
#
|
145
|
+
# @return [String] The CSS
|
146
|
+
# @raise [Sass::SyntaxError] if there's an error in the document
|
86
147
|
def render
|
87
|
-
|
88
|
-
render_to_tree.to_s
|
89
|
-
rescue SyntaxError => err
|
90
|
-
unless err.sass_filename
|
91
|
-
err.add_backtrace_entry(@options[:filename])
|
92
|
-
end
|
93
|
-
raise err
|
94
|
-
end
|
148
|
+
to_tree.render
|
95
149
|
end
|
96
150
|
|
97
151
|
alias_method :to_css, :render
|
98
152
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
@
|
107
|
-
end
|
108
|
-
|
109
|
-
def render_to_tree
|
110
|
-
split_lines
|
111
|
-
|
112
|
-
root = Tree::Node.new(@options[:style])
|
113
|
-
index = 0
|
114
|
-
while @lines[index]
|
115
|
-
old_index = index
|
116
|
-
child, index = build_tree(index, true)
|
117
|
-
|
118
|
-
if child.is_a? Tree::Node
|
119
|
-
child.line = old_index + 1
|
120
|
-
root << child
|
121
|
-
elsif child.is_a? Array
|
122
|
-
child.each do |c|
|
123
|
-
root << c
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|
127
|
-
@lines.clear
|
128
|
-
|
153
|
+
# Parses the document into its parse tree.
|
154
|
+
#
|
155
|
+
# @return [Sass::Tree::Node] The root of the parse tree.
|
156
|
+
# @raise [Sass::SyntaxError] if there's an error in the document
|
157
|
+
def to_tree
|
158
|
+
root = Tree::Node.new
|
159
|
+
append_children(root, tree(tabulate(@template)).first, true)
|
160
|
+
root.options = @options
|
129
161
|
root
|
162
|
+
rescue SyntaxError => e; e.add_metadata(@options[:filename], @line)
|
130
163
|
end
|
131
164
|
|
132
165
|
private
|
133
166
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
tabs = count_tabs(line)
|
144
|
-
|
145
|
-
if line[0] == COMMENT_CHAR && line[1] == SASS_COMMENT_CHAR && tabs == 0
|
146
|
-
tabs = old_tabs
|
147
|
-
last_line_was_comment = true
|
148
|
-
elsif last_line_was_comment && tabs
|
149
|
-
if tabs > 0
|
150
|
-
context = "On line #{@line}"
|
151
|
-
context << " of #{@options[:filename]}" if @options[:filename]
|
152
|
-
warn <<ENDENDEND
|
153
|
-
DEPRECATION WARNING:
|
154
|
-
#{context}
|
155
|
-
Silent comments (//) in version 2.2 will comment out the lines indented beneath them.
|
156
|
-
Please indent line #{@line - 1} to match the surrounding indentation level
|
157
|
-
ENDENDEND
|
158
|
-
end
|
159
|
-
last_line_was_comment = false
|
167
|
+
def tabulate(string)
|
168
|
+
tab_str = nil
|
169
|
+
first = true
|
170
|
+
lines = []
|
171
|
+
string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/).each_with_index do |line, index|
|
172
|
+
index += (@options[:line] || 1)
|
173
|
+
if line.strip.empty?
|
174
|
+
lines.last.text << "\n" if lines.last && lines.last.comment?
|
175
|
+
next
|
160
176
|
end
|
161
177
|
|
162
|
-
|
163
|
-
|
178
|
+
line_tab_str = line[/^\s*/]
|
179
|
+
unless line_tab_str.empty?
|
180
|
+
tab_str ||= line_tab_str
|
164
181
|
|
165
|
-
|
166
|
-
|
182
|
+
raise SyntaxError.new("Indenting at the beginning of the document is illegal.", index) if first
|
183
|
+
if tab_str.include?(?\s) && tab_str.include?(?\t)
|
184
|
+
raise SyntaxError.new("Indentation can't use both tabs and spaces.", index)
|
167
185
|
end
|
168
|
-
@lines << [line.strip, tabs]
|
169
|
-
|
170
|
-
old_tabs = tabs
|
171
|
-
else
|
172
|
-
@lines << ['//', old_tabs || 0]
|
173
186
|
end
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
187
|
+
first &&= !tab_str.nil?
|
188
|
+
if tab_str.nil?
|
189
|
+
lines << Line.new(line.strip, 0, index, 0, @options[:filename], [])
|
190
|
+
next
|
191
|
+
end
|
178
192
|
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
193
|
+
if lines.last && lines.last.comment? && line =~ /^(?:#{tab_str}){#{lines.last.tabs + 1}}(.*)$/
|
194
|
+
lines.last.text << "\n" << $1
|
195
|
+
next
|
196
|
+
end
|
183
197
|
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
elsif line[spaces] == ?\t
|
189
|
-
raise SyntaxError.new(<<END.strip, @line)
|
190
|
-
A tab character was used for indentation. Sass must be indented using two spaces.
|
191
|
-
Are you sure you have soft tabs enabled in your editor?
|
198
|
+
line_tabs = line_tab_str.scan(tab_str).size
|
199
|
+
raise SyntaxError.new(<<END.strip.gsub("\n", ' '), index) if tab_str * line_tabs != line_tab_str
|
200
|
+
Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,
|
201
|
+
but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}.
|
192
202
|
END
|
203
|
+
lines << Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
|
193
204
|
end
|
194
|
-
|
205
|
+
lines
|
195
206
|
end
|
196
207
|
|
197
|
-
def
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
# Node is a symbol if it's non-outputting, like a constant assignment
|
207
|
-
unless node.is_a? Tree::Node
|
208
|
-
if has_children
|
209
|
-
if node == :constant
|
210
|
-
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath constants.", @line + 1)
|
211
|
-
elsif node.is_a? Array
|
212
|
-
# arrays can either be full of import statements
|
213
|
-
# or attributes from mixin includes
|
214
|
-
# in either case they shouldn't have children.
|
215
|
-
# Need to peek into the array in order to give meaningful errors
|
216
|
-
directive_type = (node.first.is_a?(Tree::DirectiveNode) ? "import" : "mixin")
|
217
|
-
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath #{directive_type} directives.", @line + 1)
|
208
|
+
def tree(arr, i = 0)
|
209
|
+
return [], i if arr[i].nil?
|
210
|
+
|
211
|
+
base = arr[i].tabs
|
212
|
+
nodes = []
|
213
|
+
while (line = arr[i]) && line.tabs >= base
|
214
|
+
if line.tabs > base
|
215
|
+
if line.tabs > base + 1
|
216
|
+
raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.", line.index)
|
218
217
|
end
|
219
|
-
end
|
220
218
|
|
221
|
-
|
222
|
-
|
219
|
+
nodes.last.children, i = tree(arr, i)
|
220
|
+
else
|
221
|
+
nodes << line
|
222
|
+
i += 1
|
223
|
+
end
|
223
224
|
end
|
225
|
+
return nodes, i
|
226
|
+
end
|
224
227
|
|
225
|
-
|
228
|
+
def build_tree(parent, line, root = false)
|
229
|
+
@line = line.index
|
230
|
+
node = parse_line(parent, line, root)
|
226
231
|
|
227
|
-
if
|
228
|
-
|
229
|
-
|
230
|
-
node << line
|
232
|
+
# Node is a symbol if it's non-outputting, like a variable assignment,
|
233
|
+
# or an array if it's a group of nodes to add
|
234
|
+
return node unless node.is_a? Tree::Node
|
231
235
|
|
232
|
-
|
233
|
-
|
236
|
+
node.line = line.index
|
237
|
+
node.filename = line.filename
|
234
238
|
|
235
|
-
|
239
|
+
if node.is_a?(Tree::CommentNode)
|
240
|
+
node.lines = line.children
|
241
|
+
else
|
242
|
+
append_children(node, line.children, false)
|
236
243
|
end
|
244
|
+
return node
|
245
|
+
end
|
237
246
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
247
|
+
def append_children(parent, children, root)
|
248
|
+
continued_rule = nil
|
249
|
+
children.each do |line|
|
250
|
+
child = build_tree(parent, line, root)
|
251
|
+
|
252
|
+
if child.is_a?(Tree::RuleNode) && child.continued?
|
253
|
+
raise SyntaxError.new("Rules can't end in commas.", child.line) unless child.children.empty?
|
254
|
+
if continued_rule
|
255
|
+
continued_rule.add_rules child
|
256
|
+
else
|
257
|
+
continued_rule = child
|
244
258
|
end
|
259
|
+
next
|
260
|
+
end
|
245
261
|
|
246
|
-
|
262
|
+
if continued_rule
|
263
|
+
raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) unless child.is_a?(Tree::RuleNode)
|
264
|
+
continued_rule.add_rules child
|
265
|
+
continued_rule.children = child.children
|
266
|
+
continued_rule, child = nil, continued_rule
|
247
267
|
end
|
248
|
-
|
268
|
+
|
269
|
+
validate_and_append_child(parent, child, line, root)
|
249
270
|
end
|
250
271
|
|
251
|
-
|
252
|
-
child, index = build_tree(index)
|
272
|
+
raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) if continued_rule
|
253
273
|
|
254
|
-
|
274
|
+
parent
|
275
|
+
end
|
255
276
|
|
256
|
-
|
277
|
+
def validate_and_append_child(parent, child, line, root)
|
278
|
+
unless root
|
279
|
+
case child
|
280
|
+
when Tree::MixinDefNode
|
281
|
+
raise SyntaxError.new("Mixins may only be defined at the root of a document.", line.index)
|
282
|
+
when Tree::DirectiveNode, Tree::FileNode
|
283
|
+
raise SyntaxError.new("Import directives may only be used at the root of a document.", line.index)
|
284
|
+
end
|
257
285
|
end
|
258
286
|
|
259
|
-
return node, index
|
260
|
-
end
|
261
|
-
|
262
|
-
def validate_and_append_child(parent, child)
|
263
287
|
case child
|
264
|
-
when :constant
|
265
|
-
raise SyntaxError.new("Constants may only be declared at the root of a document.", @line)
|
266
|
-
when :mixin
|
267
|
-
raise SyntaxError.new("Mixins may only be defined at the root of a document.", @line)
|
268
288
|
when Array
|
269
|
-
child.each {|c| parent
|
289
|
+
child.each {|c| validate_and_append_child(parent, c, line, root)}
|
270
290
|
when Tree::Node
|
271
291
|
parent << child
|
272
292
|
end
|
273
293
|
end
|
274
294
|
|
275
|
-
def
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
end
|
281
|
-
next_line && next_line[1] > tabs
|
282
|
-
end
|
283
|
-
|
284
|
-
def raw_next_line(index)
|
285
|
-
[@lines[index][0], index + 1]
|
286
|
-
end
|
287
|
-
|
288
|
-
def parse_line(line)
|
289
|
-
case line[0]
|
290
|
-
when ATTRIBUTE_CHAR
|
291
|
-
if line[1] != ATTRIBUTE_CHAR
|
292
|
-
parse_attribute(line, ATTRIBUTE)
|
295
|
+
def parse_line(parent, line, root)
|
296
|
+
case line.text[0]
|
297
|
+
when PROPERTY_CHAR
|
298
|
+
if line.text[1] != PROPERTY_CHAR
|
299
|
+
parse_property(line, PROPERTY_OLD)
|
293
300
|
else
|
294
301
|
# Support CSS3-style pseudo-elements,
|
295
302
|
# which begin with ::
|
296
|
-
Tree::RuleNode.new(line
|
303
|
+
Tree::RuleNode.new(line.text)
|
297
304
|
end
|
298
|
-
when
|
299
|
-
|
305
|
+
when Script::VARIABLE_CHAR
|
306
|
+
parse_variable(line)
|
300
307
|
when COMMENT_CHAR
|
301
|
-
parse_comment(line)
|
308
|
+
parse_comment(line.text)
|
302
309
|
when DIRECTIVE_CHAR
|
303
|
-
parse_directive(line)
|
310
|
+
parse_directive(parent, line, root)
|
304
311
|
when ESCAPE_CHAR
|
305
|
-
Tree::RuleNode.new(line[1..-1]
|
312
|
+
Tree::RuleNode.new(line.text[1..-1])
|
306
313
|
when MIXIN_DEFINITION_CHAR
|
307
314
|
parse_mixin_definition(line)
|
308
315
|
when MIXIN_INCLUDE_CHAR
|
309
|
-
if line[1].nil? || line[1] == ?\s
|
310
|
-
Tree::RuleNode.new(line
|
316
|
+
if line.text[1].nil? || line.text[1] == ?\s
|
317
|
+
Tree::RuleNode.new(line.text)
|
311
318
|
else
|
312
|
-
parse_mixin_include(line)
|
319
|
+
parse_mixin_include(line, root)
|
313
320
|
end
|
314
321
|
else
|
315
|
-
if line =~
|
316
|
-
|
322
|
+
if line.text =~ PROPERTY_NEW_MATCHER
|
323
|
+
parse_property(line, PROPERTY_NEW)
|
317
324
|
else
|
318
|
-
Tree::RuleNode.new(line
|
325
|
+
Tree::RuleNode.new(line.text)
|
319
326
|
end
|
320
327
|
end
|
321
328
|
end
|
322
329
|
|
323
|
-
def
|
324
|
-
|
325
|
-
attribute_regx == ATTRIBUTE_ALTERNATE
|
326
|
-
raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
|
327
|
-
elsif @options[:attribute_syntax] == :alternate &&
|
328
|
-
attribute_regx == ATTRIBUTE
|
329
|
-
raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
|
330
|
-
end
|
331
|
-
|
332
|
-
name, eq, value = line.scan(attribute_regx)[0]
|
330
|
+
def parse_property(line, property_regx)
|
331
|
+
name, eq, value = line.text.scan(property_regx)[0]
|
333
332
|
|
334
333
|
if name.nil? || value.nil?
|
335
|
-
raise SyntaxError.new("Invalid
|
334
|
+
raise SyntaxError.new("Invalid property: \"#{line.text}\".", @line)
|
336
335
|
end
|
337
|
-
|
338
|
-
|
339
|
-
|
336
|
+
expr = if (eq.strip[0] == SCRIPT_CHAR)
|
337
|
+
parse_script(value, :offset => line.offset + line.text.index(value))
|
338
|
+
else
|
339
|
+
value
|
340
340
|
end
|
341
|
-
|
342
|
-
Tree::AttrNode.new(name, value, @options[:style])
|
341
|
+
Tree::PropNode.new(name, expr, property_regx == PROPERTY_OLD ? :old : :new)
|
343
342
|
end
|
344
343
|
|
345
|
-
def
|
346
|
-
name, op, value = line.scan(
|
347
|
-
|
348
|
-
|
349
|
-
end
|
350
|
-
|
351
|
-
constant = Sass::Constant.parse(value, @constants, @line)
|
352
|
-
if op == '||='
|
353
|
-
@constants[name] ||= constant
|
354
|
-
else
|
355
|
-
@constants[name] = constant
|
356
|
-
end
|
344
|
+
def parse_variable(line)
|
345
|
+
name, op, value = line.text.scan(Script::MATCH)[0]
|
346
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", @line + 1) unless line.children.empty?
|
347
|
+
raise SyntaxError.new("Invalid variable: \"#{line.text}\".", @line) unless name && value
|
357
348
|
|
358
|
-
:
|
349
|
+
Tree::VariableNode.new(name, parse_script(value, :offset => line.offset + line.text.index(value)), op == '||=')
|
359
350
|
end
|
360
351
|
|
361
352
|
def parse_comment(line)
|
362
|
-
if line[1] == SASS_COMMENT_CHAR
|
363
|
-
|
364
|
-
elsif line[1] == CSS_COMMENT_CHAR
|
365
|
-
Tree::CommentNode.new(line, @options[:style])
|
353
|
+
if line[1] == CSS_COMMENT_CHAR || line[1] == SASS_COMMENT_CHAR
|
354
|
+
Tree::CommentNode.new(line, line[1] == SASS_COMMENT_CHAR)
|
366
355
|
else
|
367
|
-
Tree::RuleNode.new(line
|
356
|
+
Tree::RuleNode.new(line)
|
368
357
|
end
|
369
358
|
end
|
370
359
|
|
371
|
-
def parse_directive(line)
|
372
|
-
directive, value = line[1..-1].split(
|
360
|
+
def parse_directive(parent, line, root)
|
361
|
+
directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
|
362
|
+
offset = directive.size + whitespace.size + 1 if whitespace
|
373
363
|
|
374
364
|
# If value begins with url( or ",
|
375
365
|
# it's a CSS @import rule and we don't want to touch it.
|
376
366
|
if directive == "import" && value !~ /^(url\(|")/
|
367
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line + 1) unless line.children.empty?
|
377
368
|
import(value)
|
369
|
+
elsif directive == "for"
|
370
|
+
parse_for(line, root, value)
|
371
|
+
elsif directive == "else"
|
372
|
+
parse_else(parent, line, value)
|
373
|
+
elsif directive == "while"
|
374
|
+
raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
|
375
|
+
Tree::WhileNode.new(parse_script(value, :offset => offset))
|
376
|
+
elsif directive == "if"
|
377
|
+
raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
|
378
|
+
Tree::IfNode.new(parse_script(value, :offset => offset))
|
379
|
+
elsif directive == "debug"
|
380
|
+
raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
|
381
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.", @line + 1) unless line.children.empty?
|
382
|
+
offset = line.offset + line.text.index(value).to_i
|
383
|
+
Tree::DebugNode.new(parse_script(value, :offset => offset))
|
378
384
|
else
|
379
|
-
Tree::DirectiveNode.new(line
|
380
|
-
end
|
381
|
-
end
|
382
|
-
|
383
|
-
def parse_mixin_definition(line)
|
384
|
-
mixin_name = line[1..-1].strip
|
385
|
-
@mixins[mixin_name] = []
|
386
|
-
index = @line
|
387
|
-
line, tabs = @lines[index]
|
388
|
-
while !line.nil? && tabs > 0
|
389
|
-
child, index = build_tree(index)
|
390
|
-
validate_and_append_child(@mixins[mixin_name], child)
|
391
|
-
line, tabs = @lines[index]
|
385
|
+
Tree::DirectiveNode.new(line.text)
|
392
386
|
end
|
393
|
-
:mixin
|
394
387
|
end
|
395
388
|
|
396
|
-
def
|
397
|
-
|
398
|
-
unless @mixins.has_key?(mixin_name)
|
399
|
-
raise SyntaxError.new("Undefined mixin '#{mixin_name}'.", @line)
|
400
|
-
end
|
401
|
-
@mixins[mixin_name]
|
402
|
-
end
|
389
|
+
def parse_for(line, root, text)
|
390
|
+
var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
|
403
391
|
|
404
|
-
|
405
|
-
|
406
|
-
|
392
|
+
if var.nil? # scan failed, try to figure out why for error message
|
393
|
+
if text !~ /^[^\s]+/
|
394
|
+
expected = "variable name"
|
395
|
+
elsif text !~ /^[^\s]+\s+from\s+.+/
|
396
|
+
expected = "'from <expr>'"
|
397
|
+
else
|
398
|
+
expected = "'to <expr>' or 'through <expr>'"
|
399
|
+
end
|
400
|
+
raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.", @line)
|
407
401
|
end
|
402
|
+
raise SyntaxError.new("Invalid variable \"#{var}\".", @line) unless var =~ Script::VALIDATE
|
408
403
|
|
409
|
-
|
404
|
+
parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
|
405
|
+
parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
|
406
|
+
Tree::ForNode.new(var[1..-1], parsed_from, parsed_to, to_name == 'to')
|
407
|
+
end
|
410
408
|
|
411
|
-
|
412
|
-
|
409
|
+
def parse_else(parent, line, text)
|
410
|
+
previous = parent.last
|
411
|
+
raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
|
413
412
|
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
raise SyntaxError.new(e.message, @line)
|
413
|
+
if text
|
414
|
+
if text !~ /^if\s+(.+)/
|
415
|
+
raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.", @line)
|
418
416
|
end
|
417
|
+
expr = parse_script($1, :offset => line.offset + line.text.index($1))
|
418
|
+
end
|
419
419
|
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
new_options[:filename] = filename
|
426
|
-
engine = Sass::Engine.new(file.read, @options)
|
427
|
-
end
|
420
|
+
node = Tree::IfNode.new(expr)
|
421
|
+
append_children(node, line.children, false)
|
422
|
+
previous.add_else node
|
423
|
+
nil
|
424
|
+
end
|
428
425
|
|
429
|
-
|
430
|
-
|
426
|
+
# parses out the arguments between the commas and cleans up the mixin arguments
|
427
|
+
# returns nil if it fails to parse, otherwise an array.
|
428
|
+
def parse_mixin_arguments(arg_string)
|
429
|
+
arg_string = arg_string.strip
|
430
|
+
return [] if arg_string.empty?
|
431
|
+
return nil unless (arg_string[0] == ?( && arg_string[-1] == ?))
|
432
|
+
arg_string = arg_string[1...-1]
|
433
|
+
arg_string.split(",", -1).map {|a| a.strip}
|
434
|
+
end
|
431
435
|
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
@constants = engine.constants
|
443
|
-
@mixins = engine.mixins
|
436
|
+
def parse_mixin_definition(line)
|
437
|
+
name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
|
438
|
+
args = parse_mixin_arguments(arg_string)
|
439
|
+
raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil? || args.nil?
|
440
|
+
default_arg_found = false
|
441
|
+
required_arg_count = 0
|
442
|
+
args.map! do |arg|
|
443
|
+
raise SyntaxError.new("Mixin arguments can't be empty.", @line) if arg.empty? || arg == "!"
|
444
|
+
unless arg[0] == Script::VARIABLE_CHAR
|
445
|
+
raise SyntaxError.new("Mixin argument \"#{arg}\" must begin with an exclamation point (!).", @line)
|
444
446
|
end
|
447
|
+
arg, default = arg.split(/\s*=\s*/, 2)
|
448
|
+
required_arg_count += 1 unless default
|
449
|
+
default_arg_found ||= default
|
450
|
+
raise SyntaxError.new("Invalid variable \"#{arg}\".", @line) unless arg =~ Script::VALIDATE
|
451
|
+
raise SyntaxError.new("Required arguments must not follow optional arguments \"#{arg}\".", @line) if default_arg_found && !default
|
452
|
+
default = parse_script(default, :offset => line.offset + line.text.index(default)) if default
|
453
|
+
[arg[1..-1], default]
|
445
454
|
end
|
446
|
-
|
447
|
-
nodes
|
455
|
+
Tree::MixinDefNode.new(name, args)
|
448
456
|
end
|
449
457
|
|
450
|
-
def
|
451
|
-
|
452
|
-
|
458
|
+
def parse_mixin_include(line, root)
|
459
|
+
name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
|
460
|
+
args = parse_mixin_arguments(arg_string)
|
461
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.", @line + 1) unless line.children.empty?
|
462
|
+
raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil? || args.nil?
|
463
|
+
args.each {|a| raise SyntaxError.new("Mixin arguments can't be empty.", @line) if a.empty?}
|
453
464
|
|
454
|
-
|
455
|
-
|
456
|
-
was_sass = true
|
457
|
-
elsif filename[-4..-1] == ".css"
|
458
|
-
return filename
|
459
|
-
end
|
465
|
+
Tree::MixinNode.new(name, args.map {|s| parse_script(s, :offset => line.offset + line.text.index(s))})
|
466
|
+
end
|
460
467
|
|
461
|
-
|
468
|
+
def parse_script(script, options = {})
|
469
|
+
line = options[:line] || @line
|
470
|
+
offset = options[:offset] || 0
|
471
|
+
Script.parse(script, line, offset, @options[:filename])
|
472
|
+
end
|
462
473
|
|
463
|
-
|
464
|
-
|
465
|
-
|
474
|
+
def import_paths
|
475
|
+
paths = (@options[:load_paths] || []).dup
|
476
|
+
paths.unshift(File.dirname(@options[:filename])) if @options[:filename]
|
477
|
+
paths
|
466
478
|
end
|
467
479
|
|
468
|
-
def
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
480
|
+
def import(files)
|
481
|
+
files.split(/,\s*/).map do |filename|
|
482
|
+
engine = nil
|
483
|
+
|
484
|
+
begin
|
485
|
+
filename = Sass::Files.find_file_to_import(filename, import_paths)
|
486
|
+
rescue Exception => e
|
487
|
+
raise SyntaxError.new(e.message, @line)
|
475
488
|
end
|
476
|
-
|
477
|
-
|
489
|
+
|
490
|
+
next Tree::DirectiveNode.new("@import url(#{filename})") if filename =~ /\.css$/
|
491
|
+
|
492
|
+
Tree::FileNode.new(filename)
|
493
|
+
end.flatten
|
478
494
|
end
|
479
495
|
end
|
480
496
|
end
|