haparanda 0.0.1 → 0.0.2

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.
@@ -10,135 +10,285 @@ module Haparanda
10
10
  end
11
11
  end
12
12
 
13
- class Input
14
- def initialize(value)
15
- @stack = [value]
16
- end
13
+ module ValueDigger
14
+ private
17
15
 
18
- def dig(*keys)
19
- index = -1
20
- value = @stack[index]
16
+ def dig_value(value, keys)
21
17
  keys.each do |key|
22
- if key == :".."
23
- index -= 1
24
- value = @stack[index]
25
- end
26
- next if %i[.. . this].include? key
18
+ next if [DOT, THIS].include? key
27
19
 
28
20
  value = case value
29
21
  when Hash
30
- value[key]
22
+ value.fetch(key) do |k|
23
+ value.fetch(k.to_s, nil)
24
+ end
25
+ when Array
26
+ value[key.to_s.to_i]
31
27
  when nil
32
28
  nil
33
29
  else
34
- value.send key
30
+ value.send key if value.respond_to? key
35
31
  end
36
32
  end
37
33
 
38
34
  value
39
35
  end
36
+ end
37
+
38
+ class Input
39
+ include ValueDigger
40
+
41
+ def initialize(value, parent = nil)
42
+ @value = value
43
+ @parent = parent
44
+ end
45
+
46
+ attr_reader :value
47
+
48
+ def dig(*keys)
49
+ return @parent&.dig(*keys[1..]) if keys.first == UP
50
+
51
+ dig_value(@value, keys)
52
+ end
53
+
54
+ def [](key)
55
+ dig(key)
56
+ end
57
+ end
58
+
59
+ class InputStack
60
+ def initialize(value, compat: false)
61
+ input = Input.new(value)
62
+ @stack = [input]
63
+ @compat = compat
64
+ end
65
+
66
+ def dig(*keys)
67
+ if @compat
68
+ @stack.reverse_each do |item|
69
+ return item.dig(*keys) if item[keys.first]
70
+ end
71
+ nil
72
+ else
73
+ top&.dig(*keys)
74
+ end
75
+ end
76
+
77
+ def [](key)
78
+ dig(key)
79
+ end
40
80
 
41
81
  def with_new_context(value, &block)
42
- # TODO: This prevents a SystemStackError. Make this unnecessary, for
43
- # example by moving the stacking behavior out of the Input class.
44
- if self == value
82
+ # TODO: Remove the self == value case
83
+ if self == value || value == top.value
45
84
  block.call
46
85
  else
47
- @stack.push value
86
+ @stack.push Input.new(value, top)
48
87
  result = block.call
49
88
  @stack.pop
50
89
  result
51
90
  end
52
91
  end
53
92
 
54
- def to_s
55
- @stack.last.to_s
56
- end
57
-
58
- def this
59
- self
60
- end
61
-
62
- def respond_to_missing?(_method_name)
63
- true
93
+ def with_isolated_context(value, &block)
94
+ input = value.is_a?(Input) ? value : Input.new(value)
95
+ @stack.push input
96
+ result = block.call
97
+ @stack.pop
98
+ result
64
99
  end
65
100
 
66
- def method_missing(method_name, *_args)
67
- dig(method_name)
101
+ def top
102
+ @stack.last
68
103
  end
69
104
  end
70
105
 
71
106
  class Data
72
107
  def initialize(data = {})
73
- @data = data
108
+ @stack = [data]
74
109
  end
75
110
 
76
111
  def data(*keys)
77
- @data.dig(*keys)
112
+ @stack.reverse_each do |item|
113
+ case keys.first
114
+ when UP
115
+ keys.shift
116
+ next
117
+ when DOT
118
+ keys.shift
119
+ end
120
+ return item.dig(*keys)
121
+ end
122
+ end
123
+
124
+ def [](key)
125
+ top[key]
126
+ end
127
+
128
+ def key?(key)
129
+ top.key? key
78
130
  end
