eggshell 0.8.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,854 @@
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)
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)
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
+ # strip off first indent
241
+ if indent_level > 0
242
+ #idx = indents.length / indent_level
243
+ line = indents + line
244
+ if pre
245
+ line = "\n#{line}"
246
+ elsif line == ''
247
+ line = ' '
248
+ end
249
+ else
250
+ blank = line == ''
251
+ if pre
252
+ line = "\n#{line}"
253
+ end
254
+ end
255
+ else
256
+ line = " " if line == '\\'
257
+ blank = line == ''
258
+ end
259
+
260
+ if blank
261
+ @lines.delete('')
262
+ start_tag = ''
263
+ end_tag = ''
264
+ content = ''
265
+ if @type != 'raw'
266
+ bp = @block_params[@type]
267
+ attrs = bp['attributes'] || {}
268
+ attrs['class'] = bp['class'] || ''
269
+ attrs['style'] = bp['style'] || ''
270
+ attrs['id'] = bp['id'] || ''
271
+ @type = 'blockquote' if @type == 'bq'
272
+ # @todo get attributes from block param
273
+ start_tag = create_tag(@type, attrs)
274
+ end_tag = "</#{@type}>"
275
+ join = @type == 'pre' ? "" : "<br />\n"
276
+
277
+ content = "#{start_tag}#{@lines.join(join)}#{end_tag}"
278
+ else
279
+ content = @lines.join("\n")
280
+ end
281
+
282
+ buff << content
283
+ @lines = []
284
+ ret = DONE
285
+ else
286
+ line = !nofmt ? @proc.fmt_line(line) : @proc.expand_expr(line)
287
+ @lines << line
288
+ ret = raw ? COLLECT_RAW : COLLECT
289
+ end
290
+ ret
291
+ end
292
+
293
+ def fmt_cell(val, header = false, colnum = 0)
294
+ tag = header ? 'th' : 'td'
295
+ buff = []
296
+ attribs = ''
297
+
298
+ if val[0] == '\\'
299
+ val = val[1..val.length]
300
+ elsif val[0] == CELL_ATTR_START
301
+ rt = val.index(CELL_ATTR_END)
302
+ if rt
303
+ attribs = val[1...rt]
304
+ val = val[rt+CELL_ATTR_END.length..val.length]
305
+ end
306
+ end
307
+
308
+ # inject column position via class
309
+ olen = attribs.length
310
+ attribs = attribs.gsub(/class=(['"])/, 'class=$1' + "td-col-#{colnum}")
311
+ if olen == attribs.length
312
+ attribs += " class='td-col-#{colnum}'"
313
+ end
314
+
315
+ buff << "<#{tag} #{attribs}>"
316
+ cclass =
317
+ if val[0] == '\\'
318
+ val = val[1..val.length]
319
+ end
320
+
321
+ buff << @proc.fmt_line(val)
322
+ buff << "</#{tag}>"
323
+ return buff.join('')
324
+ end
325
+ end
326
+
327
+ # Handles blocks that deal with sectioning of content.
328
+ <<-docblock
329
+ h1. Header 1
330
+ h2. Header 2
331
+ h3({class: 'header-3', id: 'custom-id'}). Header 3
332
+
333
+ section.
334
+ h1. Another Header 1 inside a section
335
+ end-section.
336
+
337
+ !# outputs a table of contents based on all headers accumulated
338
+ !# define the output of each header (or use 'default' as fallback)
339
+ toc.
340
+ start: <div class='toc'>
341
+ end: </div>
342
+ default: <div class='toc-$level'><a href='$id'>$title</a></div>
343
+ h4: <div class='toc-h4 special-exception'><a href='$id'>$title</a></div>
344
+ section: <div class='toc-section-wrap'>
345
+ section_end: </div>
346
+ docblock
347
+ class SectionBlocks
348
+ include Eggshell::BlockHandler
349
+
350
+ TOC_TEMPLATE = {
351
+ :default => "<div class='toc-h$level'><a href='\#$id'>$title</a></div>"
352
+ }
353
+
354
+ def set_processor(proc)
355
+ @proc = proc
356
+ @proc.register_block(self, *%w(h1 h2 h3 h4 h5 h6 hr section end-section toc))
357
+ @header_list = []
358
+ @header_idx = {}
359
+ end
360
+
361
+ def start(name, line, buffer, indents = '', indent_level = 0)
362
+ set_block_params(name)
363
+ bp = @block_params[name]
364
+
365
+ if name[0] == 'h'
366
+ if name == 'hr'
367
+ buffer << "<hr />"
368
+ else
369
+ lvl = name[1].to_i
370
+ attrs = bp['attributes'] || {}
371
+ attrs['class'] = bp['class'] || ''
372
+ attrs['style'] = bp['style'] || ''
373
+
374
+ id = bp['id'] || line.downcase.strip.gsub(/[^a-z0-9_-]+/, '-')
375
+ lid = id
376
+ i = 1
377
+ while @header_idx[lid] != nil
378
+ lid = "#{id}-#{i}"
379
+ i += 1
380
+ end
381
+ id = lid
382
+ attrs['id'] = id
383
+ title = @proc.fmt_line(line)
384
+
385
+ buffer << "#{create_tag(name, attrs)}#{title}</#{name}>"
386
+
387
+ @header_list << {:level => lvl, :id => lid, :title => title, :tag => name}
388
+ @header_idx[lid] = @header_list.length - 1
389
+ end
390
+ return DONE
391
+ elsif name == 'section'
392
+ attrs = bp['attributes'] || {}
393
+ attrs['class'] = bp['class'] || ''
394
+ attrs['style'] = bp['style'] || ''
395
+ attrs['id'] = bp['id'] || ''
396
+ buffer << create_tag('section', attrs)
397
+ @header_list << name
398
+ return DONE
399
+ elsif name == 'end-section'
400
+ buffer << '</section>'
401
+ @header_list << name
402
+ return DONE
403
+ elsif name == 'toc'
404
+ @toc_template = TOC_TEMPLATE.clone
405
+ return COLLECT
406
+ end
407
+ end
408
+
409
+ def collect(line, buffer, indents = '', indent_level = 0)
410
+ if line == '' || !line
411
+ buffer << @proc.fmt_line(@toc_template[:start]) if @toc_template[:start]
412
+ @header_list.each do |entry|
413
+ if entry == 'section'
414
+ buffer << @proc.fmt_line(@toc_template[:section]) if @toc_template[:section]
415
+ elsif entry == 'section_end'
416
+ buffer << @proc.fmt_line(@toc_template[:section_end]) if @toc_template[:section_end]
417
+ elsif entry.is_a?(Hash)
418
+ tpl = @toc_template[entry[:tag]] || @toc_template[:default]
419
+ buffer << @proc.fmt_line(
420
+ tpl.gsub('$id', entry[:id]).gsub('$title', entry[:title]).gsub('$level', entry[:level].to_s)
421
+ )
422
+ end
423
+ end
424
+ buffer << @proc.fmt_line(@toc_template[:end]) if @toc_template[:end]
425
+ return DONE
426
+ else
427
+ key, val = line.split(':', 2)
428
+ @toc_template[key.to_sym] = val
429
+ return COLLECT
430
+ end
431
+ end
432
+ end
433
+
434
+ class InlineMacros
435
+ include Eggshell::MacroHandler
436
+
437
+ def initialize
438
+ @capvar = nil
439
+ @collbuff = nil
440
+ @depth = 0
441
+ end
442
+
443
+ HASH_FMT_DECORATORS = {
444
+ '[*' => '<b>',
445
+ '[**' => '<strong>',
446
+ '[_' => '<i>',
447
+ '[__' => '<em>',
448
+ '*]'=> '</b>',
449
+ '**]' => '</strong>',
450
+ '_]' => '</i>',
451
+ '__]' => '</em>',
452
+ '[-_' => '<u>',
453
+ '_-]' => '</u>',
454
+ '[-' => '<strike>',
455
+ '-]' => '</strike>'
456
+ }.freeze
457
+
458
+ def set_processor(eggshell)
459
+ @proc = eggshell
460
+ @proc.register_macro(self, '[!', '[~', '[^', '[.', '[*', '[**', '[/', '[//', '[_', '[-')
461
+ end
462
+
463
+ def process(buffer, macname, args, lines, depth)
464
+ prefix = macname[0..1]
465
+ textpart = args.shift
466
+ tag = nil
467
+
468
+ case prefix
469
+ when '[^'
470
+ tag = 'sup'
471
+ when '[.'
472
+ tag = 'sub'
473
+ when '[*'
474
+ tag = macname == '[**' ? 'strong' : 'b'
475
+ when '[/'
476
+ tag = macname == '[//' ? 'em' : 'i'
477
+ when '[-'
478
+ tag = 'strike'
479
+ when '[_'
480
+ tag = 'u'
481
+ when '[~'
482
+ tag = 'a'
483
+ link = textpart ? textpart.strip : textpart
484
+ text = nil
485
+ if link == ''
486
+ text = ''
487
+ elsif link.index('; ') == nil
488
+ textpart = link
489
+ args.unshift('href:'+ link);
490
+ else
491
+ textpart, link = link.split('; ')
492
+ link = '' if !link
493
+ args.unshift('href:'+link)
494
+ end
495
+ when '[!'
496
+ tag = 'img'
497
+ args.unshift('src:'+textpart)
498
+ textpart = nil
499
+ end
500
+
501
+ buffer << restructure_html(tag, textpart ? textpart.strip : textpart, args)
502
+ end
503
+
504
+ def restructure_html(tag, text, attributes = [])
505
+ buff = "<#{tag}"
506
+ attributes.each do |attrib|
507
+ key, val = attrib.split(':', 2)
508
+ # @todo html escape?
509
+ if val
510
+ buff = "#{buff} #{key}=\"#{val.gsub('\\|', '|')}\""
511
+ else
512
+ buff = "#{buff} #{key}"
513
+ end
514
+ end
515
+
516
+ if text == nil
517
+ buff += ' />'
518
+ else
519
+ buff = "#{buff}>#{text}</#{tag}>"
520
+ end
521
+ buff
522
+ end
523
+ end
524
+
525
+ # Macros:
526
+ #
527
+ # - `include`
528
+ # - `capture`
529
+ # - `var`
530
+ class BasicMacros
531
+ include Eggshell::MacroHandler
532
+
533
+ def initialize
534
+ @capvar = nil
535
+ @collbuff = nil
536
+ @depth = 0
537
+ end
538
+
539
+ def set_processor(eggshell)
540
+ @proc = eggshell
541
+ @proc.register_macro(self, *%w(! = capture var include process parse_test))
542
+ end
543
+
544
+ def process(buffer, macname, args, lines, depth)
545
+ if macname == '!'
546
+ @proc.vars[:block_params] = @proc.expr_eval(args)
547
+ elsif macname == 'process'
548
+ if args[0]
549
+ proclines = @proc.expr_eval(args[0])
550
+ proclines = proclines.split(/[\r\n]+/) if proclines.is_a?(String)
551
+ buffer << @proc.process(proclines, depth + 1) if proclines.is_a?(Array)
552
+ end
553
+ elsif macname == 'capture'
554
+ # @todo check args for fragment to parse
555
+ return if !lines
556
+ var = args[0]
557
+ @proc.vars[var] = @proc.process(lines, depth)
558
+ elsif macname == 'var' || macname == '='
559
+ # @todo support multiple vars via lines
560
+ # @todo expand value if expression
561
+ if args.length >= 2
562
+ key = args[0]
563
+ val = args[1]
564
+
565
+ if val.is_a?(Array) && val[0].is_a?(Symbol)
566
+ @proc.vars[key] = @proc.expr_eval(val)
567
+ else
568
+ @proc.vars[key] = val
569
+ end
570
+
571
+ # @todo fix this so it's possible to set things like arr[0].setter, etc.
572
+ # ptrs = EE.retrieve_var(key, @proc.vars, {}, true)
573
+ # val = val.is_a?(Array) && val[0] == :var ? EE.retrieve_var(val[1], @proc.vars, {}) : val
574
+ # if ptrs
575
+ # EE.set_var(ptrs[0], ptrs[1][0], ptrs[1][1], val)
576
+ # else
577
+ # @proc.vars[key] = val
578
+ # end
579
+ end
580
+ elsif macname == 'include'
581
+ paths = args[0]
582
+ opts = args[1] || {}
583
+ if opts['encoding']
584
+ opts[:encoding] = opts['encoding']
585
+ else
586
+ opts[:encoding] = 'utf-8'
587
+ end
588
+ if lines && lines.length > 0
589
+ paths = lines
590
+ end
591
+ do_include(paths, buffer, depth, opts)
592
+ end
593
+ end
594
+
595
+ def do_include(paths, buff, depth, opts = {})
596
+ @proc.vars[:include_stack] = [] if !@proc.vars[:include_stack]
597
+ paths = [paths] if !paths.is_a?(Array)
598
+ # @todo check all include paths?
599
+ paths.each do |inc|
600
+ inc = @proc.expand_expr(inc.strip)
601
+ checks = []
602
+ if inc[0] != '/'
603
+ @proc.vars[:include_paths].each do |root|
604
+ checks << "#{root}/#{inc}"
605
+ end
606
+ # @todo if :include_root, expand path and check that it's under the root, otherwise, sandbox
607
+ else
608
+ # sandboxed root include
609
+ if @proc.vars[:include_root]
610
+ checks << "#{@proc.vars[:include_root]}#{inc}"
611
+ else
612
+ checks << inc
613
+ end
614
+ end
615
+
616
+ checks.each do |inc|
617
+ if File.exists?(inc)
618
+ lines = IO.readlines(inc, $/, opts)
619
+ @proc.vars[:include_stack] << inc
620
+ @proc.context.push_line_counter
621
+ begin
622
+ buff << @proc.process(lines, depth + 1)
623
+ @proc._debug("include: 200 #{inc}")
624
+ rescue => ex
625
+ @proc._error("include: 500 #{inc}: #{ex.message}")
626
+ end
627
+
628
+ @proc.vars[:include_stack].pop
629
+ @proc.context.pop_line_counter
630
+ break
631
+ else
632
+ @proc._warn("include: 404 #{inc}")
633
+ end
634
+ end
635
+ end
636
+ end
637
+ end
638
+
639
+ class ControlMacros
640
+ include Eggshell::MacroHandler
641
+
642
+ def initialize
643
+ @stack = []
644
+ @state = []
645
+ @macstack = []
646
+ end
647
+
648
+ def set_processor(eggshell)
649
+ @proc = eggshell
650
+ @proc.register_macro(self, *%w(if elsif else loop for while break next))
651
+ end
652
+
653
+ def process(buffer, macname, args, lines, depth)
654
+ p0 = args.is_a?(Array) ? args[0] : nil
655
+ lines ? lines.delete('') : ''
656
+
657
+ macname = macname.to_sym
658
+ st = @state[depth]
659
+ if !@state[depth]
660
+ st = {:type => macname}
661
+
662
+ @state[depth] = st
663
+ # erase nested state
664
+ @state[depth+1] = nil
665
+ end
666
+
667
+ if macname == :for || macname == :loop
668
+ p0 = p0 || {}
669
+ st[:var] = p0['var']
670
+ st[:start] = p0['start']
671
+ st[:stop] = p0['stop']
672
+ st[:step] = p0['step'] || 1
673
+ st[:iter] = p0['items'] || 'items'
674
+ st[:item] = p0['item'] || 'item'
675
+ st[:counter] = p0['counter'] || 'counter'
676
+ st[:raw] = p0['raw'] # @todo inherit if not set?
677
+ st[:collect] = p0['collect']
678
+ st[:agg_block] = p0['aggregate_block']
679
+
680
+ mbuff = []
681
+ looper = nil
682
+ loop_is_map = false
683
+
684
+ # in for, construct range that can be looped over
685
+ # in loop, detect 'each' method
686
+ if macname == :for
687
+ st[:item] = st[:var]
688
+ looper = Range.new(st[:start], st[:stop]).step(st[:step]).to_a
689
+ elsif macname == :loop
690
+ begin
691
+ looper = st[:iter].is_a?(Array) && st[:iter][0].is_a?(Symbol) ? @proc.expr_eval(st[:iter]) : st[:iter]
692
+ looper = nil if !looper.respond_to?(:each)
693
+ loop_is_map = looper.is_a?(Hash)
694
+ rescue
695
+ end
696
+ end
697
+
698
+ collector = []
699
+ if looper
700
+ counter = 0
701
+ looper.each do |i1, i2|
702
+ # for maps, use key as the counter
703
+ val = nil
704
+ if loop_is_map
705
+ @proc.vars[st[:counter]] = i1
706
+ val = i2
707
+ else
708
+ val = i1
709
+ @proc.vars[st[:counter]] = counter
710
+ end
711
+
712
+ # inject value into :item -- if it's an expression, evaluate first
713
+ @proc.vars[st[:item]] = val.is_a?(Array) && val[0].is_a?(Symbol) ? @proc.expr_eval(val) : val
714
+ # divert lines to collector
715
+ if st[:collect]
716
+ lines.each do |_line|
717
+ if _line.is_a?(String)
718
+ collector << @proc.expand_expr(_line)
719
+ elsif _line.is_a?(Eggshell::Block)
720
+ _line.process(collector)
721
+ end
722
+ end
723
+ else
724
+ process_lines(lines, buffer, depth + 1, st[:raw])
725
+ end
726
+ break if st[:break]
727
+
728
+ counter += 1
729
+ end
730
+ end
731
+
732
+ # since there are lines here, process collected as an aggregated set of blocks
733
+ if collector.length > 0
734
+ if st[:collect] == 'aggregate'
735
+ if st[:agg_block]
736
+ collector.unshift(st[:agg_block])
737
+ end
738
+ process_lines(collector, buffer, depth + 1, false)
739
+ else
740
+ buffer << collector.join("\n")
741
+ end
742
+ end
743
+
744
+ # clear state
745
+ @state[depth] = nil
746
+ elsif macname == :while
747
+ raw = args[1]
748
+ while @proc.expr_eval(p0)
749
+ process_lines(lines, buffer, depth + 1, raw)
750
+ break if st[:break]
751
+ end
752
+ elsif macname == :if || macname == :elsif || macname == :else
753
+ cond = p0
754
+ st[:if] = true if macname == :if
755
+ if st[:cond_count] == nil || macname == :if
756
+ st[:cond_count] = 0
757
+ st[:cond_eval] = false
758
+ st[:cond_met] = false
759
+ end
760
+
761
+ last_action = st[:last_action]
762
+ st[:last_action] = macname.to_sym
763
+
764
+ # @todo more checks (e.g. no elsif after else, no multiple else, etc.)
765
+ if !st[:if] || (macname != :else && !cond)
766
+ # @todo exception?
767
+ return
768
+ end
769
+
770
+ if macname != :else
771
+ if !st[:cond_eval]
772
+ cond_struct = Eggshell::ExpressionEvaluator.struct(cond)
773
+ st[:cond_eval] = @proc.expr_eval(cond_struct)[0]
774
+ end
775
+ else
776
+ st[:cond_eval] = true
777
+ end
778
+
779
+ if st[:cond_eval] && !st[:cond_met]
780
+ st[:cond_met] = true
781
+ process_lines(lines, buffer, depth + 1)
782
+ end
783
+ elsif macname == :break
784
+ lvl = p0 || 1
785
+ i = depth - 1
786
+
787
+ # set breaks at each found loop until # of levels reached
788
+ while i >= 0
789
+ st = @state[i]
790
+ i -= 1
791
+ next if !st
792
+ if st[:type] == :for || st[:type] == :while || st[:type] == :loop
793
+ lvl -= 1
794
+ st[:break] = true
795
+ break if lvl <= 0
796
+ end
797
+ end
798
+ elsif macname == :next
799
+ lvl = p0 || 1
800
+ i = depth - 1
801
+
802
+ # set breaks at each found loop until # of levels reached
803
+ while i >= 0
804
+ st = @state[i]
805
+ i -= 1
806
+ next if !st
807
+ if st[:type] == :for || st[:type] == :while || st[:type] == :loop
808
+ lvl -= 1
809
+ st[:next] = true
810
+ break if lvl <= 0
811
+ end
812
+ end
813
+ end
814
+ end
815
+
816
+ def process_lines(lines, buffer, depth, raw = false)
817
+ return if !lines
818
+ ln = []
819
+ lines.each do |line|
820
+ if line.is_a?(Eggshell::Block)
821
+ if ln.length > 0
822
+ buffer << (raw ? @proc.expand_expr(ln.join("\n")) : @proc.process(ln, depth))
823
+ ln = []
824
+ end
825
+ line.process(buffer, depth)
826
+ if @state[depth-1][:next]
827
+ @state[depth-1][:next] = false
828
+ break
829
+ end
830
+ else
831
+ ln << line
832
+ end
833
+ end
834
+ if ln.length > 0
835
+ buffer << (raw ? @proc.expand_expr(ln.join("\n")) : @proc.process(ln, depth))
836
+ end
837
+ end
838
+
839
+ protected :process_lines
840
+ end
841
+
842
+ # Baseline functions.
843
+ # @todo catch exceptions???
844
+ module StdFunctions
845
+ # Repeats `str` by a given `amt`
846
+ def self.str_repeat(str, amt)
847
+ return str * amt
848
+ end
849
+
850
+ FUNC_NAMES = %w(str_repeat).freeze
851
+ end
852
+
853
+ include Eggshell::Bundles::Bundle
854
+ end