unboxed-less 1.2.12

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.gitignore +4 -0
  2. data/CHANGELOG +62 -0
  3. data/LICENSE +179 -0
  4. data/README.md +48 -0
  5. data/Rakefile +60 -0
  6. data/VERSION +1 -0
  7. data/bin/lessc +92 -0
  8. data/lib/ext.rb +31 -0
  9. data/lib/less.rb +32 -0
  10. data/lib/less/command.rb +98 -0
  11. data/lib/less/engine.rb +55 -0
  12. data/lib/less/engine/grammar/common.tt +29 -0
  13. data/lib/less/engine/grammar/entity.tt +130 -0
  14. data/lib/less/engine/grammar/less.tt +326 -0
  15. data/lib/less/engine/nodes.rb +9 -0
  16. data/lib/less/engine/nodes/element.rb +278 -0
  17. data/lib/less/engine/nodes/entity.rb +79 -0
  18. data/lib/less/engine/nodes/function.rb +84 -0
  19. data/lib/less/engine/nodes/literal.rb +171 -0
  20. data/lib/less/engine/nodes/property.rb +231 -0
  21. data/lib/less/engine/nodes/ruleset.rb +12 -0
  22. data/lib/less/engine/nodes/selector.rb +44 -0
  23. data/spec/command_spec.rb +102 -0
  24. data/spec/css/accessors.css +18 -0
  25. data/spec/css/big.css +3768 -0
  26. data/spec/css/colors.css +14 -0
  27. data/spec/css/comments.css +9 -0
  28. data/spec/css/css-3.css +17 -0
  29. data/spec/css/css.css +50 -0
  30. data/spec/css/functions.css +6 -0
  31. data/spec/css/import.css +12 -0
  32. data/spec/css/lazy-eval.css +1 -0
  33. data/spec/css/mixins-args.css +32 -0
  34. data/spec/css/mixins.css +28 -0
  35. data/spec/css/operations.css +28 -0
  36. data/spec/css/parens.css +20 -0
  37. data/spec/css/rulesets.css +17 -0
  38. data/spec/css/scope.css +11 -0
  39. data/spec/css/selectors.css +13 -0
  40. data/spec/css/strings.css +12 -0
  41. data/spec/css/variables.css +7 -0
  42. data/spec/css/whitespace.css +7 -0
  43. data/spec/engine_spec.rb +112 -0
  44. data/spec/less/accessors.less +20 -0
  45. data/spec/less/big.less +4810 -0
  46. data/spec/less/colors.less +35 -0
  47. data/spec/less/comments.less +46 -0
  48. data/spec/less/css-3.less +45 -0
  49. data/spec/less/css.less +104 -0
  50. data/spec/less/exceptions/mixed-units-error.less +3 -0
  51. data/spec/less/exceptions/name-error-1.0.less +3 -0
  52. data/spec/less/exceptions/syntax-error-1.0.less +3 -0
  53. data/spec/less/functions.less +6 -0
  54. data/spec/less/hidden.less +25 -0
  55. data/spec/less/import.less +8 -0
  56. data/spec/less/import/import-test-a.less +2 -0
  57. data/spec/less/import/import-test-b.less +8 -0
  58. data/spec/less/import/import-test-c.less +7 -0
  59. data/spec/less/import/import-test-d.css +1 -0
  60. data/spec/less/lazy-eval.less +6 -0
  61. data/spec/less/literal-css.less +11 -0
  62. data/spec/less/mixins-args.less +59 -0
  63. data/spec/less/mixins.less +43 -0
  64. data/spec/less/operations.less +39 -0
  65. data/spec/less/parens.less +26 -0
  66. data/spec/less/rulesets.less +30 -0
  67. data/spec/less/scope.less +32 -0
  68. data/spec/less/selectors.less +24 -0
  69. data/spec/less/strings.less +14 -0
  70. data/spec/less/variables.less +24 -0
  71. data/spec/less/whitespace.less +34 -0
  72. data/spec/spec.css +50 -0
  73. data/spec/spec_helper.rb +8 -0
  74. data/unboxed-less.gemspec +121 -0
  75. metadata +140 -0