79
131
 
80
132
  def set_data(key, value)
81
- @data[key] = value
133
+ top[key] = value
82
134
  end
83
135
 
84
- def with_new_data(&block)
85
- data = @data.clone
136
+ def with_new_data(new_data = nil, &block)
137
+ data = top.clone
138
+ data.merge! new_data if new_data
139
+ @stack.push data
86
140
  result = block.call
87
- @data = data
141
+ @stack.pop
88
142
  result
89
143
  end
90
144
 
91
- def respond_to_missing?(method_name)
92
- @data.key? method_name
93
- end
145
+ private
94
146
 
95
- def method_missing(method_name, *_args)
96
- @data[method_name] if @data.key? method_name
147
+ def top
148
+ @stack.last
97
149
  end
98
150
  end
99
151
 
100
152
  class NoData
101
153
  def set_data(key, value); end
102
154
 
103
- def with_new_data(&block)
155
+ def key?(_key)
156
+ false
157
+ end
158
+
159
+ def with_new_data(_new_data = nil, &block)
104
160
  block.call
105
161
  end
106
162
  end
107
163
 
108
164
  class Options
109
- def initialize(fn:, inverse:, hash:, data:)
165
+ def initialize(fn:, inverse:, hash:, data:, block_params:, name: nil)
110
166
  @fn = fn
111
167
  @inverse = inverse
168
+ @name = name
112
169
  @hash = hash
113
170
  @data = data
171
+ @block_params = block_params
114
172
  end
115
173
 
116
- attr_reader :hash, :data
174
+ attr_reader :name, :hash, :data, :block_params
117
175
 
118
- def fn(arg = nil)
119
- @fn&.call(arg)
176
+ def fn(arg = nil, options = {})
177
+ @fn&.call(arg, options)
120
178
  end
121
179
 
122
180
  def inverse(arg = nil)
123
181
  @inverse&.call(arg)
124
182
  end
