eggshell 1.0.3 → 1.1.1

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.
@@ -0,0 +1,422 @@
1
+ module Eggshell; class ExpressionEvaluator;
2
+ module Parser
3
+ ST_NULL = 0
4
+ ST_NUM = 1
5
+ ST_STRING = 2
6
+ ST_STRING_EMBED = 4
7
+ ST_STRING_BLOCK = 8
8
+ ST_LABEL = 16
9
+ ST_LABEL_CALL = 17
10
+ ST_LABEL_MEMBER = 19
11
+ ST_OPERATOR = 32
12
+ ST_OPERATOR_TERN = 33
13
+ ST_GROUP = 64
14
+ ST_HASH = 128
15
+ ST_ARRAY = 256
16
+ ST_INDEX_ACCESS = ST_ARRAY|ST_LABEL
17
+
18
+ CONSTS = {
19
+ 'true' => true,
20
+ 'false' => false,
21
+ 'nil' => nil,
22
+ 'null' => nil
23
+ }.freeze
24
+
25
+ # Indicates only one statement allowed
26
+ FL_ONE_STATEMENT = 1
27
+ # Indicates that for an opening brace (e.g. `if (...) {`), collect remaining items after brace.
28
+ FL_BRACE_OP_COLLECT = 2
29
+
30
+ # Builds a parse tree from the lexer.
31
+ class DefaultParser
32
+ def initialize()
33
+ @lexer = Eggshell::ExpressionEvaluator::Lexer.new(self)
34
+ end
35
+
36
+ def reset()
37
+ @tree = []
38
+ @ptr = @tree
39
+ @last_ptr = [@tree]
40
+ @tokens = []
41
+ @state = [ST_NULL]
42
+ @term_last = [nil]
43
+ @term_str = nil
44
+ @quote_delim = nil
45
+ @word_pos = 0
46
+ @expect_label = false
47
+ @array_state = [0]
48
+ @hash_state = [nil]
49
+ @last_comma = false
50
+ end
51
+
52
+ def emit(type, data = nil, ts = nil, te = nil)
53
+ word = data[ts...te].pack('c*')
54
+ @tokens << word # if type != :space
55
+ lst = @state[-1]
56
+
57
+ # before inserting term, need to make sure it follows semantics of function/array
58
+ # @todo look out for case of '(,' or '[,'
59
+ arg_check = true
60
+ if lst == ST_LABEL_CALL
61
+ arg_check = @ptr.length == 0 || @last_comma
62
+ elsif lst == ST_ARRAY
63
+ arg_check = @ptr.length == 1 || @last_comma
64
+ end
65
+ #arg_check = arg_check && (@hash_state[-1] == :colon || @hash_state[-1] == :comma)
66
+ @last_comma = false if type != :space
67
+
68
+ expr_frag = ''
69
+ if @tokens.length > 5
70
+ expr_frag = @tokens[@tokens.length-5..-2].join('')
71
+ else
72
+ expr_frag = @tokens[0..-2].join('')
73
+ end
74
+
75
+ # @todo need to track last literal/var/func for operator syntax check
76
+ if type == :escape
77
+ raise Exception.new("expecting identifier after '#{@ptr[-1][1]}'") if @expect_label
78
+ raise Exception.new("escaping character outside of string") if lst != ST_STRING
79
+ word_unesc = ESCAPE_MAP[word]
80
+ raise Exception.new("invalid escape sequence: #{word}") if !word_unesc
81
+
82
+ @term_str += word_unesc
83
+ return
84
+ elsif type == :str_delim
85
+ raise Exception.new("expecting identifier after '#{@ptr[-1][1]}', not #{word}") if @expect_label
86
+ if !@quote_delim
87
+ @term_last.pop if lst != ST_INDEX_ACCESS
88
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
89
+ if @hash_state[-1] == :key
90
+ @hash_state[-1] = :colon
91
+ elsif @hash_state[-1] == :value
92
+ @hash_state[-1] = :comma
93
+ elsif @hash_state[-1] == :comma
94
+ raise Exception.new("expecting comma after: #{expr_frag}")
95
+ end
96
+ @quote_delim = word
97
+ @state << ST_STRING
98
+ @term_str = ''
99
+ elsif @quote_delim == word
100
+ @quote_delim = nil
101
+ @state.pop
102
+ @ptr << @term_str
103
+ @term_str = nil
104
+ end
105
+ elsif lst == ST_STRING
106
+ @term_str += word
107
+ elsif type == :number_literal
108
+ @term_last.pop if lst != ST_INDEX_ACCESS
109
+ raise Exception.new("expecting identifier after: '#{@ptr[-1][1]}'") if @expect_label
110
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
111
+ @ptr << (word.index('.') ? word.to_f : word.to_i)
112
+
113
+ if @hash_state[-1] == :key
114
+ @hash_state[-1] = :colon
115
+ elsif @hash_state[-1] == :value
116
+ @hash_state[-1] = :comma
117
+ end
118
+ elsif type == :identifier
119
+ if lst == ST_LABEL_MEMBER
120
+ @state.pop
121
+ @ptr << word
122
+ @last_ptr[-1][-1] << @ptr
123
+ @ptr = @last_ptr.pop
124
+ elsif @expect_label
125
+ @ptr[-1][1] += word
126
+ @expect_label = false
127
+ else
128
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
129
+
130
+ if CONSTS.has_key?(word)
131
+ @ptr << CONSTS[word]
132
+ else
133
+ @ptr << [:var, word]
134
+ end
135
+
136
+ if @hash_state[-1] == :key
137
+ @hash_state[-1] = :colon
138
+ elsif @hash_state[-1] == :value
139
+ @hash_state[-1] = :comma
140
+ end
141
+ end
142
+ @term_last << ST_LABEL
143
+ elsif type == :logical_op
144
+ # STORED AS: `[:op, [operand1, operator1, operand2, operator2, ...]]
145
+ # > @last_ptr holds entire structure, @ptr holds structure[1]
146
+ # @todo deal with - prefix
147
+ @term_last.pop
148
+ raise Exception.new("expecting identifier after '#{@ptr[-1][1]}'") if @expect_label
149
+ if word == '?'
150
+ tern = [:op_tern, nil, [], nil]
151
+ tern[1] = @last_ptr[-1].pop
152
+ @last_ptr[-1] << tern
153
+ @ptr = tern[2]
154
+ @state[-1] = ST_OPERATOR_TERN
155
+ elsif lst == ST_OPERATOR
156
+ op = word.to_sym
157
+ prec_l = OPERATOR_MAP[@ptr[-2]]
158
+ prec_r = OPERATOR_MAP[op]
159
+
160
+ if prec_r > prec_l
161
+ # need to group next 2 terms since this operator has higher precedence
162
+ nop = [:op, [@ptr.pop]]
163
+ @state << ST_OPERATOR
164
+ @ptr << nop
165
+ @last_ptr << @ptr
166
+ @ptr = nop[1]
167
+ elsif prec_l > prec_r
168
+ # check if @last_ptr[-1] is an op; pop if so
169
+ type = @last_ptr[-2].is_a?(Array) ? @last_ptr[-2][0][0] : nil
170
+ if type == :op
171
+ @state.pop
172
+ @last_ptr.pop
173
+ @ptr = @last_ptr[-1][0][1]
174
+ end
175
+ end
176
+ @ptr << op
177
+ else
178
+ lele = @ptr.pop
179
+ op = [:op, [lele, word.to_sym]]
180
+ @last_ptr << @ptr
181
+ @ptr << op
182
+ @ptr = op[1]
183
+ @state << ST_OPERATOR
184
+ end
185
+ elsif type == :paren_group
186
+ # STORED AS: [:group, [nested_statement]]
187
+ # STORED AS: [:func, 'funcname', [args*]]
188
+ raise Exception.new("expecting identifier after '#{@ptr[-1][1]}'") if @expect_label
189
+ if word == '('
190
+ if @term_last[-1] == ST_LABEL
191
+ @state << ST_LABEL_CALL
192
+ @ptr[-1][0] = :func
193
+ @ptr[-1][2] = []
194
+
195
+ @last_ptr << @ptr
196
+ @ptr = @ptr[-1][2]
197
+ @expect_separator = ','
198
+ else
199
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
200
+ @state << ST_GROUP
201
+ @last_ptr << @ptr
202
+ @ptr << [:group, []]
203
+ @ptr = @ptr[-1][1]
204
+ end
205
+ @term_last.pop
206
+ else
207
+ do_close = false
208
+ if lst == ST_OPERATOR
209
+ do_close = @state[-2] == ST_GROUP
210
+ if do_close
211
+ @state.pop
212
+ @ptr = @last_ptr.pop[1]
213
+ end
214
+ else
215
+ do_close = lst == ST_LABEL_CALL || lst == ST_GROUP
216
+ end
217
+
218
+ if do_close
219
+ s = @state.pop
220
+ @ptr = @last_ptr.pop
221
+ @expect_separator = nil
222
+
223
+ if @hash_state[-1] == :key
224
+ @hash_state[-1] = :colon
225
+ elsif @hash_state[-1] == :value
226
+ @hash_state[-1] = :comma
227
+ end
228
+ else
229
+ # @todo exception
230
+ end
231
+ end
232
+ elsif type == :brace_group
233
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
234
+ if word == '{'
235
+ if @hash_state[-1] && @hash_state[-1] != :value
236
+ raise Exception.new("invalid hash start")
237
+ end
238
+
239
+ @state << ST_HASH
240
+ @last_ptr << @ptr
241
+ @ptr = [:hash]
242
+ @hash_state << :key
243
+ else
244
+ if lst == ST_HASH
245
+ if @hash_state[-1] != :comma
246
+ msg = "missing value for key #{@ptr[-1]}"
247
+ msg = "missing ':' and a value for key #{@ptr[-1]}" if @hash_state[-1] == :key
248
+ msg = "missing a value for key #{@ptr[-1]}" if @hash_state[-1] == :value
249
+ raise Exception.new(msg)
250
+ end
251
+
252
+ @last_ptr[-1] << @ptr
253
+ @ptr = @last_ptr.pop
254
+ @state.pop
255
+ @hash_state.pop
256
+
257
+ if @hash_state[-1] == :key
258
+ @hash_state[-1] = :colon
259
+ elsif @hash_state[-1] == :value
260
+ @hash_state[-1] = :comma
261
+ end
262
+ else
263
+ # @todo throw exception
264
+ end
265
+ end
266
+ elsif type == :index_group
267
+ if word == '['
268
+ if @term_last[-1] == ST_LABEL
269
+ @state << ST_INDEX_ACCESS
270
+ @last_ptr << @ptr
271
+ @ptr = [:index_access]
272
+ else
273
+ raise Exception.new("missing a comma after: #{expr_frag}") if !arg_check
274
+ # @todo check if array being defined within index_access
275
+ if @hash_state[-1] && @hash_state[-1] != :value
276
+ raise Exception.new("invalid array start near: #{expr_frag}")
277
+ end
278
+
279
+ @ptr << [:array]
280
+ @state << ST_ARRAY
281
+ @last_ptr << @ptr
282
+ @ptr = @ptr[-1]
283
+ end
284
+ else
285
+ if lst == ST_INDEX_ACCESS
286
+ acc = @ptr
287
+ @ptr = @last_ptr.pop
288
+ @ptr[-1] << acc
289
+ @state.pop
290
+ elsif lst == ST_ARRAY
291
+ @ptr = @last_ptr.pop
292
+ @state.pop
293
+
294
+ if @hash_state[-1] == :key
295
+ @hash_state[-1] = :colon
296
+ elsif @hash_state[-1] == :value
297
+ @hash_state[-1] = :comma
298
+ end
299
+ else
300
+ # @todo throw exception
301
+ end
302
+ end
303
+ elsif type == :separator
304
+ # @todo need to ensure proper separation!!!
305
+ if word == '.'
306
+ if @term_last[-1] == ST_LABEL
307
+ @state << ST_LABEL_MEMBER
308
+ @last_ptr << @ptr
309
+ @ptr = [:member_access]
310
+ else
311
+ # @todo throw exception
312
+ end
313
+ elsif word == ','
314
+ if lst == ST_HASH
315
+ if @hash_state[-1] == :comma
316
+ @hash_state[-1] = :key
317
+ else
318
+ raise Exception.new("misplaced comma near: #{expr_frag}")
319
+ end
320
+ elsif lst == ST_ARRAY
321
+ @last_comma = true
322
+ elsif lst == ST_LABEL_CALL
323
+ @last_comma = true
324
+ end
325
+ elsif word == ';'
326
+ end
327
+ elsif type == :modifier
328
+ if word == ':'
329
+ if lst == ST_HASH
330
+ if @hash_state[-1] == :colon
331
+ @hash_state[-1] = :value
332
+ else
333
+ raise Exception.new("misplaced colon near: #{expr_frag} -- #{@hash_state[-1]}")
334
+ end
335
+ elsif lst == ST_OPERATOR_TERN
336
+ # assumes [:op_tern, cond, true, false] structure
337
+ tern = @last_ptr[-1][-1]
338
+ if tern[3] == nil
339
+ tern[3] = []
340
+ @ptr = tern[3]
341
+ else
342
+ # @todo throw exception. more than 1 ':' encountered
343
+ end
344
+ elsif @term_last[-1] == ST_LABEL
345
+ @ptr[-1][1] += ':'
346
+ @expect_label = true
347
+ @term_last[-1] = nil
348
+ end
349
+ end
350
+ elsif type == :space
351
+ else
352
+ puts "woooo"
353
+ end
354
+ end
355
+
356
+ def parse(src, flags = 0)
357
+ reset
358
+ begin
359
+ @lexer.process(src)
360
+ @tree
361
+ rescue => ex
362
+ $stderr.write("parse exception: #{ex}\n")
363
+ $stderr.write("\t#{ex.backtrace.join("\n\t")}\n")
364
+ nil
365
+ end
366
+ end
367
+
368
+ def debug
369
+ puts "STATE : #{@state.inspect}"
370
+ puts "TREE : #{@tree.inspect}"
371
+ puts " PTR : #{@ptr.inspect}"
372
+ puts "TOKENS: #{@tokens.inspect}"
373
+ end
374
+ end
375
+
376
+ def self.reassemble(struct, sep = '')
377
+ buff = []
378
+ s = ''
379
+ struct.each do |ele|
380
+ if ele.is_a?(Array)
381
+ if ele[0] == :op
382
+ buff << reassemble(ele[1])
383
+ elsif ele[0] == :op_tern
384
+ buff << reassemble(ele[1])
385
+ buff << ' ? '
386
+ buff << reassemble(ele[2])
387
+ buff << ' : '
388
+ buff << reassemble(ele[3])
389
+ elsif ele[0] == :func
390
+ buff << ele[1] + '('
391
+ buff << reassemble(ele[2], ',')
392
+ buff << ')'
393
+ elsif ele[0] == :group
394
+ buff << '('
395
+ buff << reassemble(ele[1..-1])
396
+ buff << ')'
397
+ elsif ele[0] == :var
398
+ if ele.length > 2
399
+ buff << reassemble(ele[1..-1])
400
+ else
401
+ buff << ele[1]
402
+ end
403
+ elsif ele[0] == :index_access
404
+ buff << '['
405
+ if ele[1].is_a?(Array)
406
+ buff << reassemble(ele[1])
407
+ else
408
+ buff << ele[1]
409
+ end
410
+ buff << ']'
411
+ end
412
+ else
413
+ buff << s
414
+ buff << (ele.is_a?(String) ? '"' + ele.gsub('"', '\\"') + '"' : ele)
415
+ s = sep
416
+ end
417
+ end
418
+
419
+ buff.join('')
420
+ end
421
+ end
422
+ end; end;
@@ -21,7 +21,6 @@ module Eggshell::MacroHandler
21
21
  include Eggshell::BaseHandler