@@ -0,0 +1,31 @@
1
+ module Treetop
2
+ module Runtime
3
+ class CompiledParser
4
+ def failure_message color
5
+ o = color ? Mutter.new.clear : lambda {|i, *args| i }
6
+ return nil unless (tf = terminal_failures) && tf.size > 0
7
+ msg = "on line #{failure_line}: expected " + (
8
+ tf.size == 1 ?
9
+ o[tf[0].expected_string, :yellow] :
10
+ "one of #{o[tf.map {|f| f.expected_string }.uniq * ' ', :yellow]}"
11
+ )
12
+ f = input[failure_index]
13
+ got = case f
14
+ when "\n" then o['\n', :cyan]
15
+ when nil then o["EOF", :cyan]
16
+ when ' ' then o["white-space", :cyan]
17
+ else o[f.chr, :yellow]
18
+ end
19
+ msg += " got #{got} after:\n\n#{input[index...failure_index]}\n"
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ unless :symbol.respond_to?(:to_proc)
26
+ class Symbol
27
+ def to_proc
28
+ Proc.new {|*args| args.shift.__send__(self, *args) }
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,32 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'cgi'
4
+ require 'treetop'
5
+ require 'delegate'
6
+
7
+ LESS_ROOT = File.expand_path(File.dirname(__FILE__))
8
+ LESS_PARSER = File.join(LESS_ROOT, 'less', 'engine', 'parser.rb')
9
+ LESS_GRAMMAR = File.join(LESS_ROOT, 'less', 'engine', 'grammar')
10
+
11
+ require 'ext'
12
+ require 'less/command'
13
+ require 'less/engine'
14
+
15
+ module Less
16
+ MixedUnitsError = Class.new(RuntimeError)
17
+ PathError = Class.new(RuntimeError)
18
+ VariableNameError = Class.new(NameError)
19
+ MixinNameError = Class.new(NameError)
20
+ SyntaxError = Class.new(RuntimeError)
21
+ ImportError = Class.new(RuntimeError)
22
+
23
+ $verbose = false
24
+
25
+ def self.version
26
+ File.read( File.join( File.dirname(__FILE__), '..', 'VERSION') ).strip
27
+ end
28
+
29
+ def self.parse less
30
+ Engine.new(less).to_css
31
+ end
32
+ end
@@ -0,0 +1,98 @@
1
+ module Less
2
+ class Command
3
+ attr_accessor :source, :destination, :options
4
+
5
+ def initialize options
6
+ $verbose = options[:debug]
7
+ @source = options[:source]
8
+ @destination = (options[:destination] || options[:source]).gsub /\.(less|lss)/, '.css'
9
+ @options = options
10
+ end
11
+
12
+ def watch?() @options[:watch] end
13
+ def compress?() @options[:compress] end
14
+ def debug?() @options[:debug] end
15
+
16
+ # little function which allows us to
17
+ # Ctrl-C exit inside the passed block
18
+ def watch
19
+ begin
20
+ yield
21
+ rescue Interrupt
22
+ puts
23
+ exit 0
24
+ end
25
+ end
26
+
27
+ def run!
28
+ if watch?
29
+ parse(true) unless File.exist? @destination
30
+
31
+ log "Watching for changes in #@source... Ctrl-C to abort.\n: "
32
+
33
+ # Main watch loop
34
+ loop do
35
+ watch { sleep 1 }
36
+
37
+ # File has changed
38
+ if File.stat( @source ).mtime > File.stat( @destination ).mtime
39
+ print "Change detected... "
40
+
41
+ # Loop until error is fixed
42
+ until parse
43
+ log "Press [return] to continue..."
44
+ watch { $stdin.gets }
45
+ end
46
+ end
47
+ end
48
+ else
49
+ parse
50
+ end
51
+ end
52
+
53
+ def parse is_new = false
54
+ begin
55
+ # Create a new Less object with the contents of a file
56
+ css = Less::Engine.new(File.new(@source), @options).to_css
57
+ css = css.delete " \n" if compress?
58
+
59
+ File.open( @destination, "w" ) do |file|
60
+ file.write css
61
+ end
62
+ print "* #{is_new ? 'Created' : 'Updated'} " +
63
+ "#{@destination.split('/').last}\n: " if watch?
64
+ rescue Errno::ENOENT => e
65
+ abort "#{e}"
66
+ rescue SyntaxError => e
67
+ err "#{e}\n", "Syntax"
68
+ rescue MixedUnitsError => e
69
+ err "`#{e}` you're mixing units together! What do you expect?\n", "Mixed Units"
70
+ rescue PathError => e
71
+ err "`#{e}` was not found.\n", "Path"
72
+ rescue VariableNameError => e
73
+ err "#{e} is undefined.\n", "Variable Name"
74
+ rescue MixinNameError => e
75
+ err "#{e} is undefined.\n", "Mixin Name"
76
+ else
77
+ true
78
+ end
79
+ end
80
+
81
+ # Just a logging function to avoid typing '*'
82
+ def log s = ''
83
+ print '* ' + s.to_s
84
+ end
85
+
86
+ def err s = '', type = ''
87
+ type = type.strip + ' ' unless type.empty?
88
+ $stderr.print "! #{type}Error: #{s}"
89
+ if @options[:growl]
90
+ growl = Growl.new
91
+ growl.title = "LESS"
92
+ growl.message = "#{type}Error in #@source!"
93
+ growl.run
94
+ false
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,55 @@
1
+ $:.unshift File.dirname(__FILE__)
2
+
3
+ require 'engine/nodes'
4
+
5
+ begin
6
+ require 'engine/parser'
7
+ rescue LoadError
8
+ Treetop.load File.join(LESS_GRAMMAR, 'common.tt')
9
+ Treetop.load File.join(LESS_GRAMMAR, 'entity.tt')
10
+ Treetop.load File.join(LESS_GRAMMAR, 'less.tt')
11
+ end
12
+
13
+ module Less
14
+ class Engine
15
+ attr_reader :css, :less
16
+
17
+ def initialize obj, options = {}
18
+ @less = if obj.is_a? File
19
+ @path = File.dirname File.expand_path(obj.path)
20
+ obj.read
21
+ elsif obj.is_a? String
22
+ obj.dup
23
+ else
24
+ raise ArgumentError, "argument must be an instance of File or String!"
25
+ end
26
+
27
+ @options = options
28
+ @parser = StyleSheetParser.new
29
+ end
30
+
31
+ def parse build = true, env = Node::Element.new
32
+ root = @parser.parse(self.prepare)
33
+
34
+ return root unless build
35
+
36
+ if root
37
+ env.file = @path
38
+ @tree = root.build env
39
+ else
40
+ raise SyntaxError, @parser.failure_message(@options[:color])
41
+ end
42
+
43
+ @tree
44
+ end
45
+ alias :to_tree :parse
46
+
47
+ def to_css
48
+ @css || @css = self.parse.group.to_css
49
+ end
50
+
51
+ def prepare
52
+ @less.gsub(/\r\n/, "\n").gsub(/\t/, ' ')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,29 @@
1
+ module Less
2
+ module StyleSheet
3
+ grammar Common
4
+ #
5
+ # Whitespace
6
+ #
7
+ rule s
8
+ [ ]*
9
+ end
10
+
11
+ rule S
12
+ [ ]+
13
+ end
14
+
15
+ rule ws
16
+ [\n ]*
17
+ end
18
+
19
+ rule WS
20
+ [\n ]+
21
+ end
22
+
23
+ # Non-space char
24
+ rule ns
25
+ ![ ;,!})\n] .
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,130 @@
1
+ module Less
2
+ module StyleSheet
3
+ grammar Entity
4
+ #
5
+ # Entity: Any whitespace delimited token
6
+ #
7
+ rule entity
8
+ url / function / accessor / keyword / variable / literal / font
9
+ end
10
+
11
+ rule fonts
12
+ font family:(s ',' s font)+ {
13
+ def build
14
+ Node::FontFamily.new(all.map(&:build))
15
+ end
16
+
17
+ def all
18
+ [font] + family.elements.map {|f| f.font }
19
+ end
20
+ }
21
+ end
22
+
23
+ rule font
24
+ [a-zA-Z] [-a-zA-Z0-9]* !ns {
25
+ def build
26
+ Node::Keyword.new(text_value)
27
+ end
28
+ } / string {
29
+ def build
30
+ Node::Quoted.new(text_value)
31
+ end
32
+ }
33
+ end
34
+
35
+ #
36
+ # Tokens which don't need to be evaluated
37
+ #
38
+ rule literal
39
+ color / (dimension / [-a-z]+) '/' dimension {
40
+ def build
41
+ Node::Anonymous.new(text_value)
42
+ end
43
+ } / number unit {
44
+ def build
45
+ Node::Number.new(number.text_value, unit.text_value)
46
+ end
47
+ } / string {
48
+ def build
49
+ Node::Quoted.new(text_value)
50
+ end
51
+ }
52
+ end
53
+
54
+ #
55
+ # `blue`, `small`, `normal` etc.
56
+ #
57
+ rule keyword
58
+ [-a-zA-Z]+ !ns {
59
+ def build
60
+ Node::Keyword.new(text_value)
61
+ end
62
+ }
63
+ end
64
+
65
+ #
66
+ # 'hello world' / "hello world"
67
+ #
68
+ rule string
69
+ "'" content:(!"'" . )* "'" {
70
+ def value
71
+ content.text_value
72
+ end
73
+ } / ["] content:(!["] . )* ["] {
74
+ def value
75
+ content.text_value
76
+ end
77
+ }
78
+ end
79
+
80
+ #
81
+ # Numbers & Units
82
+ #
83
+ rule dimension
84
+ number unit
85
+ end
86
+
87
+ rule number
88
+ '-'? [0-9]* '.' [0-9]+ / '-'? [0-9]+
89
+ end
90
+
91
+ rule unit
92
+ ('px'/'em'/'pc'/'%'/'ex'/'in'/'deg'/'s'/'pt'/'cm'/'mm')?
93
+ end
94
+
95
+ #
96
+ # Color
97
+ #
98
+ rule color
99
+ '#' rgb {
100
+ def build
101
+ Node::Color.new(*rgb.build)
102
+ end
103
+ } / fn:(('hsl'/'rgb') 'a'?) arguments {
104
+ def build
105
+ Node::Function.new(fn.text_value, arguments.build.flatten)
106
+ end
107
+ }
108
+ end
109
+
110
+ #
111
+ # 00ffdd / 0fd
112
+ #
113
+ rule rgb
114
+ r:(hex hex) g:(hex hex) b:(hex hex) {
115
+ def build
116
+ [r.text_value, g.text_value, b.text_value]
117
+ end
118
+ } / r:hex g:hex b:hex {
119
+ def build
120
+ [r.text_value, g.text_value, b.text_value].map {|c| c * 2 }
121
+ end
122
+ }
123
+ end
124
+
125
+ rule hex
126
+ [a-fA-F0-9]
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,326 @@
1
+ module Less
2
+ grammar StyleSheet
3
+ include Common
4
+ include Entity
5
+
6
+ rule primary
7
+ (import / declaration / mixin / ruleset / comment)* {
8
+ def build env = Less::Element.new
9
+ elements.map do |e|
10
+ e.build env if e.respond_to? :build
11
+ end; env
12
+ end
13
+ }
14
+ end
15
+
16
+ rule comment
17
+ ws '/*' (!'*/' . )* '*/' ws / ws '//' (!"\n" .)* "\n" ws
18
+ end
19
+
20
+ #
21
+ # div, .class, body > p {...}
22
+ #
23
+ rule ruleset
24
+ selectors "{" ws primary ws "}" s hide:(';'?) ws {
25
+ def build env
26
+ # Build the ruleset for each selector
27
+ selectors.build(env, :ruleset).each do |sel|
28
+ sel.hide unless hide.empty?
29
+ primary.build sel
30
+ end
31
+ end
32
+ # Mixin Declaration
33
+ } / '.' name:[-a-zA-Z0-9_]+ ws parameters ws "{" ws primary ws "}" ws {
34
+ def build env
35
+ env << Node::Mixin::Def.new(name.text_value, parameters.build(env))
36
+ primary.build env.last
37
+ #env.last
38
+ end
39
+ }
40
+ end
41
+
42
+ rule mixin
43
+ name:('.' [-a-zA-Z0-9_]+) args:(arguments) s ';' ws {
44
+ def build env
45
+ definition = env.nearest(name.text_value, :mixin) or raise MixinNameError, name.text_value
46
+ params = args.build.map {|i| Node::Expression.new i } unless args.empty?
47
+ env << Node::Mixin::Call.new(definition, params || [], env)
48
+ end
49
+ } / ws selectors ';' ws {
50
+ def build env
51
+ selectors.build(env, :mixin).each do |path|
52
+ rules = path.inject(env.root) do |current, node|
53
+ current.descend(node.selector, node) or raise MixinNameError, selectors.text_value
54
+ end.rules
55
+ env.rules += rules
56
+ #env.mix(rules)
57
+ end
58
+ end
59
+ }
60
+ end
61
+
62
+ rule selectors
63
+ ws selector tail:(s ',' ws selector)* ws {
64
+ def build env, method
65
+ all.map do |e|
66
+ e.send(method, env) if e.respond_to? method
67
+ end.compact
68
+ end
69
+
70
+ def all
71
+ [selector] + tail.elements.map {|e| e.selector }
72
+ end
73
+ }
74
+ end
75
+
76
+ #
77
+ # div > p a {...}
78
+ #
79
+ rule selector
80
+ sel:(s select element s)+ '' {
81
+ def ruleset env
82
+ sel.elements.inject(env) do |node, e|
83
+ node << Node::Element.new(e.element.text_value, e.select.text_value)
84
+ node.last
85
+ end
86
+ end
87
+
88
+ def mixin env
89
+ sel.elements.map do |e|
90
+ Node::Element.new(e.element.text_value, e.select.text_value)
91
+ end
92
+ end
93
+ }
94
+ end
95
+
96
+ rule parameters
97
+ '(' s ')' {
98
+ def build env
99
+ []
100
+ end
101
+ } / '(' parameter tail:(s ',' s parameter)* ')' {
102
+ def build env
103
+ all.map do |e|
104
+ e.build(env)
105
+ end
106
+ end
107
+
108
+ def all
109
+ [parameter] + tail.elements.map {|e| e.parameter }
110
+ end
111
+ }
112
+ end
113
+
114
+ rule parameter
115
+ variable s ':' s expressions {
116
+ def build env
117
+ Node::Variable.new(variable.text_value, expressions.build(env), env)
118
+ end
119
+ }
120
+ end
121
+
122
+ rule import
123
+ ws "@import" S url:(string / url) medias? s ';' ws {
124
+ def build env
125
+ path = File.join(env.root.file || Dir.pwd, url.value)
126
+ path += '.less' unless path =~ /\.(le|c)ss$/
127
+ if File.exist? path
128
+ unless env.root.imported.include?(path)
129
+ env.root.imported << path
130
+ env.rules += Less::Engine.new(File.new(path)).to_tree.rules
131
+ end
132
+ else
133
+ raise ImportError, path
134
+ end
135
+ end
136
+ }
137
+ end
138
+
139
+ rule url
140
+ 'url(' path:(string / [-a-zA-Z0-9_%$/.&=:;#+?]+) ')' {
141
+ def build env = nil
142
+ Node::Function.new('url', value)
143
+ end
144
+
145
+ def value
146
+ Node::Quoted.new CGI.unescape(path.text_value)
147
+ end
148
+ }
149
+ end
150
+
151
+ rule medias
152
+ [-a-z]+ (s ',' s [a-z]+)*
153
+ end
154
+
155
+ #
156
+ # @my-var: 12px;
157
+ # height: 100%;
158
+ #
159
+ rule declaration
160
+ ws name:(ident / variable) s ':' s expressions tail:(ws ',' ws expressions)* s (';'/ ws &'}') ws {
161
+ def build env
162
+ result = all.map {|e| e.build(env) if e.respond_to? :build }.compact
163
+ env << (name.text_value =~ /^@/ ?
164
+ Node::Variable : Node::Property).new(name.text_value, result, env)
165
+ end
166
+
167
+ def all
168
+ [expressions] + tail.elements.map {|f| f.expressions }
169
+ end
170
+ # Empty rule
171
+ } / ws ident s ':' s ';' ws
172
+ end
173
+
174
+ #
175
+ # An operation or compound value
176
+ #
177
+ rule expressions
178
+ # Operation
179
+ expression tail:(operator expression)+ {
180
+ def build env = nil
181
+ ret = all.map {|e| e.build(env) }.flatten.compact
182
+ ret.size == 1 ? ret.first : ret
183
+ end
184
+
185
+ def all
186
+ [expression] + tail.elements.map {|i| [i.operator, i.expression] }.flatten.compact
187
+ end
188
+ # Space-delimited expressions
189
+ } / expression tail:(WS expression)* i:important? {
190
+ def build env = nil
191
+ all.map {|e| e.build(env) if e.respond_to? :build }.compact
192
+ end
193
+
194
+ def all
195
+ [expression] + tail.elements.map {|f| f.expression } + [i]
196
+ end
197
+ # Catch-all rule
198
+ } / [-a-zA-Z0-9_%*/.&=:,#+? \[\]()]+ {
199
+ def build env
200
+ [Node::Anonymous.new(text_value)]
201
+ end
202
+ }
203
+ end
204
+
205
+ rule expression
206
+ '(' s expressions s ')' {
207
+ def build env = nil
208
+ Node::Expression.new(['('] + expressions.build(env).flatten + [')'])
209
+ end
210
+ } / entity '' {
211
+ def build env = nil
212
+ e = entity.method(:build).arity.zero?? entity.build : entity.build(env)
213
+ if e.is_a?( Array )
214
+ e = e.flatten.compact
215
+ e = e.size == 1 ? e.first : e
216
+ end
217
+ e
218
+ end
219
+ }
220
+ end
221
+
222
+ # !important
223
+ rule important
224
+ s '!' s 'important' {
225
+ def build env = nil
226
+ Node::Keyword.new(text_value.strip)
227
+ end
228
+ }
229
+ end
230
+
231
+ #
232
+ # An identifier
233
+ #
234
+ rule ident
235
+ '*'? '-'? [-a-z_] [-a-z0-9_]*
236
+ end
237
+
238
+ rule variable
239
+ '@' [-a-zA-Z0-9_]+ {
240
+ def build
241
+ Node::Variable.new(text_value)
242
+ end
243
+ }
244
+ end
245
+
246
+ #
247
+ # div / .class / #id / input[type="text"] / lang(fr)
248
+ #
249
+ rule element
250
+ ((class / id / tag / ident) attribute* ('(' (selector / number) ')')?)+ / attribute+ / '@media' / '@font-face'
251
+ end
252
+
253
+ #
254
+ # [type="text"]
255
+ #
256
+ rule attribute
257
+ '[' tag ([|~*$^]? '=') (string / [-a-zA-Z_0-9]+) ']' / '[' (tag / string) ']'
258
+ end
259
+
260
+ rule class
261
+ '.' [_a-zA-Z] [-a-zA-Z0-9_]*
262
+ end
263
+
264
+ rule id
265
+ '#' [_a-zA-Z] [-a-zA-Z0-9_]*
266
+ end
267
+
268
+ rule tag
269
+ [a-zA-Z] [-a-zA-Z]* [0-9]? / '*'
270
+ end
271
+
272
+ rule select
273
+ (s [+>~] s / '::' / s ':' / S)?
274
+ end
275
+
276
+ # TODO: Merge this with attribute rule
277
+ rule accessor
278
+ ident:(class / id / tag) '[' attr:(string / variable) ']' {
279
+ def build env
280
+ env.nearest(ident.text_value)[attr.text_value.delete(%q["'])].evaluate
281
+ end
282
+ }
283
+ end
284
+
285
+ rule operator
286
+ S [-+*/] S {
287
+ def build env
288
+ Node::Operator.new(text_value.strip)
289
+ end
290
+ } / [-+*/] {
291
+ def build env
292
+ Node::Operator.new(text_value)
293
+ end
294
+ }
295
+ end
296
+
297
+ #
298
+ # Functions and arguments
299
+ #
300
+ rule function
301
+ name:([-a-zA-Z_]+) arguments {
302
+ def build
303
+ Node::Function.new(name.text_value, arguments.build)
304
+ end
305
+ }
306
+ end
307
+
308
+ rule arguments
309
+ '(' s expressions s tail:(',' s expressions s)* ')' {
310
+ def build
311
+ all.map do |e|
312
+ e.build if e.respond_to? :build
313
+ end.compact
314
+ end
315
+
316
+ def all
317
+ [expressions] + tail.elements.map {|e| e.expressions }
318
+ end
319
+ } / '(' s ')' {
320
+ def build
321
+ []
322
+ end
323
+ }
324
+ end
325
+ end
326
+ end