183
+
184
+ def lookup_property(item, index)
185
+ case item
186
+ when Input, Hash
187
+ item[index.to_sym]
188
+ when Array
189
+ item[index]
190
+ when nil
191
+ nil
192
+ else
193
+ raise NotImplementedError
194
+ end
195
+ end
196
+ end
197
+
198
+ class HelperContext
199
+ def initialize(input_stack)
200
+ @input_stack = input_stack
201
+ end
202
+
203
+ def this
204
+ @input_stack.top.value
205
+ end
206
+ end
207
+
208
+ class BlockParameterList
209
+ include ValueDigger
210
+
211
+ def initialize
212
+ @values = {}
213
+ end
214
+
215
+ def with_new_values(&block)
216
+ values = @values.clone
217
+ result = block.call
218
+ @values = values
219
+ result
220
+ end
221
+
222
+ def key?(key)
223
+ @values.key? key
224
+ end
225
+
226
+ def set_value(key, value)
227
+ @values[key] = value
228
+ end
229
+
230
+ def value(key, *rest)
231
+ dig_value(@values.fetch(key), rest)
232
+ end
233
+ end
234
+
235
+ module Utils
236
+ module_function
237
+
238
+ def escape(str)
239
+ return str if str.is_a? SafeString
240
+
241
+ str.gsub(/[&<>"'`=]/) do |chr|
242
+ ESCAPE[chr]
243
+ end
244
+ end
245
+ end
246
+
247
+ class Segment
248
+ def initialize(name)
249
+ @name = name
250
+ end
251
+
252
+ def to_s
253
+ @name
254
+ end
125
255
  end
126
256
 
127
- def initialize(input, custom_helpers = nil, data: {})
257
+ UP = Segment.new("..")
258
+ DOT = Segment.new(".")
259
+ THIS = Segment.new("THIS")
260
+
261
+ SEGMENT_MAP = {
262
+ ".": DOT,
263
+ this: THIS,
264
+ "..": UP
265
+ }.freeze
266
+
267
+ def initialize(input, helpers: {}, partials: {}, data: {}, log: nil,
268
+ compat: false, explicit_partial_context: false, no_escape: false)
128
269
  super()
129
270
 
130
271
  self.require_empty = false
131
272
 
132
- @input = Input.new(input)
273
+ @input_stack = InputStack.new(input, compat: compat)
133
274
  @data = data ? Data.new(data) : NoData.new
275
+ @helper_context = HelperContext.new(@input_stack)
276
+ @block_parameter_list = BlockParameterList.new
277
+
278
+ @data.set_data(:root, @input_stack.top) unless @data.key?(:root)
134
279
 
135
- custom_helpers ||= {}
136
280
  @helpers = {
137
281
  if: method(:handle_if),
138
282
  unless: method(:handle_unless),
139
283
  with: method(:handle_with),
140
- each: method(:handle_each)
141
- }.merge(custom_helpers)
284
+ each: method(:handle_each),
285
+ log: method(:handle_log),
286
+ lookup: method(:handle_lookup)
287
+ }.merge(helpers)
288
+ @partials = Data.new(partials.transform_keys(&:to_s))
289
+ @log = log || method(:default_log)
290
+ @explicit_partial_context = explicit_partial_context
291
+ @escape_values = !no_escape
142
292
  end
143
293
 
144
294
  def apply(expr)
@@ -148,32 +298,125 @@ module Haparanda
148
298
 
149
299
  def process_root(expr)
150
300
  _, statements = expr
151
- process(statements)
301
+ if statements
302
+ process(statements)
303
+ else
304
+ s(:result, nil)
305
+ end
152
306
  end
153
307
 
154
308
  def process_mustache(expr)
155
309
  _, path, params, hash, escaped, _strip = expr
156
310
  params = process(params)[1]
157
- hash = process(hash)[1] if hash
158
- data, elements = path_segments process(path)
159
- value = lookup_path(data, elements)
160
- value = execute_in_context(value, params, hash: hash) if value.respond_to? :call
311
+ hash = extract_hash(hash)
312
+ value, name = lookup_value(path)
313
+
314
+ if value.nil?
315
+ value = @helpers[:helperMissing]
316
+ raise "Missing helper: \"#{name}\"" if value.nil? && !params.empty?
317
+ end
318
+
319
+ if value.respond_to? :call
320
+ value = execute_in_context(value, params, name: name, hash: hash)
321
+ end
322
+
161
323
  value = value.to_s
162
- value = escape(value) if escaped
324
+ value = Utils.escape(value) if @escape_values && escaped
163
325
  s(:result, value)
164
326
  end
165
327
 
166
328
  def process_block(expr)
167
- _, name, params, hash, program, inverse_chain, = expr
168
- hash = process(hash)[1] if hash
329
+ _, path, params, hash, program, inverse_chain, = expr
330
+ hash = extract_hash hash
169
331
  else_program = inverse_chain.sexp_body[1] if inverse_chain
170
332
  arguments = process(params)[1]
171
333
 
172
- path = process(name)
173
- data, elements = path_segments(path)
174
- value = lookup_path(data, elements)
334
+ value, name = lookup_value(path)
335
+
336
+ evaluate_program_with_value(value, arguments, program, else_program, hash, name: name)
337
+ end
338
+
339
+ def process_partial(expr)
340
+ _, name, context, hash, indent, = expr
341
+
342
+ values = process(context)[1]
343
+ if values.length > 1
344
+ raise "Unsupported number of partial arguments: #{values.length} - #{expr.line}"
345
+ end
346
+
347
+ value = values.first
348
+
349
+ partial = lookup_partial(name)
350
+ partial_f = if partial.is_a?(Sexp)
351
+ lambda do |value|
352
+ @input_stack.with_isolated_context(value) { process(partial) }
353
+ end
354
+ else
355
+ partial
356
+ end
357
+
358
+ hash = extract_hash hash
359
+ result = with_block_params(hash.keys, hash.values) do
360
+ value ||= @input_stack.top unless @explicit_partial_context
361
+ partial_f.call(value)
362
+ end
363
+
364
+ apply_indent result, indent
365
+
366
+ result
367
+ end
368
+
369
+ # rubocop:todo Metrics/MethodLength
370
+ # rubocop:todo Metrics/AbcSize
371
+ def process_partial_block(expr)
372
+ _, name, context, _hash, partial_block = expr
373
+
374
+ values = process(context)[1]
375
+ value = values.first
376
+
377
+ current_partial_block = @data.data(:"partial-block")
378
+
379
+ @data.with_new_data do
380
+ data = @data
381
+ processor = self
382
+ partial_block_wrapper = lambda do |value|
383
+ data.with_new_data do
384
+ data.set_data(:"partial-block", current_partial_block)
385
+ @input_stack.with_isolated_context(value) { processor.process(partial_block) }
386
+ end
387
+ end
388
+
389
+ @data.set_data(:"partial-block", partial_block_wrapper)
390
+
391
+ partial_block&.sexp_body&.each do |sexp|
392
+ process(sexp) if sexp.sexp_type == :directive_block
393
+ end
394
+
395
+ partial = lookup_partial(name, raise_error: false)
396
+ partial ||= partial_block_wrapper
397
+ partial_f = if partial.is_a?(Sexp)
398
+ lambda do |value|
399
+ @input_stack.with_isolated_context(value) { process(partial) }
400
+ end
401
+ else
402
+ partial
403
+ end
404
+
405
+ value ||= @input_stack.top unless @explicit_partial_context
406
+ partial_f.call(value)
407
+ end
408
+ end
409
+ # rubocop:enable Metrics/AbcSize
410
+ # rubocop:enable Metrics/MethodLength
411
+
412
+ def process_directive_block(expr)
413
+ _, name, params, _hash, program, _inverse_chain, = expr
414
+ name = name.dig(2, 1)
415
+ raise "Only 'inline' is supported, got #{name}" unless name == "inline"
175
416
 
176
- evaluate_program_with_value(value, arguments, program, else_program, hash)
417
+ args = process(params)[1]
418
+ partial_name = args[0]
419
+ @partials.set_data(partial_name, program)
177
420
  end
178
421
 
179
422
  def process_statements(expr)
@@ -194,8 +437,16 @@ module Haparanda
194
437
 
195
438
  def process_path(expr)
196
439
  _, data, *segments = expr
197
- segments = segments.each_slice(2).map { |elem, _sep| elem[1].to_sym }
198
- s(:segments, data, segments)
440
+ path_segments = []
441
+ name_parts = segments.each_slice(2).flat_map do |elem, sep|
442
+ sep = sep&.at(1)
443
+ part = elem[1].to_sym
444
+ part = SEGMENT_MAP.fetch(part, part) unless elem[2]
445
+ path_segments << part
446
+ [part, sep]
447
+ end
448
+ name = name_parts.join
449
+ s(:segments, data, name, path_segments)
199
450
  end
200
451
 
201
452
  def process_exprs(expr)
@@ -213,116 +464,298 @@ module Haparanda
213
464
  s(:hash, hash)
214
465
  end
215
466
 
467
+ def process_sub_expression(expr)
468
+ _, path, params, hash = expr
469
+ value, name = lookup_value(path)
470
+
471
+ arguments = process(params)[1]
472
+ hash = extract_hash hash
473
+
474
+ result = execute_in_context(value, arguments, hash: hash, name: name)
475
+ s(:result, result)
476
+ end
477
+
478
+ LOG_LEVELS = %w[debug info warn error].freeze
479
+
216
480
  private
217
481
 
218
- def evaluate_program_with_value(value, arguments, program, else_program, hash)
219
- fn = make_contextual_lambda(program)
482
+ # rubocop:todo Metrics/PerceivedComplexity
483
+ # rubocop:todo Metrics/MethodLength
484
+ # rubocop:todo Metrics/AbcSize
485
+ # rubocop:todo Metrics/CyclomaticComplexity
486
+ def evaluate_program_with_value(value, arguments, program, else_program, hash,
487
+ name: nil)
488
+ block_params = extract_block_param_names(program)
489
+ fn = make_contextual_lambda(program, block_params)
220
490
  inverse = make_contextual_lambda(else_program)
221
491
 
222
- if value.respond_to? :call
223
- value = execute_in_context(value, arguments, fn: fn, inverse: inverse, hash: hash)
492
+ if value.nil? && (hash.any? || arguments.any?)
493
+ value = @helpers[:helperMissing] or raise "Missing helper: \"#{name}\""
494
+ end
495
+
496
+ while value.respond_to?(:call)
497
+ value = execute_in_context(value, arguments, name: name,
498
+ fn: fn, inverse: inverse, hash: hash,
499
+ block_params: block_params&.count)
500
+ end
501
+ return s(:result, value.to_s) if arguments.any?
502
+
503
+ if (helper = @helpers[:blockHelperMissing])
504
+ value = execute_in_context(helper, [value], name: name,
505
+ fn: fn, inverse: inverse, hash: hash,
506
+ block_params: block_params&.count)
224
507
  return s(:result, value.to_s)
225
508
  end
226
509
 
227
- case value
228
- when Array
229
- return s(:result, inverse.call(@input)) if value.empty?
510
+ result = case value
511
+ when Array
512
+ if value.empty?
513
+ inverse.call(@input_stack)
514
+ else
515
+ parts = value.each_with_index.map do |elem, index|
516
+ @data.set_data(:index, index)
517
+ fn.call(elem)
518
+ end
519
+ parts.join
520
+ end
521
+ when true
522
+ fn.call(@input_stack)
523
+ when false, nil
524
+ inverse.call(@input_stack)
525
+ else
526
+ fn.call(value)
527
+ end
528
+
529
+ s(:result, result)
530
+ end
531
+ # rubocop:enable Metrics/AbcSize
532
+ # rubocop:enable Metrics/MethodLength
533
+ # rubocop:enable Metrics/PerceivedComplexity
534
+ # rubocop:enable Metrics/CyclomaticComplexity
230
535
 
231
- parts = value.each_with_index.map do |elem, index|
232
- @data.set_data(:index, index)
233
- fn.call(elem)
234
- end
235
- s(:result, parts.join)
536
+ def extract_block_param_names(program)
537
+ return [] unless program&.sexp_type == :program
538
+
539
+ if (params_definition = program[1])
540
+ params_definition.sexp_body.map { _1[1].to_sym }
236
541
  else
237
- result = value ? fn.call(value) : inverse.call(@input)
238
- s(:result, result)
542
+ []
239
543
  end
240
544
  end
241
545
 
242
- def make_contextual_lambda(program)
546
+ def extract_hash(expr)
547
+ if expr
548
+ process(expr)[1]
549
+ else
550
+ {}
551
+ end
552
+ end
553
+
554
+ def make_contextual_lambda(program, block_param_names = [])
243
555
  if program
244
- ->(item) { @input.with_new_context(item) { apply(program) } }
556
+ if block_param_names.any?
557
+ lambda { |item, options = {}|
558
+ with_new_context(item, options[:data]) do
559
+ with_block_params(block_param_names, options[:block_params]) do
560
+ apply(program)
561
+ end
562
+ end
563
+ }
564
+ else
565
+ lambda { |item, options = {}|
566
+ with_new_context(item, options[:data]) { apply(program) }
567
+ }
568
+ end
245
569
  else
246
570
  ->(_item) { "" }
247
571
  end
248
572
  end
249
573
 
574
+ def with_new_context(item, data, &)
575
+ @data.with_new_data(data) do
576
+ @partials.with_new_data do
577
+ @input_stack.with_new_context(item, &)
578
+ end
579
+ end
580
+ end
581
+
582
+ def with_block_params(block_param_names, block_param_values, &block)
583
+ @block_parameter_list.with_new_values do
584
+ if block_param_values
585
+ block_param_names.zip(block_param_values) do |name, value|
586
+ @block_parameter_list.set_value(name, value)
587
+ end
588
+ end
589
+ block.call
590
+ end
591
+ end
592
+
250
593
  def evaluate_expr(expr)
251
- path = process(expr)
252
- case path.sexp_type
253
- when :segments
254
- data, elements = path.sexp_body
255
- value = lookup_path(data, elements)
256
- value = execute_in_context(value) if value.respond_to? :call
594
+ case expr.sexp_type
595
+ when :path
596
+ value, name = lookup_value(expr)
597
+ value = execute_in_context(value, name: name) if value.respond_to? :call
257
598
  value
258
599
  when :undefined, :null
259
600
  nil
260
601
  else
261
- path[1]
602
+ process(expr)[1]
262
603
  end
263
604
  end
264
605
 
606
+ def lookup_value(expr)
607
+ path = process(expr)
608
+ data, name, elements = path_segments(path)
609
+ value = if data
610
+ @data.data(*elements)
611
+ elsif @block_parameter_list.key?(elements.first)
612
+ @block_parameter_list.value(*elements)
613
+ elsif elements.one? && @helpers.key?(elements.first)
614
+ @helpers[elements.first]
615
+ else
616
+ @input_stack.dig(*elements)
617
+ end
618
+ return value, name
619
+ end
620
+
621
+ # TODO: Remove boolean parameter code smell
622
+ def lookup_partial(expr, raise_error: true)
623
+ path = process(expr)
624
+ data, name, elements = path_segments(path)
625
+
626
+ result = if data
627
+ @data.data(*elements)
628
+ else
629
+ @partials.data(name)
630
+ end
631
+
632
+ raise KeyError, "The partial \"#{name}\" could not be found" if !result && raise_error
633
+
634
+ result
635
+ end
636
+
265
637
  def path_segments(path)
266
638
  case path.sexp_type
267
639
  when :segments
268
- data, elements = path.sexp_body
640
+ data, name, elements = path.sexp_body
269
641
  when :undefined, :null
270
642
  elements = [path.sexp_type]
643
+ name = elements.join
271
644
  else
272
645
  elements = [path[1]]
646
+ name = elements.join
273
647
  end
274
- return data, elements
648
+ return data, name, elements
275
649
  end
276
650
 
277
- def lookup_path(data, elements)
278
- if data
279
- @data.data(*elements)
280
- elsif elements.count == 1 && @helpers.key?(elements.first)
281
- @helpers[elements.first]
282
- else
283
- @input.dig(*elements)
651
+ def execute_in_context(callable, params = [], name:,
652
+ fn: nil, inverse: nil, block_params: 0, hash: nil)
653
+ arity = callable.arity
654
+ num_params = params.count
655
+ arity = num_params + 2 if arity < 0
656
+ raise_arity_error(arity, name, is_block: !fn.nil?) if arity > num_params + 2
657
+
658
+ params = params.take(arity) if num_params > arity
659
+
660
+ if arity > num_params
661
+ options = Options.new(name: name,
662
+ fn: fn, inverse: inverse,
663
+ block_params: block_params, hash: hash,
664
+ data: @data)
665
+ params.push options
284
666
  end
667
+ params.unshift @helper_context.this if arity > num_params + 1
668
+ @helper_context.instance_exec(*params, &callable)
285
669
  end
286
670
 
287
- def execute_in_context(callable, params = [], fn: nil, inverse: nil, hash: nil)
288
- num_params = callable.arity
289
- raise NotImplementedError if num_params < 0
290
-
291
- args = [*params, Options.new(fn: fn, inverse: inverse, hash: hash, data: @data)]
292
- args = args.take(num_params)
293
- @input.instance_exec(*args, &callable)
671
+ def raise_arity_error(arity, name, is_block: false)
672
+ expected = arity - 2
673
+ identifier = is_block ? "##{name}" : name
674
+ raise ArgumentError,
675
+ "Expected #{expected} argument#{'s' if expected > 1} for #{identifier}"
294
676
  end
295
677
 
296
- def handle_if(value, options)
678
+ def handle_if(context, *values, options)
679
+ raise ArgumentError, "#if requires exactly one argument" unless values.size == 1
680
+
681
+ value = values.first
297
682
  if value
298
- options.fn(@input)
683
+ options.fn(context)
299
684
  else
300
- options.inverse(@input)
685
+ options.inverse(context)
301
686
  end
302
687
  end
303
688
 
304
- def handle_unless(value, options)
305
- options.fn(@input) unless value
689
+ def handle_unless(context, *values, options)
690
+ raise ArgumentError, "#unless requires exactly one argument" unless values.size == 1
691
+
692
+ value = values.first
693
+ options.fn(context) unless value
306
694
  end
307
695
 
308
- def handle_with(value, options)
696
+ def handle_with(_context, *values, options)
697
+ raise ArgumentError, "#with requires exactly one argument" unless values.size == 1
698
+
699
+ value = values.first
309
700
  if value
310
- options.fn(value)
701
+ options.fn(value, block_params: [value])
311
702
  else
312
703
  options.inverse(value)
313
704
  end
314
705
  end
315
706
 
316
- def handle_each(value, options)
707
+ def handle_each(_context, value, options)
317
708
  return unless value
318
709
 
319
- value = value.values if value.is_a? Hash
320
- @data.with_new_data do
321
- value.each_with_index.map do |item, index|
322
- @data.set_data(:index, index)
323
- options.fn(item)
324
- end.join
710
+ last = value.respond_to?(:length) ? value.length - 1 : -1
711
+ data = options.data
712
+ data.with_new_data do
713
+ unless value.is_a? Hash
714
+ value = value.each_with_index.lazy.map { |item, index| [index, item] }
715
+ end
716
+ items = value.each_with_index.map do |(key, item), index|
717
+ data.set_data(:key, key)
718
+ data.set_data(:index, index)
719
+ data.set_data(:first, index == 0)
720
+ data.set_data(:last, index == last)
721
+ options.fn(item, block_params: [item, index])
722
+ end
723
+ items.to_a.join
724
+ end
725
+ end
726
+
727
+ def handle_log(_context, *values, options)
728
+ level = options.hash[:level] || @data.data(:level) || 1
729
+ @log.call(level, *values)
730
+ nil
731
+ end
732
+
733
+ def handle_lookup(_context, item, index, options)
734
+ options.lookup_property(item, index)
735
+ end
736
+
737
+ def apply_indent(result, indent)
738
+ if indent && (indent_text = indent[1])
739
+ str = result[1].lines.map { |line| "#{indent_text}#{line}" }
740
+ result[1] = str
741
+ end
742
+ end
743
+
744
+ def default_log(level, *values)
745
+ case level
746
+ when String
747
+ level = Integer(level, exception: false) || LOG_LEVELS.index(level.downcase)
325
748
  end
749
+ level ||= Logger::UNKNOWN
750
+ logger.add(level, values.join(" "))
751
+ end
752
+
753
+ def logger
754
+ @logger ||= Logger.new($stderr)
755
+ end
756
+
757
+ def raise_helper_missing(name)
758
+ raise "Missing helper: \"#{name}\""
326
759
  end
327
760
 
328
761
  ESCAPE = {
@@ -335,13 +768,5 @@ module Haparanda
335
768
  "=" => "&#x3D;"
336
769
  }.freeze
337
770
  private_constant :ESCAPE
338
-
339
- def escape(str)
340
- return str if str.is_a? SafeString
341
-
342
- str.gsub(/[&<>"'`=]/) do |chr|
343
- ESCAPE[chr]
344
- end
345
- end
346
771
  end
347
772
  end