22
22
  include Eggshell::ProcessHandler
23
23
 
24
-
25
24
  COLLECT_NORMAL = :collect_normal
26
25
  COLLECT_RAW_MACRO = :collect_raw_macro
27
26
  COLLECT_RAW = :collect_raw
@@ -35,6 +34,19 @@ module Eggshell::MacroHandler
35
34
  COLLECT_NORMAL
36
35
  end
37
36
 
37
+ CHAIN_NONE = 0
38
+ CHAIN_START = 1
39
+ CHAIN_CONTINUE = 2
40
+ CHAIN_END = 3
41
+
42
+ # If a sequence of macros are related by conditional execution (if/elsif/else, for instance),
43
+ # this provides a hint to the processor in how to group and evaluate the macros.
44
+ #
45
+ # @return Array In the form `[CHAIN_TYPE, MACRO_START]`
46
+ def chain_type(macro)
47
+ [CHAIN_NONE, nil]
48
+ end
49
+
38
50
  module Defaults
39
51
  class NoOpHandler
40
52
  include Eggshell::MacroHandler
@@ -11,6 +11,7 @@
11
11
  # ]
12
12
  class Eggshell::ParseTree
13
13
  BH = Eggshell::BlockHandler
14
+ MH = Eggshell::MacroHandler
14
15
 
