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.
@@ -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
+ "&" => "&amp;",
330
+ "<" => "&lt;",
331
+ ">" => "&gt;",
332
+ '"' => "&quot;",
333
+ "'" => "&#x27;",
334
+ "`" => "&#x60;",
335
+ "=" => "&#x3D;"
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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Current Haparanda version
4
+ module Haparanda
5
+ VERSION = "0.0.1"
6
+ end