Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 373
- Log:
Initial import of Radiant 0.9.1, which is now packaged as a gem. This is an
import of the tagged 0.9.1 source checked out from GitHub, which isn't quite
the same as the gem distribution - but it doesn't seem to be available in an
archived form and the installed gem already has modifications, so this is
the closest I can get.
- Author:
- rool
- Date:
- Mon Mar 21 13:40:05 +0000 2011
- Size:
- 9791 Bytes
1 | require File.dirname(__FILE__) + '/../sass' |
2 | require 'sass/tree/node' |
3 | require 'strscan' |
4 | |
5 | module Sass |
6 | module Tree |
7 | class Node |
8 | # Converts a node to Sass code that will generate it. |
9 | # |
10 | # @param tabs [Fixnum] The amount of tabulation to use for the Sass code |
11 | # @param opts [{Symbol => Object}] An options hash (see {Sass::CSS#initialize}) |
12 | # @return [String] The Sass code corresponding to the node |
13 | def to_sass(tabs = 0, opts = {}) |
14 | result = '' |
15 | |
16 | children.each do |child| |
17 | result << "#{' ' * tabs}#{child.to_sass(0, opts)}\n" |
18 | end |
19 | |
20 | result |
21 | end |
22 | end |
23 | |
24 | class RuleNode |
25 | # @see Node#to_sass |
26 | def to_sass(tabs, opts = {}) |
27 | name = rules.first |
28 | name = "\\" + name if name[0] == ?: |
29 | str = "\n#{' ' * tabs}#{name}#{children.any? { |c| c.is_a? PropNode } ? "\n" : ''}" |
30 | |
31 | children.each do |child| |
32 | str << "#{child.to_sass(tabs + 1, opts)}" |
33 | end |
34 | |
35 | str |
36 | end |
37 | end |
38 | |
39 | class PropNode |
40 | # @see Node#to_sass |
41 | def to_sass(tabs, opts = {}) |
42 | "#{' ' * tabs}#{opts[:old] ? ':' : ''}#{name}#{opts[:old] ? '' : ':'} #{value}\n" |
43 | end |
44 | end |
45 | |
46 | class DirectiveNode |
47 | # @see Node#to_sass |
48 | def to_sass(tabs, opts = {}) |
49 | "#{' ' * tabs}#{value}#{children.map {|c| c.to_sass(tabs + 1, opts)}}\n" |
50 | end |
51 | end |
52 | end |
53 | |
54 | # This class converts CSS documents into Sass templates. |
55 | # It works by parsing the CSS document into a {Sass::Tree} structure, |
56 | # and then applying various transformations to the structure |
57 | # to produce more concise and idiomatic Sass. |
58 | # |
59 | # Example usage: |
60 | # |
61 | # Sass::CSS.new("p { color: blue }").render #=> "p\n color: blue" |
62 | class CSS |
63 | # @param template [String] The CSS code |
64 | # @option options :old [Boolean] (false) |
65 | # Whether or not to output old property syntax |
66 | # (`:color blue` as opposed to `color: blue`). |
67 | def initialize(template, options = {}) |
68 | if template.is_a? IO |
69 | template = template.read |
70 | end |
71 | |
72 | @options = options.dup |
73 | # Backwards compatibility |
74 | @options[:old] = true if @options[:alternate] == false |
75 | @template = StringScanner.new(template) |
76 | end |
77 | |
78 | # Converts the CSS template into Sass code. |
79 | # |
80 | # @return [String] The resulting Sass code |
81 | def render |
82 | begin |
83 | build_tree.to_sass(0, @options).strip + "\n" |
84 | rescue Exception => err |
85 | line = @template.string[0...@template.pos].split("\n").size |
86 | |
87 | err.backtrace.unshift "(css):#{line}" |
88 | raise err |
89 | end |
90 | end |
91 | |
92 | private |
93 | |
94 | # Parses the CSS template and applies various transformations |
95 | # |
96 | # @return [Tree::Node] The root node of the parsed tree |
97 | def build_tree |
98 | root = Tree::Node.new |
99 | whitespace |
100 | rules root |
101 | expand_commas root |
102 | parent_ref_rules root |
103 | remove_parent_refs root |
104 | flatten_rules root |
105 | fold_commas root |
106 | root |
107 | end |
108 | |
109 | # Parses a set of CSS rules. |
110 | # |
111 | # @param root [Tree::Node] The parent node of the rules |
112 | def rules(root) |
113 | while r = rule |
114 | root << r |
115 | whitespace |
116 | end |
117 | end |
118 | |
119 | # Parses a single CSS rule. |
120 | # |
121 | # @return [Tree::Node] The parsed rule |
122 | def rule |
123 | rule = "" |
124 | loop do |
125 | token = @template.scan(/(?:[^\{\};\/\s]|\/[^*])+/) |
126 | if token.nil? |
127 | return if rule.empty? |
128 | break |
129 | end |
130 | rule << token |
131 | break unless @template.match?(/\s|\/\*/) |
132 | whitespace |
133 | rule << " " |
134 | end |
135 | |
136 | rule.strip! |
137 | directive = rule[0] == ?@ |
138 | |
139 | if directive |
140 | node = Tree::DirectiveNode.new(rule) |
141 | return node if @template.scan(/;/) |
142 | |
143 | assert_match /\{/ |
144 | whitespace |
145 | |
146 | rules(node) |
147 | return node |
148 | end |
149 | |
150 | assert_match /\{/ |
151 | node = Tree::RuleNode.new(rule) |
152 | properties(node) |
153 | return node |
154 | end |
155 | |
156 | # Parses a set of CSS properties within a rule. |
157 | # |
158 | # @param rule [Tree::RuleNode] The parent node of the properties |
159 | def properties(rule) |
160 | while @template.scan(/[^:\}\s]+/) |
161 | name = @template[0] |
162 | whitespace |
163 | |
164 | assert_match /:/ |
165 | |
166 | value = '' |
167 | while @template.scan(/[^;\s\}]+/) |
168 | value << @template[0] << whitespace |
169 | end |
170 | |
171 | assert_match /(;|(?=\}))/ |
172 | rule << Tree::PropNode.new(name, value, nil) |
173 | end |
174 | |
175 | assert_match /\}/ |
176 | end |
177 | |
178 | # Moves the scanner over a section of whitespace or comments. |
179 | # |
180 | # @return [String] The ignored whitespace |
181 | def whitespace |
182 | space = @template.scan(/\s*/) || '' |
183 | |
184 | # If we've hit a comment, |
185 | # go past it and look for more whitespace |
186 | if @template.scan(/\/\*/) |
187 | @template.scan_until(/\*\//) |
188 | return space + whitespace |
189 | end |
190 | return space |
191 | end |
192 | |
193 | # Moves the scanner over a regular expression, |
194 | # raising an exception if it doesn't match. |
195 | # |
196 | # @param re [Regexp] The regular expression to assert |
197 | def assert_match(re) |
198 | if @template.scan(re) |
199 | whitespace |
200 | return |
201 | end |
202 | |
203 | line = @template.string[0..@template.pos].count "\n" |
204 | pos = @template.pos |
205 | |
206 | after = @template.string[pos - 15...pos] |
207 | after = "..." + after if pos >= 15 |
208 | |
209 | # Display basic regexps as plain old strings |
210 | expected = re.source == Regexp.escape(re.source) ? "\"#{re.source}\"" : re.inspect |
211 | |
212 | was = @template.rest[0...15] |
213 | was += "..." if @template.rest.size >= 15 |
214 | raise Exception.new(<<MESSAGE) |
215 | Invalid CSS on line #{line + 1} after #{after.inspect}: |
216 | expected #{expected}, was #{was.inspect} |
217 | MESSAGE |
218 | end |
219 | |
220 | # Transform |
221 | # |
222 | # foo, bar, baz |
223 | # color: blue |
224 | # |
225 | # into |
226 | # |
227 | # foo |
228 | # color: blue |
229 | # bar |
230 | # color: blue |
231 | # baz |
232 | # color: blue |
233 | # |
234 | # @param root [Tree::Node] The parent node |
235 | def expand_commas(root) |
236 | root.children.map! do |child| |
237 | next child unless Tree::RuleNode === child && child.rules.first.include?(',') |
238 | child.rules.first.split(',').map do |rule| |
239 | node = Tree::RuleNode.new(rule.strip) |
240 | node.children = child.children |
241 | node |
242 | end |
243 | end |
244 | root.children.flatten! |
245 | end |
246 | |
247 | # Make rules use parent refs so that |
248 | # |
249 | # foo |
250 | # color: green |
251 | # foo.bar |
252 | # color: blue |
253 | # |
254 | # becomes |
255 | # |
256 | # foo |
257 | # color: green |
258 | # &.bar |
259 | # color: blue |
260 | # |
261 | # This has the side effect of nesting rules, |
262 | # so that |
263 | # |
264 | # foo |
265 | # color: green |
266 | # foo bar |
267 | # color: red |
268 | # foo baz |
269 | # color: blue |
270 | # |
271 | # becomes |
272 | # |
273 | # foo |
274 | # color: green |
275 | # & bar |
276 | # color: red |
277 | # & baz |
278 | # color: blue |
279 | # |
280 | # @param root [Tree::Node] The parent node |
281 | def parent_ref_rules(root) |
282 | current_rule = nil |
283 | root.children.select { |c| Tree::RuleNode === c }.each do |child| |
284 | root.children.delete child |
285 | first, rest = child.rules.first.scan(/^(&?(?: .|[^ ])[^.#: \[]*)([.#: \[].*)?$/).first |
286 | |
287 | if current_rule.nil? || current_rule.rules.first != first |
288 | current_rule = Tree::RuleNode.new(first) |
289 | root << current_rule |
290 | end |
291 | |
292 | if rest |
293 | child.rules = ["&" + rest] |
294 | current_rule << child |
295 | else |
296 | current_rule.children += child.children |
297 | end |
298 | end |
299 | |
300 | root.children.each { |v| parent_ref_rules(v) } |
301 | end |
302 | |
303 | # Remove useless parent refs so that |
304 | # |
305 | # foo |
306 | # & bar |
307 | # color: blue |
308 | # |
309 | # becomes |
310 | # |
311 | # foo |
312 | # bar |
313 | # color: blue |
314 | # |
315 | # @param root [Tree::Node] The parent node |
316 | def remove_parent_refs(root) |
317 | root.children.each do |child| |
318 | if child.is_a?(Tree::RuleNode) |
319 | child.rules.first.gsub! /^& +/, '' |
320 | remove_parent_refs child |
321 | end |
322 | end |
323 | end |
324 | |
325 | # Flatten rules so that |
326 | # |
327 | # foo |
328 | # bar |
329 | # color: red |
330 | # |
331 | # becomes |
332 | # |
333 | # foo bar |
334 | # color: red |
335 | # |
336 | # and |
337 | # |
338 | # foo |
339 | # &.bar |
340 | # color: blue |
341 | # |
342 | # becomes |
343 | # |
344 | # foo.bar |
345 | # color: blue |
346 | # |
347 | # @param root [Tree::Node] The parent node |
348 | def flatten_rules(root) |
349 | root.children.each { |child| flatten_rule(child) if child.is_a?(Tree::RuleNode) } |
350 | end |
351 | |
352 | # Flattens a single rule |
353 | # |
354 | # @param rule [Tree::RuleNode] The candidate for flattening |
355 | # @see #flatten_rules |
356 | def flatten_rule(rule) |
357 | while rule.children.size == 1 && rule.children.first.is_a?(Tree::RuleNode) |
358 | child = rule.children.first |
359 | |
360 | if child.rules.first[0] == ?& |
361 | rule.rules = [child.rules.first.gsub(/^&/, rule.rules.first)] |
362 | else |
363 | rule.rules = ["#{rule.rules.first} #{child.rules.first}"] |
364 | end |
365 | |
366 | rule.children = child.children |
367 | end |
368 | |
369 | flatten_rules(rule) |
370 | end |
371 | |
372 | # Transform |
373 | # |
374 | # foo |
375 | # bar |
376 | # color: blue |
377 | # baz |
378 | # color: blue |
379 | # |
380 | # into |
381 | # |
382 | # foo |
383 | # bar, baz |
384 | # color: blue |
385 | # |
386 | # @param rule [Tree::RuleNode] The candidate for flattening |
387 | def fold_commas(root) |
388 | prev_rule = nil |
389 | root.children.map! do |child| |
390 | next child unless child.is_a?(Tree::RuleNode) |
391 | |
392 | if prev_rule && prev_rule.children == child.children |
393 | prev_rule.rules.first << ", #{child.rules.first}" |
394 | next nil |
395 | end |
396 | |
397 | fold_commas(child) |
398 | prev_rule = child |
399 | child |
400 | end |
401 | root.children.compact! |
402 | end |
403 | end |
404 | end |
405 |