railsdog-less 1.2.17

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.
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