15
16
  IDX_TYPE = 0
16
17
  IDX_NAME = 1
@@ -30,23 +31,23 @@ class Eggshell::ParseTree
30
31
  @ptr = @tree
31
32
  end
32
33
 
33
- def new_macro(line_obj, line_start)
34
+ def new_macro(line_obj, line_start, macro, args, delim, mode)
34
35
  line = line_obj.line
35
- macro, args, delim = Eggshell::Processor.parse_macro_start(line)
36
+ #macro, args, delim = Eggshell::Processor.parse_macro_start(line)
36
37
 
37
38
  push_block
38
39
 
39
40
  if delim
40
- @modes << :macro
41
+ @modes << (mode == MH::COLLECT_RAW_MACRO ? :macro_raw : macro)
41
42
  @macro_delims << delim.reverse.gsub('[', ']').gsub('(', ')').gsub('{', '}')
42
43
  @macro_open << line
43
44
  @macro_ptr << @ptr
44
45
  # set ptr to entry's tree
45
- entry = [:macro, macro, args, [], line_start]
46
+ entry = [:macro, macro, args, [], line_start, line_start]
46
47
  @ptr << entry
47
48
  @ptr = entry[IDX_LINES]
48
49
  else
49
- @ptr << [:macro, macro, args, [], line_start]
50
+ @ptr << [:macro, macro, args, [], line_start, line_start]
50
51
  end
