eggshell 0.8.3 → 1.0.0

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,850 @@
1
+ class Eggshell::Bundles::Basics
2
+ BUNDLE_ID = 'basics'
3
+ EE = Eggshell::ExpressionEvaluator
4
+
5
+ def self.new_instance(proc, opts = nil)
6
+ BasicBlocks.new.set_processor(proc)
7
+ SectionBlocks.new.set_processor(proc)
8
+ InlineMacros.new.set_processor(proc)
9
+ BasicMacros.new.set_processor(proc)
10
+ ControlMacros.new.set_processor(proc)
11
+
12
+ proc.register_functions('', StdFunctions::FUNC_NAMES)
13
+ proc.register_functions('sprintf', Kernel)
14
+ end
15
+
16
+ # `table` block parameters:
17
+ # - `row.classes`: defaults to `['odd', 'even']`. The number of elements represents the number of cycles.
18
+ class BasicBlocks
19
+ include Eggshell::BlockHandler
20
+
21
+ CELL_ATTR_START = '!'
22
+ CELL_ATTR_END = '<!'
23
+
24
+ def set_processor(proc)
25
+ @proc = proc
26
+ @proc.register_block(self, *%w(table pre p bq div raw ol ul # - / | >))
27
+ end
28
+
29
+ def start(name, line, buffer, indents = '', indent_level = 0, line_count = -1)
30
+ set_block_params(name)
31
+ bp = @block_params[name]
32
+
33
+ if name == '#' || name == '-' || name == 'ol' || name == 'ul'
34
+ @type = :list
35
+ @lines = []
36
+ @lines << [line, indent_level, name == '#' ? 'ol' : 'ul'] if name == '#' || name == '-'
37
+ @indent_start = indent_level
38
+ return COLLECT
39
+ end
40
+
41
+ if name == 'table' || name == '/' || name == '|'
42
+ @type = :table
43
+ @lines = []
44
+ @lines << line if name != 'table'
45
+ return COLLECT
46
+ end
47
+
48
+ if name == '>'
49
+ @type = :dt
50
+ @lines = [line.split('::', 2)]
51
+ return COLLECT
52
+ end
53
+
54
+ # assume block text
55
+ @type = name
56
+ if line.index(name) == 0
57
+ line = line.lstrip
58
+ end
59
+ @lines = [@type == 'raw' ? @proc.expand_expr(line) : @proc.fmt_line(line)]
60
+ return @type == 'raw' ? COLLECT_RAW : COLLECT
61
+ end
62
+
63
+ def collect(line, buff, indents = '', indent_level = 0, line_count = -1)
64
+ line = '' if !line
65
+ ret = COLLECT
66
+ if @type == :list
67
+ ret = do_list(line, buff, indents, indent_level)
68
+ elsif @type == :table
69
+ ret = do_table(line, buff, indents, indent_level)
70
+ elsif @type == :dt
71
+ ret = do_dt(line, buff, indents, indent_level)
72
+ else
73
+ ret = do_default(line, buff, indents, indent_level)
74
+ end
75
+ return ret
76
+ end
77
+
78
+ def do_list(line, buff, indents = '', indent_level = 0)
79
+ ret = COLLECT
80
+ lstrip = line.lstrip
81
+ if line && (lstrip[0] == '#' || lstrip[0] == '-')
82
+ @lines << [line[1..-1].strip, indent_level+@indent_start, lstrip[0] == '#' ? 'ol' : 'ul']
83
+ else
84
+ # if non-empty line, reprocess this line but also process buffer
85
+ ret = (line && line != '') ? RETRY : DONE
86
+ order_stack = []
87
+ otype_stack = []
88
+ last = nil
89
+ @lines.each do |tuple|
90
+ line, indent, type = tuple
91
+ # normalize indent
92
+ indent -= @indent_start
93
+ if order_stack.length == 0
94
+ order_stack << "<#{type}>"
95
+ otype_stack << type
96
+ # @todo make sure that previous item was a list
97
+ # remove closing li to enclose sublist
98
+ elsif indent > (otype_stack.length-1) && order_stack.length > 0
99
+ last = order_stack[-1]
100
+ last = last[0...last.length-5]
101
+ order_stack[-1] = last
102
+
103
+ order_stack << "#{"\t"*indent}<#{type}>"
104
+ otype_stack << type
105
+ elsif indent < (otype_stack.length-1)
106
+ count = otype_stack.length - 1 - indent
107
+ while count > 0
108
+ ltype = otype_stack.pop
109
+ order_stack << "#{"\t"*count}</#{ltype}>\n#{"\t"*(count-1)}</li>"
110
+ count -= 1
111
+ end
112
+ end
113
+ order_stack << "#{"\t"*indent}<li>#{@proc.fmt_line(line)}</li>"
114
+ end
115
+
116
+ # close nested lists
117
+ d = otype_stack.length
118
+ c = 1
119
+ otype_stack.each do |type|
120
+ ident = d - c
121
+ order_stack << "#{"\t" * ident}</#{type}>#{c == d ? '' : "</li>"}"
122
+ c += 1
123
+ end
124
+ buff << order_stack.join("\n")
125
+ end
126
+ ret
127
+ end
128
+
129
+ def do_table(line, buff, indents = '', indent_level = 0)
130
+ ret = COLLECT
131
+ if line[0] == '|' || line[0] == '/'
132
+ @lines << line
133
+ else
134
+ ret = (line[0] != '\\' && line != '') ? RETRY : DONE
135
+ map = @block_params['table'] || {}
136
+ tbl_class = map['class'] || ''
137
+ tbl_style = map['style'] || ''
138
+ tbl_attrib = ''
139
+ if map['attribs'].is_a?(String)
140
+ tbl_attrib = map['attribs']
141
+ elsif map['attribs'].is_a?(Hash)
142
+ map['attribs'].each do |key,val|
143
+ tbl_attrib = "#{tbl_attrib} #{key}='#{val}'"
144
+ end
145
+ end
146
+ row_classes = map['row.classes']
147
+ row_classes = ['odd', 'even'] if !row_classes.is_a?(Array)
148
+
149
+ @proc.vars['t.row'] = 0
150
+ buff << "<table class='#{tbl_class}' style='#{tbl_style}'#{tbl_attrib}>"
151
+ cols = []
152
+ rows = 0
153
+ rc = 0
154
+ @lines.each do |line|
155
+ ccount = 0
156
+ if line[0] == '/' && rows == 0
157
+ cols = line[1..line.length].split('|')
158
+ buff << "<thead><tr class='#{map['head.class']}'>"
159
+ cols.each do |col|
160
+ buff << "\t#{fmt_cell(col, true, ccount)}"
161
+ ccount += 1
162
+ end
163
+ buff << '</tr></thead>'
164
+ buff << '<tbody>'
165
+ elsif line[0] == '/'
166
+ # implies footer
167
+ cols = line[1..line.length].split('|')
168
+ buff << "<tfoot><tr class='#{map['foot.class']}'>"
169
+ cols.each do |col|
170
+ buff << "\t#{fmt_cell(col, true, ccount)}"
171
+ ccount += 1
172
+ end
173
+ buff << '</tr></tfoot>'
174
+ elsif line[0] == '|' || line[0..1] == '|>'
175
+ idx = 1
176
+ sep = /(?<!\\)\|/
177
+ if line[1] == '>'
178
+ idx = 2
179
+ sep = /(?<!\\)\|\>/
180
+ end
181
+ cols = line[idx..line.length].split(sep)
182
+ @proc.vars['t.row'] = rc
183
+ rclass = row_classes[rc % row_classes.length]
184
+ buff << "<tr class='tr-row-#{rc} #{rclass}'>"
185
+ cols.each do |col|
186
+ buff << "\t#{fmt_cell(col, false, ccount)}"
187
+ ccount += 1
188
+ end
189
+ buff << '</tr>'
190
+ rc += 1
191
+ else
192
+ cols = line[1..line.length].split('|') if line[0] == '\\'
193
+ end
194
+ rows += 1
195
+ end
196
+
197
+ buff << '</tbody>'
198
+ if cols.length > 0
199
+ # @todo process footer
200
+ end
201
+ buff << "</table>"
202
+ @proc.vars['table.class'] = ''
203
+ @proc.vars['table.style'] = ''
204
+ @proc.vars['table.attribs'] = ''
205
+ end
206
+ ret
207
+ end
208
+
209
+ def do_dt(line, buff, indents = '', indent_level = 0)
210
+ ret = COLLECT
211
+ if line == '' || line[0] != '>'
212
+ ret = DONE
213
+ ret = RETRY if line[0] != '>'
214
+
215
+ buff << "<dl class='#{@proc.vars['dd.class']}'>"
216
+ @lines.each do |line|
217
+ key = line[0]
218
+ val = line[1]
219
+ buff << "<dt class='#{@proc.vars['dt.class']}'>#{key}</dt><dd class='#{@proc.vars['dd.class']}'>#{val}</dd>"
220
+ end
221
+ buff << "</dl>"
222
+ else
223
+ @lines << line[1..-1].split('::', 2)
224
+ ret = COLLECT
225
+ end
226
+ ret
227
+ end
228
+
229
+ def do_default(line, buff, indents = '', indent_level = 0)
230
+ ret = COLLECT
231
+ blank = false
232
+
233
+ raw = @type == 'raw'
234
+ pre = @type == 'pre'
235
+ nofmt = raw
236
+
237
+ # we need to group indented lines as part of block, especially if it's otherwise empty
238
+ if raw || pre || @type == 'bq'
239
+ raw = true
240
+
241
+ if indent_level > 0
242
+ line = indents + line
243
+ if pre
244
+ line = "\n#{line.chomp}"
245
+ end
246
+ else
247
+ blank = line == ''
248
+ if pre
249
+ line = "\n#{line}"
250
+ end
251
+ end
252
+ else
253
+ blank = line == ''
254
+ end
255
+
256
+ if blank
257
+ @lines.delete('')
258
+ start_tag = ''
259
+ end_tag = ''
260
+ content = ''
261
+ if @type != 'raw'
262
+ bp = @block_params[@type]
263
+ attrs = bp['attributes'] || {}
264
+ attrs['class'] = bp['class'] || ''
265
+ attrs['style'] = bp['style'] || ''
266
+ attrs['id'] = bp['id'] || ''
267
+ @type = 'blockquote' if @type == 'bq'
268
+ # @todo get attributes from block param
269
+ start_tag = create_tag(@type, attrs)
270
+ end_tag = "</#{@type}>"
271
+ join = @type == 'pre' ? "" : "<br />\n"
272
+
273
+ content = "#{start_tag}#{@lines.join(join)}#{end_tag}"
274
+ else
275
+ content = @lines.join("\n")
276
+ end
277
+
278
+ buff << content
279
+ @lines = []
280
+ ret = DONE
281
+ else
282
+ line = !nofmt ? @proc.fmt_line(line) : @proc.expand_expr(line)
283
+ @lines << line
284
+ ret = raw ? COLLECT_RAW : COLLECT
285
+ end
286
+ ret
287
+ end
288
+
289
+ def fmt_cell(val, header = false, colnum = 0)
290
+ tag = header ? 'th' : 'td'
291
+ buff = []
292
+ attribs = ''
293
+
294
+ if val[0] == '\\'
295
+ val = val[1..val.length]
296
+ elsif val[0] == CELL_ATTR_START
297
+ rt = val.index(CELL_ATTR_END)
298
+ if rt
299
+ attribs = val[1...rt]
300
+ val = val[rt+CELL_ATTR_END.length..val.length]
301
+ end
302
+ end
303
+
304
+ # inject column position via class
305
+ olen = attribs.length
306
+ attribs = attribs.gsub(/class=(['"])/, 'class=$1' + "td-col-#{colnum}")
307
+ if olen == attribs.length
308
+ attribs += " class='td-col-#{colnum}'"
309
+ end
310
+
311
+ buff << "<#{tag} #{attribs}>"
312
+ cclass =
313
+ if val[0] == '\\'
314
+ val = val[1..val.length]
315
+ end
316
+
317
+ buff << @proc.fmt_line(val)
318
+ buff << "</#{tag}>"
319
+ return buff.join('')
320
+ end
321
+ end
322
+
323
+ # Handles blocks that deal with sectioning of content.
324
+ <<-docblock
325
+ h1. Header 1
326
+ h2. Header 2
327
+ h3({class: 'header-3', id: 'custom-id'}). Header 3
328
+
329
+ section.
330
+ h1. Another Header 1 inside a section
331
+ end-section.
332
+
333
+ !# outputs a table of contents based on all headers accumulated
334
+ !# define the output of each header (or use 'default' as fallback)
335
+ toc.
336
+ start: <div class='toc'>
337
+ end: </div>
338
+ default: <div class='toc-$level'><a href='$id'>$title</a></div>
339
+ h4: <div class='toc-h4 special-exception'><a href='$id'>$title</a></div>
340
+ section: <div class='toc-section-wrap'>
341
+ section_end: </div>
342
+ docblock
343
+ class SectionBlocks
344
+ include Eggshell::BlockHandler
345
+
346
+ TOC_TEMPLATE = {
347
+ :default => "<div class='toc-h$level'><a href='\#$id'>$title</a></div>"
348
+ }
349
+
350
+ def set_processor(proc)
351
+ @proc = proc
352
+ @proc.register_block(self, *%w(h1 h2 h3 h4 h5 h6 hr section end-section toc))
353
+ @header_list = []
354
+ @header_idx = {}
355
+ end
356
+
357
+ def start(name, line, buffer, indents = '', indent_level = 0, line_count = -1)
358
+ set_block_params(name)
359
+ bp = @block_params[name]
360
+
361
+ if name[0] == 'h'
362
+ if name == 'hr'
363
+ buffer << "<hr />"
364
+ else
365
+ lvl = name[1].to_i
366
+ attrs = bp['attributes'] || {}
367
+ attrs['class'] = bp['class'] || ''
368
+ attrs['style'] = bp['style'] || ''
369
+
370
+ id = bp['id'] || line.downcase.strip.gsub(/[^a-z0-9_-]+/, '-')
371
+ lid = id
372
+ i = 1
373
+ while @header_idx[lid] != nil
374
+ lid = "#{id}-#{i}"
375
+ i += 1
376
+ end
377
+ id = lid
378
+ attrs['id'] = id
379
+ title = @proc.fmt_line(line)
380
+
381
+ buffer << "#{create_tag(name, attrs)}#{title}</#{name}>"
382
+
383
+ @header_list << {:level => lvl, :id => lid, :title => title, :tag => name}
384
+ @header_idx[lid] = @header_list.length - 1
385
+ end
386
+ return DONE
387
+ elsif name == 'section'
388
+ attrs = bp['attributes'] || {}
389
+ attrs['class'] = bp['class'] || ''
390
+ attrs['style'] = bp['style'] || ''
391
+ attrs['id'] = bp['id'] || ''
392
+ buffer << create_tag('section', attrs)
393
+ @header_list << name
394
+ return DONE
395
+ elsif name == 'end-section'
396
+ buffer << '</section>'
397
+ @header_list << name
398
+ return DONE
399
+ elsif name == 'toc'
400
+ @toc_template = TOC_TEMPLATE.clone
401
+ return COLLECT
402
+ end
403
+ end
404
+
405
+ def collect(line, buffer, indents = '', indent_level = 0, line_count = -1)
406
+ if line == '' || !line
407
+ buffer << @proc.fmt_line(@toc_template[:start]) if @toc_template[:start]
408
+ @header_list.each do |entry|
409
+ if entry == 'section'
410
+ buffer << @proc.fmt_line(@toc_template[:section]) if @toc_template[:section]
411
+ elsif entry == 'section_end'
412
+ buffer << @proc.fmt_line(@toc_template[:section_end]) if @toc_template[:section_end]
413
+ elsif entry.is_a?(Hash)
414
+ tpl = @toc_template[entry[:tag]] || @toc_template[:default]
415
+ buffer << @proc.fmt_line(
416
+ tpl.gsub('$id', entry[:id]).gsub('$title', entry[:title]).gsub('$level', entry[:level].to_s)
417
+ )
418
+ end
419
+ end
420
+ buffer << @proc.fmt_line(@toc_template[:end]) if @toc_template[:end]
421
+ return DONE
422
+ else
423
+ key, val = line.split(':', 2)
424
+ @toc_template[key.to_sym] = val
425
+ return COLLECT
426
+ end
427
+ end
428
+ end
429
+
430
+ class InlineMacros
431
+ include Eggshell::MacroHandler
432
+
433
+ def initialize
434
+ @capvar = nil
435
+ @collbuff = nil
436
+ @depth = 0
437
+ end
438
+
439
+ HASH_FMT_DECORATORS = {
440
+ '[*' => '<b>',
441
+ '[**' => '<strong>',
442
+ '[_' => '<i>',
443
+ '[__' => '<em>',
444
+ '*]'=> '</b>',
445
+ '**]' => '</strong>',
446
+ '_]' => '</i>',
447
+ '__]' => '</em>',
448
+ '[-_' => '<u>',
449
+ '_-]' => '</u>',
450
+ '[-' => '<strike>',
451
+ '-]' => '</strike>'
452
+ }.freeze
453
+
454
+ def set_processor(eggshell)
455
+ @proc = eggshell
456
+ @proc.register_macro(self, '[!', '[~', '[^', '[.', '[*', '[**', '[/', '[//', '[_', '[-')
457
+ end
458
+
459
+ def process(buffer, macname, args, lines, depth)
460
+ prefix = macname[0..1]
461
+ textpart = args.shift
462
+ tag = nil
463
+
464
+ case prefix
465
+ when '[^'
466
+ tag = 'sup'
467
+ when '[.'
468
+ tag = 'sub'
469
+ when '[*'
470
+ tag = macname == '[**' ? 'strong' : 'b'
471
+ when '[/'
472
+ tag = macname == '[//' ? 'em' : 'i'
473
+ when '[-'
474
+ tag = 'strike'
475
+ when '[_'
476
+ tag = 'u'
477
+ when '[~'
478
+ tag = 'a'
479
+ link = textpart ? textpart.strip : textpart
480
+ text = nil
481
+ if link == ''
482
+ text = ''
483
+ elsif link.index('; ') == nil
484
+ textpart = link
485
+ args.unshift('href:'+ link);
486
+ else
487
+ textpart, link = link.split('; ')
488
+ link = '' if !link
489
+ args.unshift('href:'+link)
490
+ end
491
+ when '[!'
492
+ tag = 'img'
493
+ args.unshift('src:'+textpart)
494
+ textpart = nil
495
+ end
496
+
497
+ buffer << restructure_html(tag, textpart ? textpart.strip : textpart, args)
498
+ end
499
+
500
+ def restructure_html(tag, text, attributes = [])
501
+ buff = "<#{tag}"
502
+ attributes.each do |attrib|
503
+ key, val = attrib.split(':', 2)
504
+ # @todo html escape?
505
+ if val
506
+ buff = "#{buff} #{key}=\"#{val.gsub('\\|', '|')}\""
507
+ else
508
+ buff = "#{buff} #{key}"
509
+ end
510
+ end
511
+
512
+ if text == nil
513
+ buff += ' />'
514
+ else
515
+ buff = "#{buff}>#{text}</#{tag}>"
516
+ end
517
+ buff
518
+ end
519
+ end
520
+
521
+ # Macros:
522
+ #
523
+ # - `include`
524
+ # - `capture`
525
+ # - `var`
526
+ class BasicMacros
527
+ include Eggshell::MacroHandler
528
+
529
+ def initialize
530
+ @capvar = nil
531
+ @collbuff = nil
532
+ @depth = 0
533
+ end
534
+
535
+ def set_processor(eggshell)
536
+ @proc = eggshell
537
+ @proc.register_macro(self, *%w(! = capture var include process parse_test))
538
+ end
539
+
540
+ def process(buffer, macname, args, lines, depth)
541
+ if macname == '!'
542
+ @proc.vars[:block_params] = @proc.expr_eval(args)
543
+ elsif macname == 'process'
544
+ if args[0]
545
+ proclines = @proc.expr_eval(args[0])
546
+ proclines = proclines.split(/[\r\n]+/) if proclines.is_a?(String)
547
+ buffer << @proc.process(proclines, depth + 1) if proclines.is_a?(Array)
548
+ end
549
+ elsif macname == 'capture'
550
+ # @todo check args for fragment to parse
551
+ return if !lines
552
+ var = args[0]
553
+ @proc.vars[var] = @proc.process(lines, depth)
554
+ elsif macname == 'var' || macname == '='
555
+ # @todo support multiple vars via lines
556
+ # @todo expand value if expression
557
+ if args.length >= 2
558
+ key = args[0]
559
+ val = args[1]
560
+
561
+ if val.is_a?(Array) && val[0].is_a?(Symbol)
562
+ @proc.vars[key] = @proc.expr_eval(val)
563
+ else
564
+ @proc.vars[key] = val
565
+ end
566
+
567
+ # @todo fix this so it's possible to set things like arr[0].setter, etc.
568
+ # ptrs = EE.retrieve_var(key, @proc.vars, {}, true)
569
+ # val = val.is_a?(Array) && val[0] == :var ? EE.retrieve_var(val[1], @proc.vars, {}) : val
570
+ # if ptrs
571
+ # EE.set_var(ptrs[0], ptrs[1][0], ptrs[1][1], val)
572
+ # else
573
+ # @proc.vars[key] = val
574
+ # end
575
+ end
576
+ elsif macname == 'include'
577
+ paths = args[0]
578
+ opts = args[1] || {}
579
+ if opts['encoding']
580
+ opts[:encoding] = opts['encoding']
581
+ else
582
+ opts[:encoding] = 'utf-8'
583
+ end
584
+ if lines && lines.length > 0
585
+ paths = lines
586
+ end
587
+ do_include(paths, buffer, depth, opts)
588
+ end
589
+ end
590
+
591
+ def do_include(paths, buff, depth, opts = {})
592
+ @proc.vars[:include_stack] = [] if !@proc.vars[:include_stack]
593
+ paths = [paths] if !paths.is_a?(Array)
594
+ # @todo check all include paths?
595
+ paths.each do |inc|
596
+ inc = @proc.expand_expr(inc.strip)
597
+ checks = []
598
+ if inc[0] != '/'
599
+ @proc.vars[:include_paths].each do |root|
600
+ checks << "#{root}/#{inc}"
601
+ end
602
+ # @todo if :include_root, expand path and check that it's under the root, otherwise, sandbox
603
+ else
604
+ # sandboxed root include
605
+ if @proc.vars[:include_root]
606
+ checks << "#{@proc.vars[:include_root]}#{inc}"
607
+ else
608
+ checks << inc
609
+ end
610
+ end
611
+
612
+ checks.each do |inc|
613
+ if File.exists?(inc)
614
+ lines = IO.readlines(inc, $/, opts)
615
+ @proc.vars[:include_stack] << inc
616
+ @proc.context.push_line_counter
617
+ begin
618
+ buff << @proc.process(lines, depth + 1)
619
+ @proc._debug("include: 200 #{inc}")
620
+ rescue => ex
621
+ @proc._error("include: 500 #{inc}: #{ex.message}")
622
+ end
623
+
624
+ @proc.vars[:include_stack].pop
625
+ @proc.context.pop_line_counter
626
+ break
627
+ else
628
+ @proc._warn("include: 404 #{inc}")
629
+ end
630
+ end
631
+ end
632
+ end
633
+ end
634
+
635
+ class ControlMacros
636
+ include Eggshell::MacroHandler
637
+
638
+ def initialize
639
+ @stack = []
640
+ @state = []
641
+ @macstack = []
642
+ end
643
+
644
+ def set_processor(eggshell)
645
+ @proc = eggshell
646
+ @proc.register_macro(self, *%w(if elsif else loop for while break next))
647
+ end
648
+
649
+ def process(buffer, macname, args, lines, depth)
650
+ p0 = args.is_a?(Array) ? args[0] : nil
651
+ lines ? lines.delete('') : ''
652
+
653
+ macname = macname.to_sym
654
+ st = @state[depth]
655
+ if !@state[depth]
656
+ st = {:type => macname}
657
+
658
+ @state[depth] = st
659
+ # erase nested state
660
+ @state[depth+1] = nil
661
+ end
662
+
663
+ if macname == :for || macname == :loop
664
+ p0 = p0 || {}
665
+ st[:var] = p0['var']
666
+ st[:start] = p0['start']
667
+ st[:stop] = p0['stop']
668
+ st[:step] = p0['step'] || 1
669
+ st[:iter] = p0['items'] || 'items'
670
+ st[:item] = p0['item'] || 'item'
671
+ st[:counter] = p0['counter'] || 'counter'
672
+ st[:raw] = p0['raw'] # @todo inherit if not set?
673
+ st[:collect] = p0['collect']
674
+ st[:agg_block] = p0['aggregate_block']
675
+
676
+ mbuff = []
677
+ looper = nil
678
+ loop_is_map = false
679
+
680
+ # in for, construct range that can be looped over
681
+ # in loop, detect 'each' method
682
+ if macname == :for
683
+ st[:item] = st[:var]
684
+ looper = Range.new(st[:start], st[:stop]).step(st[:step]).to_a
685
+ elsif macname == :loop
686
+ begin
687
+ looper = st[:iter].is_a?(Array) && st[:iter][0].is_a?(Symbol) ? @proc.expr_eval(st[:iter]) : st[:iter]
688
+ looper = nil if !looper.respond_to?(:each)
689
+ loop_is_map = looper.is_a?(Hash)
690
+ rescue
691
+ end
692
+ end
693
+
694
+ collector = []
695
+ if looper
696
+ counter = 0
697
+ looper.each do |i1, i2|
698
+ # for maps, use key as the counter
699
+ val = nil
700
+ if loop_is_map
701
+ @proc.vars[st[:counter]] = i1
702
+ val = i2
703
+ else
704
+ val = i1
705
+ @proc.vars[st[:counter]] = counter
706
+ end
707
+
708
+ # inject value into :item -- if it's an expression, evaluate first
709
+ @proc.vars[st[:item]] = val.is_a?(Array) && val[0].is_a?(Symbol) ? @proc.expr_eval(val) : val
710
+ # divert lines to collector
711
+ if st[:collect]
712
+ lines.each do |_line|
713
+ if _line.is_a?(String)
714
+ collector << @proc.expand_expr(_line)
715
+ elsif _line.is_a?(Eggshell::Block)
716
+ _line.process(collector)
717
+ end
718
+ end
719
+ else
720
+ process_lines(lines, buffer, depth + 1, st[:raw])
721
+ end
722
+ break if st[:break]
723
+
724
+ counter += 1
725
+ end
726
+ end
727
+
728
+ # since there are lines here, process collected as an aggregated set of blocks
729
+ if collector.length > 0
730
+ if st[:collect] == 'aggregate'
731
+ if st[:agg_block]
732
+ collector.unshift(st[:agg_block])
733
+ end
734
+ process_lines(collector, buffer, depth + 1, false)
735
+ else
736
+ buffer << collector.join("\n")
737
+ end
738
+ end
739
+
740
+ # clear state
741
+ @state[depth] = nil
742
+ elsif macname == :while
743
+ raw = args[1]
744
+ while @proc.expr_eval(p0)
745
+ process_lines(lines, buffer, depth + 1, raw)
746
+ break if st[:break]
747
+ end
748
+ elsif macname == :if || macname == :elsif || macname == :else
749
+ cond = p0
750
+ st[:if] = true if macname == :if
751
+ if st[:cond_count] == nil || macname == :if
752
+ st[:cond_count] = 0
753
+ st[:cond_eval] = false
754
+ st[:cond_met] = false
755
+ end
756
+
757
+ last_action = st[:last_action]
758
+ st[:last_action] = macname.to_sym
759
+
760
+ # @todo more checks (e.g. no elsif after else, no multiple else, etc.)
761
+ if !st[:if] || (macname != :else && !cond)
762
+ # @todo exception?
763
+ return
764
+ end
765
+
766
+ if macname != :else
767
+ if !st[:cond_eval]
768
+ cond_struct = Eggshell::ExpressionEvaluator.struct(cond)
769
+ st[:cond_eval] = @proc.expr_eval(cond_struct)[0]
770
+ end
771
+ else
772
+ st[:cond_eval] = true
773
+ end
774
+
775
+ if st[:cond_eval] && !st[:cond_met]
776
+ st[:cond_met] = true
777
+ process_lines(lines, buffer, depth + 1)
778
+ end
779
+ elsif macname == :break
780
+ lvl = p0 || 1
781
+ i = depth - 1
782
+
783
+ # set breaks at each found loop until # of levels reached
784
+ while i >= 0
785
+ st = @state[i]
786
+ i -= 1
787
+ next if !st
788
+ if st[:type] == :for || st[:type] == :while || st[:type] == :loop
789
+ lvl -= 1
790
+ st[:break] = true
791
+ break if lvl <= 0
792
+ end
793
+ end
794
+ elsif macname == :next
795
+ lvl = p0 || 1
796
+ i = depth - 1
797
+
798
+ # set breaks at each found loop until # of levels reached
799
+ while i >= 0
800
+ st = @state[i]
801
+ i -= 1
802
+ next if !st
803
+ if st[:type] == :for || st[:type] == :while || st[:type] == :loop
804
+ lvl -= 1
805
+ st[:next] = true
806
+ break if lvl <= 0
807
+ end
808
+ end
809
+ end
810
+ end
811
+
812
+ def process_lines(lines, buffer, depth, raw = false)
813
+ return if !lines
814
+ ln = []
815
+ lines.each do |line|
816
+ if line.is_a?(Eggshell::Block)
817
+ if ln.length > 0
818
+ buffer << (raw ? @proc.expand_expr(ln.join("\n")) : @proc.process(ln, depth))
819
+ ln = []
820
+ end
821
+ line.process(buffer, depth)
822
+ if @state[depth-1][:next]
823
+ @state[depth-1][:next] = false
824
+ break
825
+ end
826
+ else
827
+ ln << line
828
+ end
829
+ end
830
+ if ln.length > 0
831
+ buffer << (raw ? @proc.expand_expr(ln.join("\n")) : @proc.process(ln, depth))
832
+ end
833
+ end
834
+
835
+ protected :process_lines
836
+ end
837
+
838
+ # Baseline functions.
839
+ # @todo catch exceptions???
840
+ module StdFunctions
841
+ # Repeats `str` by a given `amt`
842
+ def self.str_repeat(str, amt)
843
+ return str * amt
844
+ end
845
+
846
+ FUNC_NAMES = %w(str_repeat).freeze
847
+ end
848
+
849
+ include Eggshell::Bundles::Bundle
850
+ end