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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/README.md +4 -2
- data/lib/haparanda/compiler.rb +50 -10
- data/lib/haparanda/content_combiner.rb +7 -0
- data/lib/haparanda/handlebars_parser.rb +63 -57
- data/lib/haparanda/handlebars_parser.y +20 -14
- data/lib/haparanda/handlebars_processor.rb +551 -126
- data/lib/haparanda/parser.rb +28 -0
- data/lib/haparanda/standalone_whitespace_handler.rb +223 -0
- data/lib/haparanda/template.rb +34 -3
- data/lib/haparanda/version.rb +1 -1
- data/lib/haparanda/whitespace_stripper.rb +135 -0
- data/lib/haparanda.rb +0 -1
- metadata +19 -4
- data/lib/haparanda/handlebars_compiler.rb +0 -18
- data/lib/haparanda/whitespace_handler.rb +0 -168
|
@@ -10,135 +10,285 @@ module Haparanda
|
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@stack = [value]
|
|
16
|
-
end
|
|
13
|
+
module ValueDigger
|
|
14
|
+
private
|
|
17
15
|
|
|
18
|
-
def
|
|
19
|
-
index = -1
|
|
20
|
-
value = @stack[index]
|
|
16
|
+
def dig_value(value, keys)
|
|
21
17
|
keys.each do |key|
|
|
22
|
-
if
|
|
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
|
|
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:
|
|
43
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
67
|
-
|
|
101
|
+
def top
|
|
102
|
+
@stack.last
|
|
68
103
|
end
|
|
69
104
|
end
|
|
70
105
|
|
|
71
106
|
class Data
|
|
72
107
|
def initialize(data = {})
|
|
73
|
-
@
|
|
108
|
+
@stack = [data]
|
|
74
109
|
end
|
|
75
110
|
|
|
76
111
|
def data(*keys)
|
|
77
|
-
@
|
|
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
|
-
|
|
133
|
+
top[key] = value
|
|
82
134
|
end
|
|
83
135
|
|
|
84
|
-
def with_new_data(&block)
|
|
85
|
-
data =
|
|
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
|
-
@
|
|
141
|
+
@stack.pop
|
|
88
142
|
result
|
|
89
143
|
end
|
|
90
144
|
|
|
91
|
-
|
|
92
|
-
@data.key? method_name
|
|
93
|
-
end
|
|
145
|
+
private
|
|
94
146
|
|
|
95
|
-
def
|
|
96
|
-
@
|
|
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
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
_,
|
|
168
|
-
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
|
-
|
|
173
|
-
|
|
174
|
-
value
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
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
|
-
|
|
219
|
-
|
|
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.
|
|
223
|
-
value =
|
|
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
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
-
|
|
238
|
-
s(:result, result)
|
|
542
|
+
[]
|
|
239
543
|
end
|
|
240
544
|
end
|
|
241
545
|
|
|
242
|
-
def
|
|
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
|
-
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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(
|
|
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(
|
|
683
|
+
options.fn(context)
|
|
299
684
|
else
|
|
300
|
-
options.inverse(
|
|
685
|
+
options.inverse(context)
|
|
301
686
|
end
|
|
302
687
|
end
|
|
303
688
|
|
|
304
|
-
def handle_unless(
|
|
305
|
-
|
|
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(
|
|
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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
end
|
|
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
|
"=" => "="
|
|
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
|