51
52
  end
52
53
 
@@ -65,24 +66,26 @@ class Eggshell::ParseTree
65
66
  return false
66
67
  end
67
68
 
68
- def new_block(handler, type, line_obj, consume_mode, line_start)
69
- block_type, args, line = Eggshell::Processor.parse_block_start(line_obj.line)
69
+ def new_block(handler, type, line_obj, consume_mode, line_start, eggshell)
70
+ block_type, args, line = eggshell.parse_block_start(line_obj.line)
70
71
  nline = Eggshell::Line.new(line, line_obj.tab_str, line_obj.indent_lvl, line_obj.line_num)
71
72
 
72
73
  if consume_mode != BH::DONE
73
- @modes << :block
74
+ #@modes << :block
74
75
  if line != ''
75
76
  @lines << nline
76
77
  line_start -= 1
77
78
  end
78
79
  @cur_block = [handler, type, args, line_start]
79
80
  if consume_mode == BH::COLLECT_RAW
80
- mode = :raw
81
+ #mode = :raw
82
+ @modes << :raw
81
83
  else consume_mode == BH::COLLECT
82
- mode = :block
84
+ #mode = :block
85
+ @modes << :block
83
86
  end
84
87
  else
85
- @ptr << [:block, type, args, [nline], line_start]
88
+ @ptr << [:block, type, args, [nline], line_start, line_start]
86
89
  end
