railsdog-less 1.2.17

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