haparanda 0.0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/COPYING.LIB +504 -0
- data/README.md +123 -0
- data/lib/haparanda/compiler.rb +29 -0
- data/lib/haparanda/content_combiner.rb +48 -0
- data/lib/haparanda/handlebars_compiler.rb +18 -0
- data/lib/haparanda/handlebars_lexer.rb +297 -0
- data/lib/haparanda/handlebars_lexer.rex +135 -0
- data/lib/haparanda/handlebars_parser.output +1691 -0
- data/lib/haparanda/handlebars_parser.rb +880 -0
- data/lib/haparanda/handlebars_parser.y +358 -0
- data/lib/haparanda/handlebars_processor.rb +347 -0
- data/lib/haparanda/template.rb +18 -0
- data/lib/haparanda/version.rb +6 -0
- data/lib/haparanda/whitespace_handler.rb +168 -0
- data/lib/haparanda.rb +15 -0
- metadata +94 -0
@@ -0,0 +1,358 @@
|
|
1
|
+
class Haparanda::HandlebarsParser
|
2
|
+
rule
|
3
|
+
start root
|
4
|
+
|
5
|
+
# %ebnf
|
6
|
+
|
7
|
+
# %%
|
8
|
+
|
9
|
+
# Based on src/handlebars.yy in handlebars-parser. Some extra rules were added
|
10
|
+
# because racc does not support certain things that jison supports.
|
11
|
+
#
|
12
|
+
# For example, the '*' to signify zero or more items. Rules that need this were
|
13
|
+
# generaly split into 'none | items', plus a new rule defining items as 'item |
|
14
|
+
# items item'.
|
15
|
+
#
|
16
|
+
# Similarly, an extra rule is needed to replace '?' signifyling zero or one of
|
17
|
+
# an item.
|
18
|
+
#
|
19
|
+
# src/handlebars.yy in handlebars-parser is covered by the ICS license. See README.md
|
20
|
+
# for details.
|
21
|
+
|
22
|
+
root
|
23
|
+
: program { result = s(:root, val[0]) }
|
24
|
+
;
|
25
|
+
|
26
|
+
program
|
27
|
+
: none
|
28
|
+
| statements
|
29
|
+
;
|
30
|
+
|
31
|
+
# Extra rule needed for racc to parse list of one or more statements
|
32
|
+
statements
|
33
|
+
: statement { result = s(:statements, val[0]) }
|
34
|
+
| statements statement { result << val[1] }
|
35
|
+
;
|
36
|
+
|
37
|
+
statement
|
38
|
+
: mustache
|
39
|
+
| block
|
40
|
+
| rawBlock
|
41
|
+
| partial
|
42
|
+
| partialBlock
|
43
|
+
| content
|
44
|
+
| COMMENT {
|
45
|
+
result = s(:comment, strip_comment(val[0]), strip_flags(val[0], val[0]))
|
46
|
+
.line(self.lexer.lineno)
|
47
|
+
};
|
48
|
+
|
49
|
+
content:
|
50
|
+
CONTENT {
|
51
|
+
result = s(:content, val[0])
|
52
|
+
result.line = self.lexer.lineno
|
53
|
+
}
|
54
|
+
;
|
55
|
+
|
56
|
+
# Extra rule needed to replace content*
|
57
|
+
contents:
|
58
|
+
: none {
|
59
|
+
result = s(:content, "")
|
60
|
+
result.line = self.lexer.lineno
|
61
|
+
}
|
62
|
+
| contentList
|
63
|
+
;
|
64
|
+
|
65
|
+
# Extra rule needed for racc to parse list of one or more contents
|
66
|
+
contentList:
|
67
|
+
content
|
68
|
+
| contentList CONTENT {
|
69
|
+
result[1] += val[1]
|
70
|
+
}
|
71
|
+
;
|
72
|
+
|
73
|
+
rawBlock
|
74
|
+
: openRawBlock contents END_RAW_BLOCK { result = prepare_raw_block(val[0], val[1], val[2]) }
|
75
|
+
;
|
76
|
+
|
77
|
+
openRawBlock
|
78
|
+
: OPEN_RAW_BLOCK helperName exprs hash CLOSE_RAW_BLOCK {
|
79
|
+
result = s(:open_raw, *val[1..3], strip_flags(val[0], val[4])).line(self.lexer.lineno)
|
80
|
+
}
|
81
|
+
;
|
82
|
+
|
83
|
+
block
|
84
|
+
: openBlock program inverseChain closeBlock { result = prepare_block(val[0], val[1], val[2], val[3], false) }
|
85
|
+
| openInverse program optInverseAndProgram closeBlock { result = prepare_block(val[0], val[1], val[2], val[3], true) }
|
86
|
+
;
|
87
|
+
|
88
|
+
openBlock
|
89
|
+
: OPEN_BLOCK helperName exprs hash blockParams CLOSE {
|
90
|
+
decorator, _escaped = interpret_open_token(val[0])
|
91
|
+
type = decorator ? :open_directive : :open
|
92
|
+
result = s(type, *val[1..4], strip_flags(val[0], val[5]))
|
93
|
+
}
|
94
|
+
;
|
95
|
+
|
96
|
+
openInverse
|
97
|
+
: OPEN_INVERSE helperName exprs hash blockParams CLOSE { result = s(:open, val[1], val[2], val[3], val[4], strip_flags(val[0], val[5])) }
|
98
|
+
;
|
99
|
+
|
100
|
+
openInverseChain
|
101
|
+
: OPEN_INVERSE_CHAIN helperName exprs hash blockParams CLOSE { result = s(:open, val[1], val[2], val[3], val[4], strip_flags(val[0], val[5])) }
|
102
|
+
;
|
103
|
+
|
104
|
+
# Extra rule needed for racc to parse zero or one of inverseAndProgram
|
105
|
+
optInverseAndProgram
|
106
|
+
: none
|
107
|
+
| inverseAndProgram
|
108
|
+
;
|
109
|
+
|
110
|
+
inverseAndProgram
|
111
|
+
: INVERSE program { result = s(:inverse, nil, val[1], strip_flags(val[0], val[0]), nil) }
|
112
|
+
;
|
113
|
+
|
114
|
+
inverseChain
|
115
|
+
: none
|
116
|
+
| openInverseChain program inverseChain {
|
117
|
+
block = prepare_block(val[0], val[1], val[2], nil, false)
|
118
|
+
result = s(:inverse, nil, block, nil, nil)
|
119
|
+
}
|
120
|
+
| inverseAndProgram
|
121
|
+
;
|
122
|
+
|
123
|
+
closeBlock
|
124
|
+
: OPEN_ENDBLOCK helperName CLOSE { result = s(:close, val[1], strip_flags(val[0], val[2])) }
|
125
|
+
;
|
126
|
+
|
127
|
+
mustache
|
128
|
+
: OPEN expr exprs hash CLOSE { result = prepare_mustache(*val) }
|
129
|
+
| OPEN_UNESCAPED expr exprs hash CLOSE_UNESCAPED { result = prepare_mustache(*val) }
|
130
|
+
;
|
131
|
+
|
132
|
+
partial
|
133
|
+
: OPEN_PARTIAL expr exprs hash CLOSE {
|
134
|
+
result = s(:partial, val[1], val[2], val[3], strip_flags(val[0], val[4]))
|
135
|
+
.line(self.lexer.lineno)
|
136
|
+
}
|
137
|
+
;
|
138
|
+
|
139
|
+
partialBlock
|
140
|
+
: openPartialBlock program closeBlock { result = prepare_partial_block(*val) }
|
141
|
+
;
|
142
|
+
|
143
|
+
openPartialBlock
|
144
|
+
: OPEN_PARTIAL_BLOCK expr exprs hash CLOSE { result = s(:open_partial, val[1], val[2], val[3], strip_flags(val[0], val[4])) }
|
145
|
+
;
|
146
|
+
|
147
|
+
expr
|
148
|
+
: helperName
|
149
|
+
| sexpr
|
150
|
+
;
|
151
|
+
|
152
|
+
# Extra rule needed to replace all cases of expr*
|
153
|
+
exprs:
|
154
|
+
: none { result = s(:exprs) }
|
155
|
+
| exprList
|
156
|
+
;
|
157
|
+
|
158
|
+
# Extra rule needed for racc to parse list of one or more exprs
|
159
|
+
exprList
|
160
|
+
: expr { result = s(:exprs, val[0]) }
|
161
|
+
| exprList expr { result.push(val[1]) }
|
162
|
+
;
|
163
|
+
|
164
|
+
sexpr
|
165
|
+
: OPEN_SEXPR expr exprs hash CLOSE_SEXPR {
|
166
|
+
result = s(:sub_expression, val[1], val[2], val[3]).line self.lexer.lineno
|
167
|
+
};
|
168
|
+
|
169
|
+
hash
|
170
|
+
: none
|
171
|
+
| hashSegments { result = result.line(self.lexer.lineno) }
|
172
|
+
;
|
173
|
+
|
174
|
+
# Extra rule needed for racc to parse list of one or more hash segments
|
175
|
+
hashSegments
|
176
|
+
hashSegment { result = s(:hash, val[0]) }
|
177
|
+
| hashSegments hashSegment { result.push(val[1]) }
|
178
|
+
;
|
179
|
+
|
180
|
+
hashSegment
|
181
|
+
: KEY_ASSIGN expr { result = s(:hash_pair, val[0], val[1]).line(self.lexer.lineno) }
|
182
|
+
;
|
183
|
+
|
184
|
+
blockParams
|
185
|
+
: none
|
186
|
+
| OPEN_BLOCK_PARAMS idSequence CLOSE_BLOCK_PARAMS { result = s(:block_params, *val[1]) }
|
187
|
+
;
|
188
|
+
|
189
|
+
# Extra rule needed for racc to parse list of one or more IDs
|
190
|
+
idSequence
|
191
|
+
: ID { result = [id(val[0])] }
|
192
|
+
| idSequence ID { result << id(val[1]) }
|
193
|
+
;
|
194
|
+
|
195
|
+
helperName
|
196
|
+
: path
|
197
|
+
| dataName
|
198
|
+
| STRING { result = s(:string, val[0]).line(self.lexer.lineno) }
|
199
|
+
| NUMBER { result = s(:number, process_number(val[0])).line(self.lexer.lineno) }
|
200
|
+
| BOOLEAN { result = s(:boolean, val[0] == "true").line(self.lexer.lineno) }
|
201
|
+
| UNDEFINED { result = s(:undefined).line(self.lexer.lineno) }
|
202
|
+
| NULL { result = s(:null).line(self.lexer.lineno) }
|
203
|
+
;
|
204
|
+
|
205
|
+
dataName
|
206
|
+
: DATA pathSegments { result = prepare_path(true, false, val[1], self.lexer.lineno) }
|
207
|
+
;
|
208
|
+
|
209
|
+
path
|
210
|
+
: sexpr SEP pathSegments {
|
211
|
+
# NOTE: Separator is always parsed as '/'
|
212
|
+
result = prepare_path(false, false, [val[0], s(:sep, "/"), *val[2]], self.lexer.lineno)
|
213
|
+
}
|
214
|
+
| pathSegments { result = prepare_path(false, false, val[0], self.lexer.lineno) }
|
215
|
+
;
|
216
|
+
|
217
|
+
pathSegments
|
218
|
+
: pathSegments SEP ID { result.push(s(:sep, val[1]), id(val[2])) }
|
219
|
+
| ID { result = [id(val[0])] }
|
220
|
+
;
|
221
|
+
|
222
|
+
# Extra rule needed to define none, used in the added rules
|
223
|
+
none
|
224
|
+
: { result = nil }
|
225
|
+
;
|
226
|
+
|
227
|
+
---- header
|
228
|
+
require "sexp"
|
229
|
+
|
230
|
+
---- inner
|
231
|
+
attr_reader :lexer
|
232
|
+
|
233
|
+
def parse(str)
|
234
|
+
@lexer = HandlebarsLexer.new
|
235
|
+
lexer.scan_setup(str)
|
236
|
+
do_parse
|
237
|
+
end
|
238
|
+
|
239
|
+
def next_token
|
240
|
+
lexer.next_token
|
241
|
+
end
|
242
|
+
|
243
|
+
# Use pure ruby racc imlementation for debugging
|
244
|
+
def do_parse
|
245
|
+
_racc_do_parse_rb(_racc_setup(), false)
|
246
|
+
end
|
247
|
+
|
248
|
+
def process_number(str)
|
249
|
+
if str =~ /\./
|
250
|
+
str.to_f
|
251
|
+
else
|
252
|
+
str.to_i
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def strip_flags(start, finish)
|
257
|
+
s(:strip, start[2] == "~", finish[-3] == "~")
|
258
|
+
end
|
259
|
+
|
260
|
+
def strip_comment(comment)
|
261
|
+
comment.sub(/^\{\{~?!-?-?/, "").sub(/-?-?~?\}\}$/, "")
|
262
|
+
end
|
263
|
+
|
264
|
+
def id(val)
|
265
|
+
if (match = /\A\[(.*)\]\Z/.match val)
|
266
|
+
# TODO: Mark as having had square brackets
|
267
|
+
s(:id, match[1])
|
268
|
+
else
|
269
|
+
s(:id, val)
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
def interpret_open_token(open)
|
274
|
+
marker = open[2..-1][-1]
|
275
|
+
decorator = marker == "*"
|
276
|
+
escaped = !["{", "&"].include?(marker)
|
277
|
+
return decorator, escaped
|
278
|
+
end
|
279
|
+
|
280
|
+
def prepare_path(data, sexpr, parts, loc)
|
281
|
+
tail = []
|
282
|
+
parts.each_slice(2) do |part, sep|
|
283
|
+
if ["..", ".", "this"].include? part[1]
|
284
|
+
unless tail.empty?
|
285
|
+
path = tail.map { _1[1] }.join + part[1]
|
286
|
+
# TODO: keep track of the position in the line as well
|
287
|
+
raise ParseError, "Invalid path: #{path} - #{loc}"
|
288
|
+
end
|
289
|
+
next
|
290
|
+
end
|
291
|
+
|
292
|
+
tail << part
|
293
|
+
tail << sep if sep
|
294
|
+
end
|
295
|
+
# TODO: Handle sexpr
|
296
|
+
s(:path, data, *parts).line loc
|
297
|
+
end
|
298
|
+
|
299
|
+
def prepare_mustache(open, path, params, hash, close)
|
300
|
+
decorator, escaped = interpret_open_token(open)
|
301
|
+
type = decorator ? :directive : :mustache
|
302
|
+
s(type, path, params, hash, escaped, strip_flags(open, close)).line(self.lexer.lineno)
|
303
|
+
end
|
304
|
+
|
305
|
+
def prepare_partial_block(open, program, close)
|
306
|
+
_, name, params, hash, open_strip = *open
|
307
|
+
_, close_name, close_strip = *close
|
308
|
+
|
309
|
+
validate_close(name, close_name)
|
310
|
+
|
311
|
+
s(:partial_block, name, params, hash, program, open_strip, close_strip)
|
312
|
+
.line(self.lexer.lineno)
|
313
|
+
end
|
314
|
+
|
315
|
+
def prepare_raw_block(open, contents, close)
|
316
|
+
_open_type, path, params, hash, open_strip = *open
|
317
|
+
name = path[2][1]
|
318
|
+
validate_close(name, close)
|
319
|
+
close_strip = strip_flags(close, close)
|
320
|
+
s(:block, path, params, hash, contents, nil, open_strip, close_strip).line(self.lexer.lineno)
|
321
|
+
end
|
322
|
+
|
323
|
+
def prepare_block(open, program, inverse_chain, close, inverted)
|
324
|
+
open_type, name, params, hash, block_params, open_strip = *open
|
325
|
+
directive = open_type == :open_directive
|
326
|
+
|
327
|
+
raise ParseError, "Unexpected inverse" if directive && inverse_chain
|
328
|
+
|
329
|
+
if close
|
330
|
+
_, close_name, close_strip = *close
|
331
|
+
validate_close(name, close_name)
|
332
|
+
end
|
333
|
+
|
334
|
+
# TODO: Get close_strip from inverse_chain if close is nil
|
335
|
+
|
336
|
+
if inverted
|
337
|
+
raise NotImplementedError if inverse_chain
|
338
|
+
inverse_chain = s(:inverse, block_params, program, open_strip, close_strip)
|
339
|
+
program = nil
|
340
|
+
else
|
341
|
+
program = s(:program, block_params, program)
|
342
|
+
end
|
343
|
+
|
344
|
+
type = directive ? :directive_block : :block
|
345
|
+
s(type, name, params, hash, program, inverse_chain, open_strip, close_strip)
|
346
|
+
.line(self.lexer.lineno)
|
347
|
+
end
|
348
|
+
|
349
|
+
def validate_close(name, close_name)
|
350
|
+
unless name == close_name
|
351
|
+
raise ParseError, "#{name[2][1]} doesn't match #{close_name[2][1]}"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def on_error(t, val, vstack)
|
356
|
+
raise ParseError, sprintf("Parse error on line %i on value %s (%s) at %s",
|
357
|
+
self.lexer.lineno, val.inspect, token_to_str(t) || '?', vstack.inspect)
|
358
|
+
end
|
@@ -0,0 +1,347 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "sexp_processor"
|
4
|
+
|
5
|
+
module Haparanda
|
6
|
+
class HandlebarsProcessor < SexpProcessor # rubocop:disable Metrics/ClassLength
|
7
|
+
class SafeString < String
|
8
|
+
def to_s
|
9
|
+
self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class Input
|
14
|
+
def initialize(value)
|
15
|
+
@stack = [value]
|
16
|
+
end
|
17
|
+
|
18
|
+
def dig(*keys)
|
19
|
+
index = -1
|
20
|
+
value = @stack[index]
|
21
|
+
keys.each do |key|
|
22
|
+
if key == :".."
|
23
|
+
index -= 1
|
24
|
+
value = @stack[index]
|
25
|
+
end
|
26
|
+
next if %i[.. . this].include? key
|
27
|
+
|
28
|
+
value = case value
|
29
|
+
when Hash
|
30
|
+
value[key]
|
31
|
+
when nil
|
32
|
+
nil
|
33
|
+
else
|
34
|
+
value.send key
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
value
|
39
|
+
end
|
40
|
+
|
41
|
+
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
|
45
|
+
block.call
|
46
|
+
else
|
47
|
+
@stack.push value
|
48
|
+
result = block.call
|
49
|
+
@stack.pop
|
50
|
+
result
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
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
|
64
|
+
end
|
65
|
+
|
66
|
+
def method_missing(method_name, *_args)
|
67
|
+
dig(method_name)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
class Data
|
72
|
+
def initialize(data = {})
|
73
|
+
@data = data
|
74
|
+
end
|
75
|
+
|
76
|
+
def data(*keys)
|
77
|
+
@data.dig(*keys)
|
78
|
+
end
|
79
|
+
|
80
|
+
def set_data(key, value)
|
81
|
+
@data[key] = value
|
82
|
+
end
|
83
|
+
|
84
|
+
def with_new_data(&block)
|
85
|
+
data = @data.clone
|
86
|
+
result = block.call
|
87
|
+
@data = data
|
88
|
+
result
|
89
|
+
end
|
90
|
+
|
91
|
+
def respond_to_missing?(method_name)
|
92
|
+
@data.key? method_name
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(method_name, *_args)
|
96
|
+
@data[method_name] if @data.key? method_name
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class NoData
|
101
|
+
def set_data(key, value); end
|
102
|
+
|
103
|
+
def with_new_data(&block)
|
104
|
+
block.call
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Options
|
109
|
+
def initialize(fn:, inverse:, hash:, data:)
|
110
|
+
@fn = fn
|
111
|
+
@inverse = inverse
|
112
|
+
@hash = hash
|
113
|
+
@data = data
|
114
|
+
end
|
115
|
+
|
116
|
+
attr_reader :hash, :data
|
117
|
+
|
118
|
+
def fn(arg = nil)
|
119
|
+
@fn&.call(arg)
|
120
|
+
end
|
121
|
+
|
122
|
+
def inverse(arg = nil)
|
123
|
+
@inverse&.call(arg)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def initialize(input, custom_helpers = nil, data: {})
|
128
|
+
super()
|
129
|
+
|
130
|
+
self.require_empty = false
|
131
|
+
|
132
|
+
@input = Input.new(input)
|
133
|
+
@data = data ? Data.new(data) : NoData.new
|
134
|
+
|
135
|
+
custom_helpers ||= {}
|
136
|
+
@helpers = {
|
137
|
+
if: method(:handle_if),
|
138
|
+
unless: method(:handle_unless),
|
139
|
+
with: method(:handle_with),
|
140
|
+
each: method(:handle_each)
|
141
|
+
}.merge(custom_helpers)
|
142
|
+
end
|
143
|
+
|
144
|
+
def apply(expr)
|
145
|
+
result = process(expr)
|
146
|
+
result[1]
|
147
|
+
end
|
148
|
+
|
149
|
+
def process_root(expr)
|
150
|
+
_, statements = expr
|
151
|
+
process(statements)
|
152
|
+
end
|
153
|
+
|
154
|
+
def process_mustache(expr)
|
155
|
+
_, path, params, hash, escaped, _strip = expr
|
156
|
+
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
|
161
|
+
value = value.to_s
|
162
|
+
value = escape(value) if escaped
|
163
|
+
s(:result, value)
|
164
|
+
end
|
165
|
+
|
166
|
+
def process_block(expr)
|
167
|
+
_, name, params, hash, program, inverse_chain, = expr
|
168
|
+
hash = process(hash)[1] if hash
|
169
|
+
else_program = inverse_chain.sexp_body[1] if inverse_chain
|
170
|
+
arguments = process(params)[1]
|
171
|
+
|
172
|
+
path = process(name)
|
173
|
+
data, elements = path_segments(path)
|
174
|
+
value = lookup_path(data, elements)
|
175
|
+
|
176
|
+
evaluate_program_with_value(value, arguments, program, else_program, hash)
|
177
|
+
end
|
178
|
+
|
179
|
+
def process_statements(expr)
|
180
|
+
results = expr.sexp_body.map { process(_1)[1] }
|
181
|
+
s(:result, results.join)
|
182
|
+
end
|
183
|
+
|
184
|
+
def process_program(expr)
|
185
|
+
_, _params, statements, = expr
|
186
|
+
statements = process(statements)[1] if statements
|
187
|
+
s(:result, statements.to_s)
|
188
|
+
end
|
189
|
+
|
190
|
+
def process_comment(expr)
|
191
|
+
_, _comment, = expr
|
192
|
+
s(:result, "")
|
193
|
+
end
|
194
|
+
|
195
|
+
def process_path(expr)
|
196
|
+
_, data, *segments = expr
|
197
|
+
segments = segments.each_slice(2).map { |elem, _sep| elem[1].to_sym }
|
198
|
+
s(:segments, data, segments)
|
199
|
+
end
|
200
|
+
|
201
|
+
def process_exprs(expr)
|
202
|
+
_, *paths = expr
|
203
|
+
values = paths.map { evaluate_expr(_1) }
|
204
|
+
s(:values, values)
|
205
|
+
end
|
206
|
+
|
207
|
+
def process_hash(expr)
|
208
|
+
_, *entries = expr
|
209
|
+
hash = entries.to_h do |_, key, value|
|
210
|
+
value = evaluate_expr(value)
|
211
|
+
[key.to_sym, value]
|
212
|
+
end
|
213
|
+
s(:hash, hash)
|
214
|
+
end
|
215
|
+
|
216
|
+
private
|
217
|
+
|
218
|
+
def evaluate_program_with_value(value, arguments, program, else_program, hash)
|
219
|
+
fn = make_contextual_lambda(program)
|
220
|
+
inverse = make_contextual_lambda(else_program)
|
221
|
+
|
222
|
+
if value.respond_to? :call
|
223
|
+
value = execute_in_context(value, arguments, fn: fn, inverse: inverse, hash: hash)
|
224
|
+
return s(:result, value.to_s)
|
225
|
+
end
|
226
|
+
|
227
|
+
case value
|
228
|
+
when Array
|
229
|
+
return s(:result, inverse.call(@input)) if value.empty?
|
230
|
+
|
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)
|
236
|
+
else
|
237
|
+
result = value ? fn.call(value) : inverse.call(@input)
|
238
|
+
s(:result, result)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
def make_contextual_lambda(program)
|
243
|
+
if program
|
244
|
+
->(item) { @input.with_new_context(item) { apply(program) } }
|
245
|
+
else
|
246
|
+
->(_item) { "" }
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
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
|
257
|
+
value
|
258
|
+
when :undefined, :null
|
259
|
+
nil
|
260
|
+
else
|
261
|
+
path[1]
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
def path_segments(path)
|
266
|
+
case path.sexp_type
|
267
|
+
when :segments
|
268
|
+
data, elements = path.sexp_body
|
269
|
+
when :undefined, :null
|
270
|
+
elements = [path.sexp_type]
|
271
|
+
else
|
272
|
+
elements = [path[1]]
|
273
|
+
end
|
274
|
+
return data, elements
|
275
|
+
end
|
276
|
+
|
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)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
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)
|
294
|
+
end
|
295
|
+
|
296
|
+
def handle_if(value, options)
|
297
|
+
if value
|
298
|
+
options.fn(@input)
|
299
|
+
else
|
300
|
+
options.inverse(@input)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def handle_unless(value, options)
|
305
|
+
options.fn(@input) unless value
|
306
|
+
end
|
307
|
+
|
308
|
+
def handle_with(value, options)
|
309
|
+
if value
|
310
|
+
options.fn(value)
|
311
|
+
else
|
312
|
+
options.inverse(value)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
def handle_each(value, options)
|
317
|
+
return unless value
|
318
|
+
|
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
|
325
|
+
end
|
326
|
+
end
|
327
|
+
|
328
|
+
ESCAPE = {
|
329
|
+
"&" => "&",
|
330
|
+
"<" => "<",
|
331
|
+
">" => ">",
|
332
|
+
'"' => """,
|
333
|
+
"'" => "'",
|
334
|
+
"`" => "`",
|
335
|
+
"=" => "="
|
336
|
+
}.freeze
|
337
|
+
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
|
+
end
|
347
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Haparanda
|
4
|
+
# Callable representation of a handlebars template
|
5
|
+
class Template
|
6
|
+
def initialize(expr, helpers)
|
7
|
+
@expr = expr
|
8
|
+
@helpers = helpers
|
9
|
+
end
|
10
|
+
|
11
|
+
def call(input, **runtime_options)
|
12
|
+
# TODO: Change interface of HandlebarsProcessor so it can be instantiated
|
13
|
+
# in Template#initialize
|
14
|
+
processor = HandlebarsProcessor.new(input, @helpers, **runtime_options)
|
15
|
+
processor.apply(@expr)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|