87
90
  end
88
91
 
@@ -113,6 +116,10 @@ class Eggshell::ParseTree
113
116
  more
114
117
  end
115
118
 
119
+ def collect_macro_raw(line_obj)
120
+ @ptr << line_obj
121
+ end
122
+
116
123
  def raw_line(line_obj)
117
124
  @ptr << line_obj
118
125
  end
@@ -127,6 +134,10 @@ class Eggshell::ParseTree
127
134
  @modes[-1]
128
135
  end
129
136
 
137
+ def collect_mode
138
+ @collect_modes[-1]
139
+ end
140
+
130
141
  # Does basic output of parse tree structure to visually inspect parsed info.
131
142
  def self.walk(struct = nil, indent = 0, out = nil)
132
143
  out = $stdout if !out
@@ -146,4 +157,44 @@ class Eggshell::ParseTree
146
157
  end
147
158
  end
148
159
  end
160
+
161
+ # Groups together chained macros. This ensures proper block-chain flow. Note that
162
+ # a related sequence is grouped within a {{pipe}} macro.
163
+ def self.condense(processor, units)
164
+ condensed = []
165
+
166
+ last_macro = nil
167
+ units.each do |unit|
168
+ if !unit.is_a?(Array) || unit[0] == :block
169
+ condensed << unit
170
+ else
171
+ mhandler = processor.get_macro_handler(unit[1])
172
+ chain_type = nil
173
+ chain_macro = nil
174
+ chain_type, chain_macro = mhandler.chain_type(unit[1]) if mhandler
175
+ if chain_type != MH::CHAIN_NONE
176
+ if chain_type == MH::CHAIN_START
177
+ unit[3] = condense(processor, unit[3])
178
+ condensed << [:macro, 'pipe', [{'chained'=>chain_macro}], [unit], unit[4], -1]
179
+ last_macro = unit[1]
180
+ elsif chain_type == MH::CHAIN_CONTINUE && chain_macro == last_macro
181
+ unit[3] = condense(processor, unit[3])
182
+ condensed[-1][3] << unit
183
+ elsif chain_type == MH::CHAIN_END && chain_macro == last_macro
184
+ unit[3] = condense(processor, unit[3])
185
+ condensed[-1][3] << unit
186
+ condensed[-1][5] = unit[5]
187
+ last_macro = nil
188
+ else
189
+ condensed << unit
190
+ last_macro = nil
191
+ end
192
+ else
193
+ condensed << unit
194
+ end
195
+ end
196
+ end
197
+
198
+ condensed
199
+ end
149
200
  end