merbjedi-haml 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/FAQ +138 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +332 -0
- data/REVISION +1 -0
- data/Rakefile +184 -0
- data/VERSION +1 -0
- data/bin/css2sass +7 -0
- data/bin/haml +9 -0
- data/bin/html2haml +7 -0
- data/bin/sass +8 -0
- data/extra/haml-mode.el +434 -0
- data/extra/sass-mode.el +98 -0
- data/init.rb +8 -0
- data/lib/haml.rb +1025 -0
- data/lib/haml/buffer.rb +255 -0
- data/lib/haml/engine.rb +268 -0
- data/lib/haml/error.rb +22 -0
- data/lib/haml/exec.rb +395 -0
- data/lib/haml/filters.rb +276 -0
- data/lib/haml/helpers.rb +465 -0
- data/lib/haml/helpers/action_view_extensions.rb +45 -0
- data/lib/haml/helpers/action_view_mods.rb +181 -0
- data/lib/haml/html.rb +218 -0
- data/lib/haml/precompiler.rb +896 -0
- data/lib/haml/shared.rb +45 -0
- data/lib/haml/template.rb +51 -0
- data/lib/haml/template/patch.rb +58 -0
- data/lib/haml/template/plugin.rb +72 -0
- data/lib/haml/util.rb +77 -0
- data/lib/haml/version.rb +47 -0
- data/lib/sass.rb +1062 -0
- data/lib/sass/css.rb +388 -0
- data/lib/sass/engine.rb +501 -0
- data/lib/sass/environment.rb +33 -0
- data/lib/sass/error.rb +35 -0
- data/lib/sass/plugin.rb +203 -0
- data/lib/sass/plugin/merb.rb +56 -0
- data/lib/sass/plugin/rails.rb +24 -0
- data/lib/sass/repl.rb +44 -0
- data/lib/sass/script.rb +38 -0
- data/lib/sass/script/bool.rb +13 -0
- data/lib/sass/script/color.rb +97 -0
- data/lib/sass/script/funcall.rb +28 -0
- data/lib/sass/script/functions.rb +122 -0
- data/lib/sass/script/lexer.rb +144 -0
- data/lib/sass/script/literal.rb +60 -0
- data/lib/sass/script/number.rb +231 -0
- data/lib/sass/script/operation.rb +30 -0
- data/lib/sass/script/parser.rb +142 -0
- data/lib/sass/script/string.rb +42 -0
- data/lib/sass/script/unary_operation.rb +21 -0
- data/lib/sass/script/variable.rb +20 -0
- data/lib/sass/tree/attr_node.rb +64 -0
- data/lib/sass/tree/comment_node.rb +30 -0
- data/lib/sass/tree/debug_node.rb +22 -0
- data/lib/sass/tree/directive_node.rb +50 -0
- data/lib/sass/tree/file_node.rb +27 -0
- data/lib/sass/tree/for_node.rb +29 -0
- data/lib/sass/tree/if_node.rb +27 -0
- data/lib/sass/tree/mixin_def_node.rb +18 -0
- data/lib/sass/tree/mixin_node.rb +34 -0
- data/lib/sass/tree/node.rb +97 -0
- data/lib/sass/tree/rule_node.rb +120 -0
- data/lib/sass/tree/variable_node.rb +24 -0
- data/lib/sass/tree/while_node.rb +20 -0
- data/rails/init.rb +1 -0
- data/test/benchmark.rb +99 -0
- data/test/haml/engine_test.rb +852 -0
- data/test/haml/helper_test.rb +224 -0
- data/test/haml/html2haml_test.rb +92 -0
- data/test/haml/markaby/standard.mab +52 -0
- data/test/haml/mocks/article.rb +6 -0
- data/test/haml/results/content_for_layout.xhtml +15 -0
- data/test/haml/results/eval_suppressed.xhtml +9 -0
- data/test/haml/results/filters.xhtml +62 -0
- data/test/haml/results/helpers.xhtml +93 -0
- data/test/haml/results/helpful.xhtml +10 -0
- data/test/haml/results/just_stuff.xhtml +68 -0
- data/test/haml/results/list.xhtml +12 -0
- data/test/haml/results/nuke_inner_whitespace.xhtml +40 -0
- data/test/haml/results/nuke_outer_whitespace.xhtml +148 -0
- data/test/haml/results/original_engine.xhtml +20 -0
- data/test/haml/results/partial_layout.xhtml +5 -0
- data/test/haml/results/partials.xhtml +21 -0
- data/test/haml/results/render_layout.xhtml +3 -0
- data/test/haml/results/silent_script.xhtml +74 -0
- data/test/haml/results/standard.xhtml +42 -0
- data/test/haml/results/tag_parsing.xhtml +23 -0
- data/test/haml/results/very_basic.xhtml +5 -0
- data/test/haml/results/whitespace_handling.xhtml +89 -0
- data/test/haml/rhtml/_av_partial_1.rhtml +12 -0
- data/test/haml/rhtml/_av_partial_2.rhtml +8 -0
- data/test/haml/rhtml/action_view.rhtml +62 -0
- data/test/haml/rhtml/standard.rhtml +54 -0
- data/test/haml/template_test.rb +204 -0
- data/test/haml/templates/_av_partial_1.haml +9 -0
- data/test/haml/templates/_av_partial_1_ugly.haml +9 -0
- data/test/haml/templates/_av_partial_2.haml +5 -0
- data/test/haml/templates/_av_partial_2_ugly.haml +5 -0
- data/test/haml/templates/_layout.erb +3 -0
- data/test/haml/templates/_layout_for_partial.haml +3 -0
- data/test/haml/templates/_partial.haml +8 -0
- data/test/haml/templates/_text_area.haml +3 -0
- data/test/haml/templates/action_view.haml +47 -0
- data/test/haml/templates/action_view_ugly.haml +47 -0
- data/test/haml/templates/breakage.haml +8 -0
- data/test/haml/templates/content_for_layout.haml +10 -0
- data/test/haml/templates/eval_suppressed.haml +11 -0
- data/test/haml/templates/filters.haml +66 -0
- data/test/haml/templates/helpers.haml +95 -0
- data/test/haml/templates/helpful.haml +11 -0
- data/test/haml/templates/just_stuff.haml +83 -0
- data/test/haml/templates/list.haml +12 -0
- data/test/haml/templates/nuke_inner_whitespace.haml +32 -0
- data/test/haml/templates/nuke_outer_whitespace.haml +144 -0
- data/test/haml/templates/original_engine.haml +17 -0
- data/test/haml/templates/partial_layout.haml +3 -0
- data/test/haml/templates/partialize.haml +1 -0
- data/test/haml/templates/partials.haml +12 -0
- data/test/haml/templates/render_layout.haml +2 -0
- data/test/haml/templates/silent_script.haml +40 -0
- data/test/haml/templates/standard.haml +42 -0
- data/test/haml/templates/standard_ugly.haml +42 -0
- data/test/haml/templates/tag_parsing.haml +21 -0
- data/test/haml/templates/very_basic.haml +4 -0
- data/test/haml/templates/whitespace_handling.haml +87 -0
- data/test/linked_rails.rb +12 -0
- data/test/sass/css2sass_test.rb +193 -0
- data/test/sass/engine_test.rb +752 -0
- data/test/sass/functions_test.rb +96 -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 +208 -0
- data/test/sass/results/alt.css +4 -0
- data/test/sass/results/basic.css +9 -0
- data/test/sass/results/compact.css +5 -0
- data/test/sass/results/complex.css +87 -0
- data/test/sass/results/compressed.css +1 -0
- data/test/sass/results/expanded.css +19 -0
- data/test/sass/results/import.css +29 -0
- data/test/sass/results/line_numbers.css +49 -0
- data/test/sass/results/mixins.css +95 -0
- data/test/sass/results/multiline.css +24 -0
- data/test/sass/results/nested.css +22 -0
- data/test/sass/results/parent_ref.css +13 -0
- data/test/sass/results/script.css +16 -0
- data/test/sass/results/subdir/nested_subdir/nested_subdir.css +1 -0
- data/test/sass/results/subdir/subdir.css +3 -0
- data/test/sass/results/units.css +11 -0
- data/test/sass/script_test.rb +152 -0
- data/test/sass/templates/_partial.sass +2 -0
- data/test/sass/templates/alt.sass +16 -0
- data/test/sass/templates/basic.sass +23 -0
- data/test/sass/templates/bork.sass +2 -0
- data/test/sass/templates/bork2.sass +2 -0
- data/test/sass/templates/compact.sass +17 -0
- data/test/sass/templates/complex.sass +309 -0
- data/test/sass/templates/compressed.sass +15 -0
- data/test/sass/templates/expanded.sass +17 -0
- data/test/sass/templates/import.sass +11 -0
- data/test/sass/templates/importee.sass +19 -0
- data/test/sass/templates/line_numbers.sass +13 -0
- data/test/sass/templates/mixins.sass +76 -0
- data/test/sass/templates/multiline.sass +20 -0
- data/test/sass/templates/nested.sass +25 -0
- data/test/sass/templates/parent_ref.sass +25 -0
- data/test/sass/templates/script.sass +101 -0
- data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +2 -0
- data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +3 -0
- data/test/sass/templates/subdir/subdir.sass +6 -0
- data/test/sass/templates/units.sass +11 -0
- data/test/test_helper.rb +21 -0
- metadata +273 -0
data/lib/sass/css.rb
ADDED
@@ -0,0 +1,388 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../sass'
|
2
|
+
require 'sass/tree/node'
|
3
|
+
require 'strscan'
|
4
|
+
|
5
|
+
module Sass
|
6
|
+
# :stopdoc:
|
7
|
+
module Tree
|
8
|
+
class Node
|
9
|
+
def to_sass(opts = {})
|
10
|
+
result = ''
|
11
|
+
|
12
|
+
children.each do |child|
|
13
|
+
result << "#{child.to_sass(0, opts)}\n"
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class RuleNode
|
21
|
+
def to_sass(tabs, opts = {})
|
22
|
+
str = "\n#{' ' * tabs}#{rule}#{children.any? { |c| c.is_a? AttrNode } ? "\n" : ''}"
|
23
|
+
|
24
|
+
children.each do |child|
|
25
|
+
str << "#{child.to_sass(tabs + 1, opts)}"
|
26
|
+
end
|
27
|
+
|
28
|
+
str
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class AttrNode
|
33
|
+
def to_sass(tabs, opts = {})
|
34
|
+
"#{' ' * tabs}#{opts[:alternate] ? '' : ':'}#{name}#{opts[:alternate] ? ':' : ''} #{value}\n"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class DirectiveNode
|
39
|
+
def to_sass(tabs, opts = {})
|
40
|
+
"#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# This class is based on the Ruby 1.9 ordered hashes.
|
46
|
+
# It keeps the semantics and most of the efficiency of normal hashes
|
47
|
+
# while also keeping track of the order in which elements were set.
|
48
|
+
class OrderedHash
|
49
|
+
Node = Struct.new(:key, :value, :next, :prev)
|
50
|
+
include Enumerable
|
51
|
+
|
52
|
+
def initialize
|
53
|
+
@hash = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize_copy(other)
|
57
|
+
@hash = other.instance_variable_get('@hash').clone
|
58
|
+
end
|
59
|
+
|
60
|
+
def [](key)
|
61
|
+
@hash[key] && @hash[key].value
|
62
|
+
end
|
63
|
+
|
64
|
+
def []=(key, value)
|
65
|
+
node = Node.new(key, value)
|
66
|
+
|
67
|
+
if old = @hash[key]
|
68
|
+
if old.prev
|
69
|
+
old.prev.next = old.next
|
70
|
+
else # old is @first and @last
|
71
|
+
@first = @last = nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
if @first.nil?
|
76
|
+
@first = @last = node
|
77
|
+
else
|
78
|
+
node.prev = @last
|
79
|
+
@last.next = node
|
80
|
+
@last = node
|
81
|
+
end
|
82
|
+
|
83
|
+
@hash[key] = node
|
84
|
+
value
|
85
|
+
end
|
86
|
+
|
87
|
+
def each
|
88
|
+
return unless @first
|
89
|
+
yield [@first.key, @first.value]
|
90
|
+
node = @first
|
91
|
+
yield [node.key, node.value] while node = node.next
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
def values
|
96
|
+
self.map { |k, v| v }
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# :startdoc:
|
101
|
+
|
102
|
+
# This class contains the functionality used in the +css2sass+ utility,
|
103
|
+
# namely converting CSS documents to Sass templates.
|
104
|
+
class CSS
|
105
|
+
|
106
|
+
# Creates a new instance of Sass::CSS that will compile the given document
|
107
|
+
# to a Sass string when +render+ is called.
|
108
|
+
def initialize(template, options = {})
|
109
|
+
if template.is_a? IO
|
110
|
+
template = template.read
|
111
|
+
end
|
112
|
+
|
113
|
+
@options = options
|
114
|
+
@template = StringScanner.new(template)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Processes the document and returns the result as a string
|
118
|
+
# containing the CSS template.
|
119
|
+
def render
|
120
|
+
begin
|
121
|
+
build_tree.to_sass(@options).strip + "\n"
|
122
|
+
rescue Exception => err
|
123
|
+
line = @template.string[0...@template.pos].split("\n").size
|
124
|
+
|
125
|
+
err.backtrace.unshift "(css):#{line}"
|
126
|
+
raise err
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def build_tree
|
133
|
+
root = Tree::Node.new({})
|
134
|
+
whitespace
|
135
|
+
rules root
|
136
|
+
expand_commas root
|
137
|
+
parent_ref_rules root
|
138
|
+
remove_parent_refs root
|
139
|
+
flatten_rules root
|
140
|
+
fold_commas root
|
141
|
+
root
|
142
|
+
end
|
143
|
+
|
144
|
+
def rules(root)
|
145
|
+
while r = rule
|
146
|
+
root << r
|
147
|
+
whitespace
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def rule
|
152
|
+
return unless rule = @template.scan(/[^\{\};]+/)
|
153
|
+
rule.strip!
|
154
|
+
directive = rule[0] == ?@
|
155
|
+
|
156
|
+
if directive
|
157
|
+
node = Tree::DirectiveNode.new(rule, {})
|
158
|
+
return node if @template.scan(/;/)
|
159
|
+
|
160
|
+
assert_match /\{/
|
161
|
+
whitespace
|
162
|
+
|
163
|
+
rules(node)
|
164
|
+
return node
|
165
|
+
end
|
166
|
+
|
167
|
+
assert_match /\{/
|
168
|
+
node = Tree::RuleNode.new(rule, {})
|
169
|
+
attributes(node)
|
170
|
+
return node
|
171
|
+
end
|
172
|
+
|
173
|
+
def attributes(rule)
|
174
|
+
while @template.scan(/[^:\}\s]+/)
|
175
|
+
name = @template[0]
|
176
|
+
whitespace
|
177
|
+
|
178
|
+
assert_match /:/
|
179
|
+
|
180
|
+
value = ''
|
181
|
+
while @template.scan(/[^;\s\}]+/)
|
182
|
+
value << @template[0] << whitespace
|
183
|
+
end
|
184
|
+
|
185
|
+
assert_match /(;|(?=\}))/
|
186
|
+
rule << Tree::AttrNode.new(name, value, {})
|
187
|
+
end
|
188
|
+
|
189
|
+
assert_match /\}/
|
190
|
+
end
|
191
|
+
|
192
|
+
def whitespace
|
193
|
+
space = @template.scan(/\s*/) || ''
|
194
|
+
|
195
|
+
# If we've hit a comment,
|
196
|
+
# go past it and look for more whitespace
|
197
|
+
if @template.scan(/\/\*/)
|
198
|
+
@template.scan_until(/\*\//)
|
199
|
+
return space + whitespace
|
200
|
+
end
|
201
|
+
return space
|
202
|
+
end
|
203
|
+
|
204
|
+
def assert_match(re)
|
205
|
+
if !@template.scan(re)
|
206
|
+
line = @template.string[0..@template.pos].count "\n"
|
207
|
+
# Display basic regexps as plain old strings
|
208
|
+
expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect
|
209
|
+
raise Exception.new("Invalid CSS on line #{line}: expected #{expected}")
|
210
|
+
end
|
211
|
+
whitespace
|
212
|
+
end
|
213
|
+
|
214
|
+
# Transform
|
215
|
+
#
|
216
|
+
# foo, bar, baz
|
217
|
+
# color: blue
|
218
|
+
#
|
219
|
+
# into
|
220
|
+
#
|
221
|
+
# foo
|
222
|
+
# color: blue
|
223
|
+
# bar
|
224
|
+
# color: blue
|
225
|
+
# baz
|
226
|
+
# color: blue
|
227
|
+
#
|
228
|
+
# Yes, this expands the amount of code,
|
229
|
+
# but it's necessary to get nesting to work properly.
|
230
|
+
def expand_commas(root)
|
231
|
+
root.children.map! do |child|
|
232
|
+
next child unless Tree::RuleNode === child && child.rule.include?(',')
|
233
|
+
child.rule.split(',').map do |rule|
|
234
|
+
node = Tree::RuleNode.new(rule.strip, {})
|
235
|
+
node.children = child.children
|
236
|
+
node
|
237
|
+
end
|
238
|
+
end
|
239
|
+
root.children.flatten!
|
240
|
+
end
|
241
|
+
|
242
|
+
# Make rules use parent refs so that
|
243
|
+
#
|
244
|
+
# foo
|
245
|
+
# color: green
|
246
|
+
# foo.bar
|
247
|
+
# color: blue
|
248
|
+
#
|
249
|
+
# becomes
|
250
|
+
#
|
251
|
+
# foo
|
252
|
+
# color: green
|
253
|
+
# &.bar
|
254
|
+
# color: blue
|
255
|
+
#
|
256
|
+
# This has the side effect of nesting rules,
|
257
|
+
# so that
|
258
|
+
#
|
259
|
+
# foo
|
260
|
+
# color: green
|
261
|
+
# foo bar
|
262
|
+
# color: red
|
263
|
+
# foo baz
|
264
|
+
# color: blue
|
265
|
+
#
|
266
|
+
# becomes
|
267
|
+
#
|
268
|
+
# foo
|
269
|
+
# color: green
|
270
|
+
# & bar
|
271
|
+
# color: red
|
272
|
+
# & baz
|
273
|
+
# color: blue
|
274
|
+
#
|
275
|
+
def parent_ref_rules(root)
|
276
|
+
rules = OrderedHash.new
|
277
|
+
root.children.select { |c| Tree::RuleNode === c }.each do |child|
|
278
|
+
root.children.delete child
|
279
|
+
first, rest = child.rule.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first
|
280
|
+
rules[first] ||= Tree::RuleNode.new(first, {})
|
281
|
+
if rest
|
282
|
+
child.rule = "&" + rest
|
283
|
+
rules[first] << child
|
284
|
+
else
|
285
|
+
rules[first].children += child.children
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
rules.values.each { |v| parent_ref_rules(v) }
|
290
|
+
root.children += rules.values
|
291
|
+
end
|
292
|
+
|
293
|
+
# Remove useless parent refs so that
|
294
|
+
#
|
295
|
+
# foo
|
296
|
+
# & bar
|
297
|
+
# color: blue
|
298
|
+
#
|
299
|
+
# becomes
|
300
|
+
#
|
301
|
+
# foo
|
302
|
+
# bar
|
303
|
+
# color: blue
|
304
|
+
#
|
305
|
+
def remove_parent_refs(root)
|
306
|
+
root.children.each do |child|
|
307
|
+
if child.is_a?(Tree::RuleNode)
|
308
|
+
child.rule.gsub! /^& +/, ''
|
309
|
+
remove_parent_refs child
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# Flatten rules so that
|
315
|
+
#
|
316
|
+
# foo
|
317
|
+
# bar
|
318
|
+
# baz
|
319
|
+
# color: red
|
320
|
+
#
|
321
|
+
# becomes
|
322
|
+
#
|
323
|
+
# foo bar baz
|
324
|
+
# color: red
|
325
|
+
#
|
326
|
+
# and
|
327
|
+
#
|
328
|
+
# foo
|
329
|
+
# &.bar
|
330
|
+
# color: blue
|
331
|
+
#
|
332
|
+
# becomes
|
333
|
+
#
|
334
|
+
# foo.bar
|
335
|
+
# color: blue
|
336
|
+
#
|
337
|
+
def flatten_rules(root)
|
338
|
+
root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) }
|
339
|
+
end
|
340
|
+
|
341
|
+
def flatten_rule(rule)
|
342
|
+
while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode)
|
343
|
+
child = rule.children.first
|
344
|
+
|
345
|
+
if child.rule[0] == ?&
|
346
|
+
rule.rule = child.rule.gsub /^&/, rule.rule
|
347
|
+
else
|
348
|
+
rule.rule = "#{rule.rule} #{child.rule}"
|
349
|
+
end
|
350
|
+
|
351
|
+
rule.children = child.children
|
352
|
+
end
|
353
|
+
|
354
|
+
flatten_rules(rule)
|
355
|
+
end
|
356
|
+
|
357
|
+
# Transform
|
358
|
+
#
|
359
|
+
# foo
|
360
|
+
# bar
|
361
|
+
# color: blue
|
362
|
+
# baz
|
363
|
+
# color: blue
|
364
|
+
#
|
365
|
+
# into
|
366
|
+
#
|
367
|
+
# foo
|
368
|
+
# bar, baz
|
369
|
+
# color: blue
|
370
|
+
#
|
371
|
+
def fold_commas(root)
|
372
|
+
prev_rule = nil
|
373
|
+
root.children.map! do |child|
|
374
|
+
next child unless child.is_a?(Tree::RuleNode)
|
375
|
+
|
376
|
+
if prev_rule && prev_rule.children == child.children
|
377
|
+
prev_rule.rule << ", #{child.rule}"
|
378
|
+
next nil
|
379
|
+
end
|
380
|
+
|
381
|
+
fold_commas(child)
|
382
|
+
prev_rule = child
|
383
|
+
child
|
384
|
+
end
|
385
|
+
root.children.compact!
|
386
|
+
end
|
387
|
+
end
|
388
|
+
end
|
data/lib/sass/engine.rb
ADDED
@@ -0,0 +1,501 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
require 'sass/tree/node'
|
3
|
+
require 'sass/tree/rule_node'
|
4
|
+
require 'sass/tree/comment_node'
|
5
|
+
require 'sass/tree/attr_node'
|
6
|
+
require 'sass/tree/directive_node'
|
7
|
+
require 'sass/tree/variable_node'
|
8
|
+
require 'sass/tree/mixin_def_node'
|
9
|
+
require 'sass/tree/mixin_node'
|
10
|
+
require 'sass/tree/if_node'
|
11
|
+
require 'sass/tree/while_node'
|
12
|
+
require 'sass/tree/for_node'
|
13
|
+
require 'sass/tree/debug_node'
|
14
|
+
require 'sass/tree/file_node'
|
15
|
+
require 'sass/environment'
|
16
|
+
require 'sass/script'
|
17
|
+
require 'sass/error'
|
18
|
+
require 'haml/shared'
|
19
|
+
|
20
|
+
module Sass
|
21
|
+
# :stopdoc:
|
22
|
+
Mixin = Struct.new(:name, :args, :environment, :tree)
|
23
|
+
# :startdoc:
|
24
|
+
|
25
|
+
# This is the class where all the parsing and processing of the Sass
|
26
|
+
# template is done. It can be directly used by the user by creating a
|
27
|
+
# new instance and calling <tt>render</tt> to render the template. For example:
|
28
|
+
#
|
29
|
+
# template = File.load('stylesheets/sassy.sass')
|
30
|
+
# sass_engine = Sass::Engine.new(template)
|
31
|
+
# output = sass_engine.render
|
32
|
+
# puts output
|
33
|
+
class Engine
|
34
|
+
include Haml::Util
|
35
|
+
Line = Struct.new(:text, :tabs, :index, :offset, :filename, :children)
|
36
|
+
|
37
|
+
# The character that begins a CSS attribute.
|
38
|
+
ATTRIBUTE_CHAR = ?:
|
39
|
+
|
40
|
+
# The character that designates that
|
41
|
+
# an attribute should be assigned to a SassScript expression.
|
42
|
+
SCRIPT_CHAR = ?=
|
43
|
+
|
44
|
+
# The character that designates the beginning of a comment,
|
45
|
+
# either Sass or CSS.
|
46
|
+
COMMENT_CHAR = ?/
|
47
|
+
|
48
|
+
# The character that follows the general COMMENT_CHAR and designates a Sass comment,
|
49
|
+
# which is not output as a CSS comment.
|
50
|
+
SASS_COMMENT_CHAR = ?/
|
51
|
+
|
52
|
+
# The character that follows the general COMMENT_CHAR and designates a CSS comment,
|
53
|
+
# which is embedded in the CSS document.
|
54
|
+
CSS_COMMENT_CHAR = ?*
|
55
|
+
|
56
|
+
# The character used to denote a compiler directive.
|
57
|
+
DIRECTIVE_CHAR = ?@
|
58
|
+
|
59
|
+
# Designates a non-parsed rule.
|
60
|
+
ESCAPE_CHAR = ?\\
|
61
|
+
|
62
|
+
# Designates block as mixin definition rather than CSS rules to output
|
63
|
+
MIXIN_DEFINITION_CHAR = ?=
|
64
|
+
|
65
|
+
# Includes named mixin declared using MIXIN_DEFINITION_CHAR
|
66
|
+
MIXIN_INCLUDE_CHAR = ?+
|
67
|
+
|
68
|
+
# The regex that matches and extracts data from
|
69
|
+
# attributes of the form <tt>:name attr</tt>.
|
70
|
+
ATTRIBUTE = /^:([^\s=:]+)\s*(=?)(?:\s+|$)(.*)/
|
71
|
+
|
72
|
+
# The regex that matches attributes of the form <tt>name: attr</tt>.
|
73
|
+
ATTRIBUTE_ALTERNATE_MATCHER = /^[^\s:]+\s*[=:](\s|$)/
|
74
|
+
|
75
|
+
# The regex that matches and extracts data from
|
76
|
+
# attributes of the form <tt>name: attr</tt>.
|
77
|
+
ATTRIBUTE_ALTERNATE = /^([^\s=:]+)(\s*=|:)(?:\s+|$)(.*)/
|
78
|
+
|
79
|
+
# Creates a new instace of Sass::Engine that will compile the given
|
80
|
+
# template string when <tt>render</tt> is called.
|
81
|
+
# See README.rdoc for available options.
|
82
|
+
#
|
83
|
+
#--
|
84
|
+
#
|
85
|
+
# TODO: Add current options to REFRENCE. Remember :filename!
|
86
|
+
#
|
87
|
+
# When adding options, remember to add information about them
|
88
|
+
# to README.rdoc!
|
89
|
+
#++
|
90
|
+
#
|
91
|
+
def initialize(template, options={})
|
92
|
+
@options = {
|
93
|
+
:style => :nested,
|
94
|
+
:load_paths => ['.']
|
95
|
+
}.merge! options
|
96
|
+
@template = template
|
97
|
+
@environment = Environment.new
|
98
|
+
@environment.set_var("important", Script::String.new("!important"))
|
99
|
+
end
|
100
|
+
|
101
|
+
# Processes the template and returns the result as a string.
|
102
|
+
def render
|
103
|
+
begin
|
104
|
+
render_to_tree.perform(@environment).to_s
|
105
|
+
rescue SyntaxError => err
|
106
|
+
err.sass_line = @line unless err.sass_line
|
107
|
+
unless err.sass_filename
|
108
|
+
err.add_backtrace_entry(@options[:filename])
|
109
|
+
end
|
110
|
+
raise err
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
alias_method :to_css, :render
|
115
|
+
|
116
|
+
protected
|
117
|
+
|
118
|
+
def environment
|
119
|
+
@environment
|
120
|
+
end
|
121
|
+
|
122
|
+
def render_to_tree
|
123
|
+
root = Tree::Node.new(@options)
|
124
|
+
append_children(root, tree(tabulate(@template)).first, true)
|
125
|
+
root
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def tabulate(string)
|
131
|
+
tab_str = nil
|
132
|
+
first = true
|
133
|
+
enum_with_index(string.gsub(/\r|\n|\r\n|\r\n/, "\n").scan(/^.*?$/)).map do |line, index|
|
134
|
+
index += 1
|
135
|
+
next if line.strip.empty? || line =~ /^\/\//
|
136
|
+
|
137
|
+
line_tab_str = line[/^\s*/]
|
138
|
+
unless line_tab_str.empty?
|
139
|
+
tab_str ||= line_tab_str
|
140
|
+
|
141
|
+
raise SyntaxError.new("Indenting at the beginning of the document is illegal.", index) if first
|
142
|
+
if tab_str.include?(?\s) && tab_str.include?(?\t)
|
143
|
+
raise SyntaxError.new("Indentation can't use both tabs and spaces.", index)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
first &&= !tab_str.nil?
|
147
|
+
next Line.new(line.strip, 0, index, 0, @options[:filename], []) if tab_str.nil?
|
148
|
+
|
149
|
+
line_tabs = line_tab_str.scan(tab_str).size
|
150
|
+
raise SyntaxError.new(<<END.strip.gsub("\n", ' '), index) if tab_str * line_tabs != line_tab_str
|
151
|
+
Inconsistent indentation: #{Haml::Shared.human_indentation line_tab_str, true} used for indentation,
|
152
|
+
but the rest of the document was indented using #{Haml::Shared.human_indentation tab_str}.
|
153
|
+
END
|
154
|
+
Line.new(line.strip, line_tabs, index, tab_str.size, @options[:filename], [])
|
155
|
+
end.compact
|
156
|
+
end
|
157
|
+
|
158
|
+
def tree(arr, i = 0)
|
159
|
+
return [], i if arr[i].nil?
|
160
|
+
|
161
|
+
base = arr[i].tabs
|
162
|
+
nodes = []
|
163
|
+
while (line = arr[i]) && line.tabs >= base
|
164
|
+
if line.tabs > base
|
165
|
+
if line.tabs > base + 1
|
166
|
+
raise SyntaxError.new("The line was indented #{line.tabs - base} levels deeper than the previous line.", line.index)
|
167
|
+
end
|
168
|
+
|
169
|
+
nodes.last.children, i = tree(arr, i)
|
170
|
+
else
|
171
|
+
nodes << line
|
172
|
+
i += 1
|
173
|
+
end
|
174
|
+
end
|
175
|
+
return nodes, i
|
176
|
+
end
|
177
|
+
|
178
|
+
def build_tree(parent, line, root = false)
|
179
|
+
@line = line.index
|
180
|
+
node = parse_line(parent, line, root)
|
181
|
+
|
182
|
+
# Node is a symbol if it's non-outputting, like a variable assignment,
|
183
|
+
# or an array if it's a group of nodes to add
|
184
|
+
return node unless node.is_a? Tree::Node
|
185
|
+
|
186
|
+
node.line = line.index
|
187
|
+
node.filename = line.filename
|
188
|
+
|
189
|
+
unless node.is_a?(Tree::CommentNode)
|
190
|
+
append_children(node, line.children, false)
|
191
|
+
else
|
192
|
+
node.children = line.children
|
193
|
+
end
|
194
|
+
return node
|
195
|
+
end
|
196
|
+
|
197
|
+
def append_children(parent, children, root)
|
198
|
+
continued_rule = nil
|
199
|
+
children.each do |line|
|
200
|
+
child = build_tree(parent, line, root)
|
201
|
+
|
202
|
+
if child.is_a?(Tree::RuleNode) && child.continued?
|
203
|
+
raise SyntaxError.new("Rules can't end in commas.", child.line) unless child.children.empty?
|
204
|
+
if continued_rule
|
205
|
+
continued_rule.add_rules child
|
206
|
+
else
|
207
|
+
continued_rule = child
|
208
|
+
end
|
209
|
+
next
|
210
|
+
end
|
211
|
+
|
212
|
+
if continued_rule
|
213
|
+
raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) unless child.is_a?(Tree::RuleNode)
|
214
|
+
continued_rule.add_rules child
|
215
|
+
continued_rule.children = child.children
|
216
|
+
continued_rule, child = nil, continued_rule
|
217
|
+
end
|
218
|
+
|
219
|
+
validate_and_append_child(parent, child, line, root)
|
220
|
+
end
|
221
|
+
|
222
|
+
raise SyntaxError.new("Rules can't end in commas.", continued_rule.line) if continued_rule
|
223
|
+
|
224
|
+
parent
|
225
|
+
end
|
226
|
+
|
227
|
+
def validate_and_append_child(parent, child, line, root)
|
228
|
+
unless root
|
229
|
+
case child
|
230
|
+
when Tree::MixinDefNode
|
231
|
+
raise SyntaxError.new("Mixins may only be defined at the root of a document.", line.index)
|
232
|
+
when Tree::DirectiveNode
|
233
|
+
raise SyntaxError.new("Import directives may only be used at the root of a document.", line.index)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
case child
|
238
|
+
when Array
|
239
|
+
child.each {|c| validate_and_append_child(parent, c, line, root)}
|
240
|
+
when Tree::Node
|
241
|
+
parent << child
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
def parse_line(parent, line, root)
|
246
|
+
case line.text[0]
|
247
|
+
when ATTRIBUTE_CHAR
|
248
|
+
if line.text[1] != ATTRIBUTE_CHAR
|
249
|
+
parse_attribute(line, ATTRIBUTE)
|
250
|
+
else
|
251
|
+
# Support CSS3-style pseudo-elements,
|
252
|
+
# which begin with ::
|
253
|
+
Tree::RuleNode.new(line.text, @options)
|
254
|
+
end
|
255
|
+
when Script::VARIABLE_CHAR
|
256
|
+
parse_variable(line)
|
257
|
+
when COMMENT_CHAR
|
258
|
+
parse_comment(line.text)
|
259
|
+
when DIRECTIVE_CHAR
|
260
|
+
parse_directive(parent, line, root)
|
261
|
+
when ESCAPE_CHAR
|
262
|
+
Tree::RuleNode.new(line.text[1..-1], @options)
|
263
|
+
when MIXIN_DEFINITION_CHAR
|
264
|
+
parse_mixin_definition(line)
|
265
|
+
when MIXIN_INCLUDE_CHAR
|
266
|
+
if line.text[1].nil?
|
267
|
+
Tree::RuleNode.new(line.text, @options)
|
268
|
+
else
|
269
|
+
parse_mixin_include(line, root)
|
270
|
+
end
|
271
|
+
else
|
272
|
+
if line.text =~ ATTRIBUTE_ALTERNATE_MATCHER
|
273
|
+
parse_attribute(line, ATTRIBUTE_ALTERNATE)
|
274
|
+
else
|
275
|
+
Tree::RuleNode.new(line.text, @options)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def parse_attribute(line, attribute_regx)
|
281
|
+
if @options[:attribute_syntax] == :normal &&
|
282
|
+
attribute_regx == ATTRIBUTE_ALTERNATE
|
283
|
+
raise SyntaxError.new("Illegal attribute syntax: can't use alternate syntax when :attribute_syntax => :normal is set.")
|
284
|
+
elsif @options[:attribute_syntax] == :alternate &&
|
285
|
+
attribute_regx == ATTRIBUTE
|
286
|
+
raise SyntaxError.new("Illegal attribute syntax: can't use normal syntax when :attribute_syntax => :alternate is set.")
|
287
|
+
end
|
288
|
+
|
289
|
+
name, eq, value = line.text.scan(attribute_regx)[0]
|
290
|
+
|
291
|
+
if name.nil? || value.nil?
|
292
|
+
raise SyntaxError.new("Invalid attribute: \"#{line.text}\".", @line)
|
293
|
+
end
|
294
|
+
expr = if (eq.strip[0] == SCRIPT_CHAR)
|
295
|
+
parse_script(value, :offset => line.offset + line.text.index(value))
|
296
|
+
else
|
297
|
+
value
|
298
|
+
end
|
299
|
+
Tree::AttrNode.new(name, expr, @options)
|
300
|
+
end
|
301
|
+
|
302
|
+
def parse_variable(line)
|
303
|
+
name, op, value = line.text.scan(Script::MATCH)[0]
|
304
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath variable declarations.", @line + 1) unless line.children.empty?
|
305
|
+
raise SyntaxError.new("Invalid variable: \"#{line.text}\".", @line) unless name && value
|
306
|
+
|
307
|
+
Tree::VariableNode.new(name, parse_script(value, :offset => line.offset + line.text.index(value)), op == '||=', @options)
|
308
|
+
end
|
309
|
+
|
310
|
+
def parse_comment(line)
|
311
|
+
if line[1] == SASS_COMMENT_CHAR
|
312
|
+
:comment
|
313
|
+
elsif line[1] == CSS_COMMENT_CHAR
|
314
|
+
Tree::CommentNode.new(line, @options)
|
315
|
+
else
|
316
|
+
Tree::RuleNode.new(line, @options)
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def parse_directive(parent, line, root)
|
321
|
+
directive, whitespace, value = line.text[1..-1].split(/(\s+)/, 2)
|
322
|
+
offset = directive.size + whitespace.size + 1 if whitespace
|
323
|
+
|
324
|
+
# If value begins with url( or ",
|
325
|
+
# it's a CSS @import rule and we don't want to touch it.
|
326
|
+
if directive == "import" && value !~ /^(url\(|")/
|
327
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath import directives.", @line + 1) unless line.children.empty?
|
328
|
+
import(value)
|
329
|
+
elsif directive == "for"
|
330
|
+
parse_for(line, root, value)
|
331
|
+
elsif directive == "else"
|
332
|
+
parse_else(parent, line, value)
|
333
|
+
elsif directive == "while"
|
334
|
+
raise SyntaxError.new("Invalid while directive '@while': expected expression.") unless value
|
335
|
+
Tree::WhileNode.new(parse_script(value, :offset => offset), @options)
|
336
|
+
elsif directive == "if"
|
337
|
+
raise SyntaxError.new("Invalid if directive '@if': expected expression.") unless value
|
338
|
+
Tree::IfNode.new(parse_script(value, :offset => offset), @options)
|
339
|
+
elsif directive == "debug"
|
340
|
+
raise SyntaxError.new("Invalid debug directive '@debug': expected expression.") unless value
|
341
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath debug directives.", @line + 1) unless line.children.empty?
|
342
|
+
offset = line.offset + line.text.index(value).to_i
|
343
|
+
Tree::DebugNode.new(parse_script(value, :offset => offset), @options)
|
344
|
+
else
|
345
|
+
Tree::DirectiveNode.new(line.text, @options)
|
346
|
+
end
|
347
|
+
end
|
348
|
+
|
349
|
+
def parse_for(line, root, text)
|
350
|
+
var, from_expr, to_name, to_expr = text.scan(/^([^\s]+)\s+from\s+(.+)\s+(to|through)\s+(.+)$/).first
|
351
|
+
|
352
|
+
if var.nil? # scan failed, try to figure out why for error message
|
353
|
+
if text !~ /^[^\s]+/
|
354
|
+
expected = "variable name"
|
355
|
+
elsif text !~ /^[^\s]+\s+from\s+.+/
|
356
|
+
expected = "'from <expr>'"
|
357
|
+
else
|
358
|
+
expected = "'to <expr>' or 'through <expr>'"
|
359
|
+
end
|
360
|
+
raise SyntaxError.new("Invalid for directive '@for #{text}': expected #{expected}.", @line)
|
361
|
+
end
|
362
|
+
raise SyntaxError.new("Invalid variable \"#{var}\".", @line) unless var =~ Script::VALIDATE
|
363
|
+
|
364
|
+
parsed_from = parse_script(from_expr, :offset => line.offset + line.text.index(from_expr))
|
365
|
+
parsed_to = parse_script(to_expr, :offset => line.offset + line.text.index(to_expr))
|
366
|
+
Tree::ForNode.new(var[1..-1], parsed_from, parsed_to, to_name == 'to', @options)
|
367
|
+
end
|
368
|
+
|
369
|
+
def parse_else(parent, line, text)
|
370
|
+
previous = parent.last
|
371
|
+
raise SyntaxError.new("@else must come after @if.") unless previous.is_a?(Tree::IfNode)
|
372
|
+
|
373
|
+
if text
|
374
|
+
if text !~ /^if\s+(.+)/
|
375
|
+
raise SyntaxError.new("Invalid else directive '@else #{text}': expected 'if <expr>'.", @line)
|
376
|
+
end
|
377
|
+
expr = parse_script($1, :offset => line.offset + line.text.index($1))
|
378
|
+
end
|
379
|
+
|
380
|
+
node = Tree::IfNode.new(expr, @options)
|
381
|
+
append_children(node, line.children, false)
|
382
|
+
previous.add_else node
|
383
|
+
nil
|
384
|
+
end
|
385
|
+
|
386
|
+
# parses out the arguments between the commas and cleans up the mixin arguments
|
387
|
+
# returns nil if it fails to parse, otherwise an array.
|
388
|
+
def parse_mixin_arguments(arg_string)
|
389
|
+
arg_string = arg_string.strip
|
390
|
+
return [] if arg_string.empty?
|
391
|
+
return nil unless (arg_string[0] == ?( && arg_string[-1] == ?))
|
392
|
+
arg_string = arg_string[1...-1]
|
393
|
+
arg_string.split(",", -1).map {|a| a.strip}
|
394
|
+
end
|
395
|
+
|
396
|
+
def parse_mixin_definition(line)
|
397
|
+
name, arg_string = line.text.scan(/^=\s*([^(]+)(.*)$/).first
|
398
|
+
args = parse_mixin_arguments(arg_string)
|
399
|
+
raise SyntaxError.new("Invalid mixin \"#{line.text[1..-1]}\".", @line) if name.nil? || args.nil?
|
400
|
+
default_arg_found = false
|
401
|
+
required_arg_count = 0
|
402
|
+
args.map! do |arg|
|
403
|
+
raise SyntaxError.new("Mixin arguments can't be empty.", @line) if arg.empty? || arg == "!"
|
404
|
+
unless arg[0] == Script::VARIABLE_CHAR
|
405
|
+
raise SyntaxError.new("Mixin argument \"#{arg}\" must begin with an exclamation point (!).", @line)
|
406
|
+
end
|
407
|
+
arg, default = arg.split(/\s*=\s*/, 2)
|
408
|
+
required_arg_count += 1 unless default
|
409
|
+
default_arg_found ||= default
|
410
|
+
raise SyntaxError.new("Invalid variable \"#{arg}\".", @line) unless arg =~ Script::VALIDATE
|
411
|
+
raise SyntaxError.new("Required arguments must not follow optional arguments \"#{arg}\".", @line) if default_arg_found && !default
|
412
|
+
default = parse_script(default, :offset => line.offset + line.text.index(default)) if default
|
413
|
+
{ :name => arg[1..-1], :default_value => default }
|
414
|
+
end
|
415
|
+
Tree::MixinDefNode.new(name, args, @options)
|
416
|
+
end
|
417
|
+
|
418
|
+
def parse_mixin_include(line, root)
|
419
|
+
name, arg_string = line.text.scan(/^\+\s*([^(]+)(.*)$/).first
|
420
|
+
args = parse_mixin_arguments(arg_string)
|
421
|
+
raise SyntaxError.new("Illegal nesting: Nothing may be nested beneath mixin directives.", @line + 1) unless line.children.empty?
|
422
|
+
raise SyntaxError.new("Invalid mixin include \"#{line.text}\".", @line) if name.nil? || args.nil?
|
423
|
+
args.each {|a| raise SyntaxError.new("Mixin arguments can't be empty.", @line) if a.empty?}
|
424
|
+
|
425
|
+
Tree::MixinNode.new(name, args.map {|s| parse_script(s, :offset => line.offset + line.text.index(s))}, @options)
|
426
|
+
end
|
427
|
+
|
428
|
+
def parse_script(script, options = {})
|
429
|
+
line = options[:line] || @line
|
430
|
+
offset = options[:offset] || 0
|
431
|
+
Script.parse(script, line, offset, @options[:filename])
|
432
|
+
end
|
433
|
+
|
434
|
+
def import_paths
|
435
|
+
paths = @options[:load_paths] || []
|
436
|
+
paths.unshift(File.dirname(@options[:filename])) if @options[:filename]
|
437
|
+
paths
|
438
|
+
end
|
439
|
+
|
440
|
+
def import(files)
|
441
|
+
files.split(/,\s*/).map do |filename|
|
442
|
+
engine = nil
|
443
|
+
|
444
|
+
begin
|
445
|
+
filename = self.class.find_file_to_import(filename, import_paths)
|
446
|
+
rescue Exception => e
|
447
|
+
raise SyntaxError.new(e.message, @line)
|
448
|
+
end
|
449
|
+
|
450
|
+
next Tree::DirectiveNode.new("@import url(#{filename})", @options) if filename =~ /\.css$/
|
451
|
+
|
452
|
+
File.open(filename) do |file|
|
453
|
+
new_options = @options.dup
|
454
|
+
new_options[:filename] = filename
|
455
|
+
engine = Sass::Engine.new(file.read, new_options)
|
456
|
+
end
|
457
|
+
|
458
|
+
begin
|
459
|
+
root = engine.render_to_tree
|
460
|
+
rescue Sass::SyntaxError => err
|
461
|
+
err.add_backtrace_entry(filename)
|
462
|
+
raise err
|
463
|
+
end
|
464
|
+
Tree::FileNode.new(filename, root.children, @options)
|
465
|
+
end.flatten
|
466
|
+
end
|
467
|
+
|
468
|
+
def self.find_file_to_import(filename, load_paths)
|
469
|
+
was_sass = false
|
470
|
+
original_filename = filename
|
471
|
+
|
472
|
+
if filename[-5..-1] == ".sass"
|
473
|
+
filename = filename[0...-5]
|
474
|
+
was_sass = true
|
475
|
+
elsif filename[-4..-1] == ".css"
|
476
|
+
return filename
|
477
|
+
end
|
478
|
+
|
479
|
+
new_filename = find_full_path("#{filename}.sass", load_paths)
|
480
|
+
|
481
|
+
return new_filename if new_filename
|
482
|
+
return filename + '.css' unless was_sass
|
483
|
+
raise SyntaxError.new("File to import not found or unreadable: #{original_filename}.", @line)
|
484
|
+
end
|
485
|
+
|
486
|
+
def self.find_full_path(filename, load_paths)
|
487
|
+
segments = filename.split(File::SEPARATOR)
|
488
|
+
segments.push "_#{segments.pop}"
|
489
|
+
partial_name = segments.join(File::SEPARATOR)
|
490
|
+
load_paths.each do |path|
|
491
|
+
[partial_name, filename].each do |name|
|
492
|
+
full_path = File.join(path, name)
|
493
|
+
if File.readable?(full_path)
|
494
|
+
return full_path
|
495
|
+
end
|
496
|
+
end
|
497
|
+
end
|
498
|
+
nil
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|