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:
- 6969 Bytes
1 | require 'sass/script/lexer' |
2 | |
3 | module Sass |
4 | module Script |
5 | # The parser for SassScript. |
6 | # It parses a string of code into a tree of {Script::Node}s. |
7 | class Parser |
8 | # @param str [String, StringScanner] The source text to parse |
9 | # @param line [Fixnum] The line on which the SassScript appears. |
10 | # Used for error reporting |
11 | # @param offset [Fixnum] The number of characters in on which the SassScript appears. |
12 | # Used for error reporting |
13 | # @param filename [String] The name of the file in which the SassScript appears. |
14 | # Used for error reporting |
15 | def initialize(str, line, offset, filename = nil) |
16 | @filename = filename |
17 | @lexer = Lexer.new(str, line, offset, filename) |
18 | end |
19 | |
20 | # Parses a SassScript expression within an interpolated segment (`#{}`). |
21 | # This means that it stops when it comes across an unmatched `}`, |
22 | # which signals the end of an interpolated segment, |
23 | # it returns rather than throwing an error. |
24 | # |
25 | # @return [Script::Node] The root node of the parse tree |
26 | # @raise [Sass::SyntaxError] if the expression isn't valid SassScript |
27 | def parse_interpolated |
28 | expr = assert_expr :expr |
29 | assert_tok :end_interpolation |
30 | expr |
31 | end |
32 | |
33 | # Parses a SassScript expression. |
34 | # |
35 | # @return [Script::Node] The root node of the parse tree |
36 | # @raise [Sass::SyntaxError] if the expression isn't valid SassScript |
37 | def parse |
38 | expr = assert_expr :expr |
39 | assert_done |
40 | expr |
41 | end |
42 | |
43 | # Parses the argument list for a mixin include. |
44 | # |
45 | # @return [Array<Script::Node>] The root nodes of the arguments. |
46 | # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript |
47 | def parse_mixin_include_arglist |
48 | args = [] |
49 | |
50 | if try_tok(:lparen) |
51 | args = arglist || args |
52 | assert_tok(:rparen) |
53 | end |
54 | assert_done |
55 | |
56 | args |
57 | end |
58 | |
59 | # Parses the argument list for a mixin definition. |
60 | # |
61 | # @return [Array<Script::Node>] The root nodes of the arguments. |
62 | # @raise [Sass::SyntaxError] if the argument list isn't valid SassScript |
63 | def parse_mixin_definition_arglist |
64 | args = [] |
65 | |
66 | if try_tok(:lparen) |
67 | args = defn_arglist(false) || args |
68 | assert_tok(:rparen) |
69 | end |
70 | assert_done |
71 | |
72 | args |
73 | end |
74 | |
75 | # Parses a SassScript expression. |
76 | # |
77 | # @overload parse(str, line, offset, filename = nil) |
78 | # @return [Script::Node] The root node of the parse tree |
79 | # @see Parser#initialize |
80 | # @see Parser#parse |
81 | def self.parse(*args) |
82 | new(*args).parse |
83 | end |
84 | |
85 | class << self |
86 | private |
87 | |
88 | # Defines a simple left-associative production. |
89 | # name is the name of the production, |
90 | # sub is the name of the production beneath it, |
91 | # and ops is a list of operators for this precedence level |
92 | def production(name, sub, *ops) |
93 | class_eval <<RUBY |
94 | def #{name} |
95 | return unless e = #{sub} |
96 | while tok = try_tok(#{ops.map {|o| o.inspect}.join(', ')}) |
97 | e = Operation.new(e, assert_expr(#{sub.inspect}), tok.type) |
98 | end |
99 | e |
100 | end |
101 | RUBY |
102 | end |
103 | |
104 | def unary(op, sub) |
105 | class_eval <<RUBY |
106 | def unary_#{op} |
107 | return #{sub} unless try_tok(:#{op}) |
108 | UnaryOperation.new(assert_expr(:unary_#{op}), :#{op}) |
109 | end |
110 | RUBY |
111 | end |
112 | end |
113 | |
114 | private |
115 | |
116 | production :expr, :concat, :comma |
117 | |
118 | def concat |
119 | return unless e = or_expr |
120 | while sub = or_expr |
121 | e = Operation.new(e, sub, :concat) |
122 | end |
123 | e |
124 | end |
125 | |
126 | production :or_expr, :and_expr, :or |
127 | production :and_expr, :eq_or_neq, :and |
128 | production :eq_or_neq, :relational, :eq, :neq |
129 | production :relational, :plus_or_minus, :gt, :gte, :lt, :lte |
130 | production :plus_or_minus, :times_div_or_mod, :plus, :minus |
131 | production :times_div_or_mod, :unary_minus, :times, :div, :mod |
132 | |
133 | unary :minus, :unary_div |
134 | unary :div, :unary_not # For strings, so /foo/bar works |
135 | unary :not, :funcall |
136 | |
137 | def funcall |
138 | return paren unless name = try_tok(:ident) |
139 | # An identifier without arguments is just a string |
140 | unless try_tok(:lparen) |
141 | warn(<<END) |
142 | DEPRECATION WARNING: |
143 | On line #{name.line}, character #{name.offset}#{" of '#{@filename}'" if @filename} |
144 | Implicit strings have been deprecated and will be removed in version 3.0. |
145 | '#{name.value}' was not quoted. Please add double quotes (e.g. "#{name.value}"). |
146 | END |
147 | Script::String.new(name.value) |
148 | else |
149 | args = arglist || [] |
150 | assert_tok(:rparen) |
151 | Script::Funcall.new(name.value, args) |
152 | end |
153 | end |
154 | |
155 | def defn_arglist(must_have_default) |
156 | return unless c = try_tok(:const) |
157 | var = Script::Variable.new(c.value) |
158 | if try_tok(:single_eq) |
159 | val = assert_expr(:concat) |
160 | elsif must_have_default |
161 | raise SyntaxError.new("Required argument #{var.inspect} must come before any optional arguments.", @line) |
162 | end |
163 | |
164 | return [[var, val]] unless try_tok(:comma) |
165 | [[var, val], *defn_arglist(val)] |
166 | end |
167 | |
168 | def arglist |
169 | return unless e = concat |
170 | return [e] unless try_tok(:comma) |
171 | [e, *arglist] |
172 | end |
173 | |
174 | def paren |
175 | return variable unless try_tok(:lparen) |
176 | e = assert_expr(:expr) |
177 | assert_tok(:rparen) |
178 | return e |
179 | end |
180 | |
181 | def variable |
182 | return string unless c = try_tok(:const) |
183 | Variable.new(c.value) |
184 | end |
185 | |
186 | def string |
187 | return literal unless first = try_tok(:string) |
188 | return first.value unless try_tok(:begin_interpolation) |
189 | mid = parse_interpolated |
190 | last = assert_expr(:string) |
191 | Operation.new(first.value, Operation.new(mid, last, :plus), :plus) |
192 | end |
193 | |
194 | def literal |
195 | (t = try_tok(:number, :color, :bool)) && (return t.value) |
196 | end |
197 | |
198 | # It would be possible to have unified #assert and #try methods, |
199 | # but detecting the method/token difference turns out to be quite expensive. |
200 | |
201 | def assert_expr(name) |
202 | (e = send(name)) && (return e) |
203 | raise Sass::SyntaxError.new("Expected expression, was #{@lexer.done? ? 'end of text' : "#{@lexer.peek.type} token"}.") |
204 | end |
205 | |
206 | def assert_tok(*names) |
207 | (t = try_tok(*names)) && (return t) |
208 | raise Sass::SyntaxError.new("Expected #{names.join(' or ')} token, was #{@lexer.done? ? 'end of text' : "#{@lexer.peek.type} token"}.") |
209 | end |
210 | |
211 | def try_tok(*names) |
212 | peeked = @lexer.peek |
213 | peeked && names.include?(peeked.type) && @lexer.next |
214 | end |
215 | |
216 | def assert_done |
217 | return if @lexer.done? |
218 | raise Sass::SyntaxError.new("Unexpected #{@lexer.peek.type} token.") |
219 | end |
220 | end |
221 | end |
222 | end |