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
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "content_combiner"
|
|
4
|
+
require_relative "standalone_whitespace_handler"
|
|
5
|
+
require_relative "whitespace_stripper"
|
|
6
|
+
|
|
7
|
+
module Haparanda
|
|
8
|
+
# Parse a handlebars string to an AST in the form needed to apply input to it:
|
|
9
|
+
# - parse the string into the raw AST
|
|
10
|
+
# - combine subsequent :content items
|
|
11
|
+
# - strip whitespace according to Handlebars' rules
|
|
12
|
+
class Parser
|
|
13
|
+
def initialize(ignore_standalone: false, prevent_indent: false, **)
|
|
14
|
+
@ignore_standalone = ignore_standalone
|
|
15
|
+
@prevent_indent = prevent_indent
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def parse(text)
|
|
19
|
+
expr = HandlebarsParser.new.parse(text)
|
|
20
|
+
expr = ContentCombiner.new.process(expr)
|
|
21
|
+
unless @ignore_standalone
|
|
22
|
+
expr = StandaloneWhitespaceHandler.new(prevent_indent: @prevent_indent)
|
|
23
|
+
.process(expr)
|
|
24
|
+
end
|
|
25
|
+
WhitespaceStripper.new.process(expr)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sexp_processor"
|
|
4
|
+
|
|
5
|
+
module Haparanda
|
|
6
|
+
# Process the handlebars AST just to do the whitespace stripping.
|
|
7
|
+
class StandaloneWhitespaceHandler < SexpProcessor # rubocop:todo Metrics/ClassLength
|
|
8
|
+
def initialize(prevent_indent: false)
|
|
9
|
+
super()
|
|
10
|
+
|
|
11
|
+
@prevent_indent = prevent_indent
|
|
12
|
+
self.require_empty = false
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def process(expr)
|
|
16
|
+
line = expr&.line
|
|
17
|
+
super.tap { _1.line(line) if line }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def process_root(expr)
|
|
21
|
+
_, statements = expr
|
|
22
|
+
|
|
23
|
+
@root = true
|
|
24
|
+
statements = process(statements)
|
|
25
|
+
s(:root, statements)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def process_block(expr)
|
|
29
|
+
_, name, params, hash, program, inverse_chain, open_strip, close_strip = expr
|
|
30
|
+
|
|
31
|
+
program = process(program)
|
|
32
|
+
inverse_chain = process(inverse_chain)
|
|
33
|
+
|
|
34
|
+
statements = program&.at(2)&.sexp_body
|
|
35
|
+
|
|
36
|
+
if statements && inverse_chain
|
|
37
|
+
strip_standalone_whitespace(statements.last, first_item(inverse_chain))
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
s(:block, name, params, hash, program, inverse_chain, open_strip, close_strip)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def process_statements(expr)
|
|
44
|
+
statements = expr.sexp_body
|
|
45
|
+
|
|
46
|
+
strip_whitespace_around_standalone_items(statements)
|
|
47
|
+
|
|
48
|
+
s(:statements, *statements)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# Strip whitespace around standalone items in a list of statements, while
|
|
54
|
+
# recursing the general processing into each item at the right moment.
|
|
55
|
+
#
|
|
56
|
+
# The goal is to correctly remove whitespace for each item that is
|
|
57
|
+
# 'standalone', i.e., appears on a line by itself with only whitespace
|
|
58
|
+
# around.
|
|
59
|
+
#
|
|
60
|
+
# The tricky bit is that removing whitespace for one item may remove the
|
|
61
|
+
# information needed for handling subsequent or nested items.
|
|
62
|
+
#
|
|
63
|
+
# To resolve this, this method splits the collection of what whitespace
|
|
64
|
+
# changes to make from actually making them. In between these two parts, it
|
|
65
|
+
# recurses into the nested items. This way, it ensures the nested process
|
|
66
|
+
# has the original information available.
|
|
67
|
+
# rubocop:todo Metrics/PerceivedComplexity
|
|
68
|
+
# rubocop:todo Metrics/MethodLength
|
|
69
|
+
def strip_whitespace_around_standalone_items(statements) # rubocop:todo Metrics/AbcSize
|
|
70
|
+
before = nil
|
|
71
|
+
|
|
72
|
+
root = @root
|
|
73
|
+
@root = false
|
|
74
|
+
last_idx = statements.length - 1
|
|
75
|
+
|
|
76
|
+
[*statements, nil].each_cons(2).with_index do |(item, after), idx|
|
|
77
|
+
before_space, inner_start_space, inner_end_space, after_space =
|
|
78
|
+
collect_whitespace_information(before, item, after)
|
|
79
|
+
|
|
80
|
+
if root
|
|
81
|
+
if [:block, :comment].include?(item.sexp_type)
|
|
82
|
+
before_space = true if idx == 0
|
|
83
|
+
after_space = true if idx == last_idx
|
|
84
|
+
end
|
|
85
|
+
if [:block, :comment, :partial].include?(item.sexp_type) &&
|
|
86
|
+
idx == 1 && before.sexp_type == :content && (before[1] =~ /^\s*$/)
|
|
87
|
+
before_space = true
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
after_space = true if [:partial].include?(item.sexp_type) && idx == last_idx
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
process(item)
|
|
94
|
+
|
|
95
|
+
apply_whitespace_clearing(before, item, after,
|
|
96
|
+
before_space, inner_start_space,
|
|
97
|
+
inner_end_space, after_space)
|
|
98
|
+
|
|
99
|
+
before = item
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
# rubocop:enable Metrics/MethodLength
|
|
103
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
104
|
+
|
|
105
|
+
def collect_whitespace_information(before, item, after)
|
|
106
|
+
before_space = preceding_whitespace? before
|
|
107
|
+
after_space = following_whitespace? after
|
|
108
|
+
|
|
109
|
+
if item.sexp_type == :block
|
|
110
|
+
inner_start_space = following_whitespace? first_item(item)
|
|
111
|
+
inner_end_space = preceding_whitespace? last_item(item)
|
|
112
|
+
end
|
|
113
|
+
return before_space, inner_start_space, inner_end_space, after_space
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# rubocop:todo Metrics/PerceivedComplexity
|
|
117
|
+
# rubocop:todo Metrics/CyclomaticComplexity
|
|
118
|
+
def apply_whitespace_clearing(before, item, after, # rubocop:todo Metrics/MethodLength
|
|
119
|
+
before_space, inner_start_space,
|
|
120
|
+
inner_end_space, after_space)
|
|
121
|
+
case item.sexp_type
|
|
122
|
+
when :block
|
|
123
|
+
if before_space && inner_start_space
|
|
124
|
+
clear_preceding_whitespace(before)
|
|
125
|
+
clear_following_whitespace(first_item(item))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if inner_end_space && after_space
|
|
129
|
+
clear_preceding_whitespace(last_item(item))
|
|
130
|
+
clear_following_whitespace(after)
|
|
131
|
+
end
|
|
132
|
+
when :partial
|
|
133
|
+
if !@prevent_indent && before_space && after_space
|
|
134
|
+
indent = clear_preceding_whitespace(before)
|
|
135
|
+
set_indent(item, indent)
|
|
136
|
+
end
|
|
137
|
+
clear_following_whitespace(after) if before_space && after
|
|
138
|
+
when :comment
|
|
139
|
+
if before_space && after_space
|
|
140
|
+
clear_preceding_whitespace(before)
|
|
141
|
+
clear_following_whitespace(after)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
146
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
147
|
+
|
|
148
|
+
def first_item(container)
|
|
149
|
+
return if container.nil?
|
|
150
|
+
|
|
151
|
+
case container.sexp_type
|
|
152
|
+
when :statements
|
|
153
|
+
container.sexp_body.first
|
|
154
|
+
when :block
|
|
155
|
+
first_item(container[4] || container[5])
|
|
156
|
+
when :inverse, :program
|
|
157
|
+
first_item container[2]
|
|
158
|
+
when :content
|
|
159
|
+
container
|
|
160
|
+
else
|
|
161
|
+
raise NotImplementedError
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def last_item(container)
|
|
166
|
+
return if container.nil?
|
|
167
|
+
|
|
168
|
+
case container.sexp_type
|
|
169
|
+
when :statements
|
|
170
|
+
container.sexp_body.last
|
|
171
|
+
when :block
|
|
172
|
+
last_item(container[5] || container[4])
|
|
173
|
+
when :inverse, :program
|
|
174
|
+
last_item container[2]
|
|
175
|
+
when :content
|
|
176
|
+
container
|
|
177
|
+
else
|
|
178
|
+
raise NotImplementedError
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def strip_standalone_whitespace(before, after)
|
|
183
|
+
return unless preceding_whitespace? before
|
|
184
|
+
return unless following_whitespace? after
|
|
185
|
+
|
|
186
|
+
clear_preceding_whitespace(before)
|
|
187
|
+
clear_following_whitespace(after)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def preceding_whitespace?(before)
|
|
191
|
+
before&.sexp_type == :content && before[1] =~ /\n\s*\z/
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def following_whitespace?(after)
|
|
195
|
+
after&.sexp_type == :content && after[1] =~ /^\s*\n/
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Strip trailing whitespace before but leave the \n. Return the stripped space.
|
|
199
|
+
def clear_preceding_whitespace(before)
|
|
200
|
+
return unless before
|
|
201
|
+
|
|
202
|
+
if (match = before[1].match(/\A(.*\n|)([ \t]+)\Z/m))
|
|
203
|
+
before[1] = match[1]
|
|
204
|
+
match[2]
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Strip leading whitespace after, including the \n if present
|
|
209
|
+
def clear_following_whitespace(after)
|
|
210
|
+
return unless after
|
|
211
|
+
|
|
212
|
+
after[1] = after[1].sub(/^[ \t]*(\n|\r\n)?/, "")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def set_indent(item, indent)
|
|
216
|
+
unless item.sexp_type == :partial
|
|
217
|
+
raise "Indenting not supported for #{item.sexp_type}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
item[-2] = s(:indent, indent)
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
data/lib/haparanda/template.rb
CHANGED
|
@@ -1,18 +1,49 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "parser"
|
|
4
|
+
|
|
3
5
|
module Haparanda
|
|
4
6
|
# Callable representation of a handlebars template
|
|
5
7
|
class Template
|
|
6
|
-
def initialize(expr, helpers)
|
|
8
|
+
def initialize(expr, helpers, partials, log, **compile_options)
|
|
7
9
|
@expr = expr
|
|
8
10
|
@helpers = helpers
|
|
11
|
+
@partials = partials
|
|
12
|
+
@log = log
|
|
13
|
+
@compile_options = compile_options
|
|
9
14
|
end
|
|
10
15
|
|
|
11
|
-
def call(input,
|
|
16
|
+
def call(input, helpers: {}, partials: {}, data: {})
|
|
17
|
+
all_helpers = @helpers.merge(helpers).compact
|
|
18
|
+
partials.transform_values! { parse_partial(_1) }
|
|
19
|
+
all_partials = @partials.merge(partials)
|
|
20
|
+
if @compile_options[:known_helpers_only]
|
|
21
|
+
keys = @compile_options[:known_helpers]&.keys || []
|
|
22
|
+
all_helpers = all_helpers.slice(*keys)
|
|
23
|
+
end
|
|
24
|
+
explicit_partial_context = true if @compile_options[:explicit_partial_context]
|
|
25
|
+
compat = true if @compile_options[:compat]
|
|
26
|
+
no_escape = true if @compile_options[:no_escape]
|
|
12
27
|
# TODO: Change interface of HandlebarsProcessor so it can be instantiated
|
|
13
28
|
# in Template#initialize
|
|
14
|
-
processor =
|
|
29
|
+
processor =
|
|
30
|
+
HandlebarsProcessor.new(input,
|
|
31
|
+
helpers: all_helpers,
|
|
32
|
+
partials: all_partials,
|
|
33
|
+
log: @log,
|
|
34
|
+
data: data,
|
|
35
|
+
compat: compat,
|
|
36
|
+
explicit_partial_context: explicit_partial_context,
|
|
37
|
+
no_escape: no_escape)
|
|
15
38
|
processor.apply(@expr)
|
|
16
39
|
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_partial(partial)
|
|
44
|
+
return partial if partial.respond_to? :call
|
|
45
|
+
|
|
46
|
+
Parser.new(**@compile_options).parse(partial)
|
|
47
|
+
end
|
|
17
48
|
end
|
|
18
49
|
end
|
data/lib/haparanda/version.rb
CHANGED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sexp_processor"
|
|
4
|
+
|
|
5
|
+
module Haparanda
|
|
6
|
+
# Process the handlebars AST just to do the whitespace stripping.
|
|
7
|
+
class WhitespaceStripper < SexpProcessor
|
|
8
|
+
def initialize
|
|
9
|
+
super
|
|
10
|
+
|
|
11
|
+
self.require_empty = false
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def process(expr)
|
|
15
|
+
line = expr&.line
|
|
16
|
+
super.tap { _1.line(line) if line }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def process_root(expr)
|
|
20
|
+
_, statements = expr
|
|
21
|
+
|
|
22
|
+
statements = process(statements)
|
|
23
|
+
s(:root, statements)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process_block(expr)
|
|
27
|
+
_, name, params, hash, program, inverse_chain, open_strip, close_strip = expr
|
|
28
|
+
|
|
29
|
+
program = process(program)
|
|
30
|
+
if inverse_chain && inverse_chain.last.nil?
|
|
31
|
+
body = inverse_chain.sexp_body
|
|
32
|
+
body[-1] = close_strip
|
|
33
|
+
inverse_chain.sexp_body = body
|
|
34
|
+
end
|
|
35
|
+
inverse_chain = process(inverse_chain)
|
|
36
|
+
|
|
37
|
+
statements = program&.at(2)&.sexp_body
|
|
38
|
+
if statements
|
|
39
|
+
strip_initial_whitespace(statements.first, open_strip)
|
|
40
|
+
strip_final_whitespace(statements.last, close_strip)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
s(:block, name, params, hash, program, inverse_chain, open_strip, close_strip)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def process_partial_block(expr)
|
|
47
|
+
_, name, params, hash, statements, open_strip, close_strip = expr
|
|
48
|
+
|
|
49
|
+
if (statements = process(statements))
|
|
50
|
+
items = statements.sexp_body
|
|
51
|
+
strip_initial_whitespace(items.first, open_strip)
|
|
52
|
+
strip_final_whitespace(items.last, close_strip)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
s(:partial_block, name, params, hash, statements, open_strip, close_strip)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def process_directive_block(expr)
|
|
59
|
+
_, name, params, hash, program, _inverse_chain, open_strip, close_strip = expr
|
|
60
|
+
program = process(program)
|
|
61
|
+
|
|
62
|
+
statements = program&.at(2)&.sexp_body
|
|
63
|
+
if statements
|
|
64
|
+
strip_initial_whitespace(statements.first, open_strip)
|
|
65
|
+
strip_final_whitespace(statements.last, close_strip)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
s(:directive_block, name, params, hash, program, nil, open_strip, close_strip)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def process_inverse(expr)
|
|
72
|
+
_, block_params, statements, open_strip, close_strip = expr
|
|
73
|
+
|
|
74
|
+
block_params = process(block_params)
|
|
75
|
+
statements = process(statements)
|
|
76
|
+
|
|
77
|
+
case statements.sexp_type
|
|
78
|
+
when :statements
|
|
79
|
+
items = statements.sexp_body
|
|
80
|
+
strip_initial_whitespace(items.first, open_strip)
|
|
81
|
+
strip_final_whitespace(items.last, close_strip)
|
|
82
|
+
end
|
|
83
|
+
# TODO: Handle :block sexp_type
|
|
84
|
+
|
|
85
|
+
s(:inverse, block_params, statements, open_strip, close_strip)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def process_statements(expr)
|
|
89
|
+
statements = expr.sexp_body
|
|
90
|
+
|
|
91
|
+
strip_pairwise_sibling_whitespace(statements)
|
|
92
|
+
|
|
93
|
+
statements = statements.map { process(_1) }
|
|
94
|
+
|
|
95
|
+
s(:statements, *statements)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def strip_pairwise_sibling_whitespace(statements)
|
|
101
|
+
statements.each_cons(2) do |prev, item|
|
|
102
|
+
strip_final_whitespace(prev, open_strip_for(item)) if item.sexp_type != :content
|
|
103
|
+
strip_initial_whitespace(item, close_strip_for(prev)) if prev.sexp_type != :content
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def strip_initial_whitespace(item, strip)
|
|
108
|
+
item[1] = item[1].sub(/^\s*/, "") if item.sexp_type == :content && strip[2]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def strip_final_whitespace(item, strip)
|
|
112
|
+
item[1] = item[1].sub(/\s*$/, "") if item.sexp_type == :content && strip&.at(1)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def open_strip_for(item)
|
|
116
|
+
case item.sexp_type
|
|
117
|
+
when :block, :directive_block, :partial_block
|
|
118
|
+
item.at(-2)
|
|
119
|
+
when :partial, :mustache, :comment
|
|
120
|
+
item.last
|
|
121
|
+
else
|
|
122
|
+
raise NotImplementedError, item.sexp_type
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def close_strip_for(item)
|
|
127
|
+
case item.sexp_type
|
|
128
|
+
when :block, :directive_block, :partial_block, :partial, :mustache, :comment
|
|
129
|
+
item.last
|
|
130
|
+
else
|
|
131
|
+
raise NotImplementedError, item.sexp_type
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
data/lib/haparanda.rb
CHANGED
|
@@ -9,7 +9,6 @@ require_relative "haparanda/version"
|
|
|
9
9
|
|
|
10
10
|
require_relative "haparanda/handlebars_lexer"
|
|
11
11
|
require_relative "haparanda/handlebars_parser"
|
|
12
|
-
require_relative "haparanda/handlebars_compiler"
|
|
13
12
|
require_relative "haparanda/handlebars_processor"
|
|
14
13
|
|
|
15
14
|
require_relative "haparanda/compiler"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: haparanda
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matijs van Zuijlen
|
|
@@ -9,6 +9,20 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: logger
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.6'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.6'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: racc
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -54,16 +68,17 @@ files:
|
|
|
54
68
|
- lib/haparanda.rb
|
|
55
69
|
- lib/haparanda/compiler.rb
|
|
56
70
|
- lib/haparanda/content_combiner.rb
|
|
57
|
-
- lib/haparanda/handlebars_compiler.rb
|
|
58
71
|
- lib/haparanda/handlebars_lexer.rb
|
|
59
72
|
- lib/haparanda/handlebars_lexer.rex
|
|
60
73
|
- lib/haparanda/handlebars_parser.output
|
|
61
74
|
- lib/haparanda/handlebars_parser.rb
|
|
62
75
|
- lib/haparanda/handlebars_parser.y
|
|
63
76
|
- lib/haparanda/handlebars_processor.rb
|
|
77
|
+
- lib/haparanda/parser.rb
|
|
78
|
+
- lib/haparanda/standalone_whitespace_handler.rb
|
|
64
79
|
- lib/haparanda/template.rb
|
|
65
80
|
- lib/haparanda/version.rb
|
|
66
|
-
- lib/haparanda/
|
|
81
|
+
- lib/haparanda/whitespace_stripper.rb
|
|
67
82
|
homepage: https://github.com/mvz/haparanda
|
|
68
83
|
licenses:
|
|
69
84
|
- LGPL-2.1-or-later
|
|
@@ -88,7 +103,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
88
103
|
- !ruby/object:Gem::Version
|
|
89
104
|
version: '0'
|
|
90
105
|
requirements: []
|
|
91
|
-
rubygems_version: 3.
|
|
106
|
+
rubygems_version: 3.7.2
|
|
92
107
|
specification_version: 4
|
|
93
108
|
summary: Pure Ruby Handlebars Parser
|
|
94
109
|
test_files: []
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "content_combiner"
|
|
4
|
-
require_relative "whitespace_handler"
|
|
5
|
-
|
|
6
|
-
module Haparanda
|
|
7
|
-
# Process the handlebars AST just to combine subsequent :content items
|
|
8
|
-
class HandlebarsCompiler
|
|
9
|
-
def initialize(ignore_standalone: false, **)
|
|
10
|
-
@ignore_standalone = ignore_standalone
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def process(expr)
|
|
14
|
-
expr = ContentCombiner.new.process(expr)
|
|
15
|
-
WhitespaceHandler.new(ignore_standalone: @ignore_standalone).process(expr)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "sexp_processor"
|
|
4
|
-
|
|
5
|
-
module Haparanda
|
|
6
|
-
# Process the handlebars AST just to do the whitespace stripping.
|
|
7
|
-
class WhitespaceHandler < SexpProcessor # rubocop:disable Metrics/ClassLength
|
|
8
|
-
def initialize(ignore_standalone: false)
|
|
9
|
-
super()
|
|
10
|
-
|
|
11
|
-
@ignore_standalone = ignore_standalone
|
|
12
|
-
|
|
13
|
-
self.require_empty = false
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def process_root(expr)
|
|
17
|
-
_, statements = expr
|
|
18
|
-
|
|
19
|
-
statements = process(statements)
|
|
20
|
-
item = statements.sexp_body[0]
|
|
21
|
-
if item.sexp_type == :block
|
|
22
|
-
content = item.dig(4, 2, 1)
|
|
23
|
-
clear_following_whitespace(content) if following_whitespace?(content)
|
|
24
|
-
end
|
|
25
|
-
s(:root, statements)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def process_block(expr)
|
|
29
|
-
_, name, params, hash, program, inverse_chain, open_strip, close_strip = expr
|
|
30
|
-
|
|
31
|
-
program = process(program)
|
|
32
|
-
if inverse_chain && inverse_chain.last.nil?
|
|
33
|
-
body = inverse_chain.sexp_body
|
|
34
|
-
body[-1] = close_strip
|
|
35
|
-
inverse_chain.sexp_body = body
|
|
36
|
-
end
|
|
37
|
-
inverse_chain = process(inverse_chain)
|
|
38
|
-
|
|
39
|
-
statements = program&.at(2)&.sexp_body
|
|
40
|
-
if statements
|
|
41
|
-
strip_initial_whitespace(statements.first, open_strip)
|
|
42
|
-
strip_final_whitespace(statements.last, close_strip)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
if statements && inverse_chain
|
|
46
|
-
strip_standalone_whitespace(statements.last, first_item(inverse_chain))
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
s(:block, name, params, hash, program, inverse_chain, open_strip, close_strip)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def process_inverse(expr)
|
|
53
|
-
_, block_params, statements, open_strip, close_strip = expr
|
|
54
|
-
|
|
55
|
-
block_params = process(block_params)
|
|
56
|
-
statements = process(statements)
|
|
57
|
-
|
|
58
|
-
case statements.sexp_type
|
|
59
|
-
when :statements
|
|
60
|
-
if (items = statements&.sexp_body)
|
|
61
|
-
strip_initial_whitespace(items.first, open_strip)
|
|
62
|
-
strip_final_whitespace(items.last, close_strip)
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
# TODO: Handle :block sexp_type
|
|
66
|
-
|
|
67
|
-
s(:inverse, block_params, statements, open_strip, close_strip)
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def process_statements(expr)
|
|
71
|
-
statements = expr.sexp_body
|
|
72
|
-
|
|
73
|
-
statements.each_cons(2) do |prev, item|
|
|
74
|
-
strip_final_whitespace(prev, open_strip_for(item)) if item.sexp_type != :content
|
|
75
|
-
strip_initial_whitespace(item, close_strip_for(prev)) if prev.sexp_type != :content
|
|
76
|
-
|
|
77
|
-
strip_standalone_whitespace(prev, item.dig(4, 2, 1)) if item.sexp_type == :block
|
|
78
|
-
strip_standalone_whitespace(last_item(prev), item) if prev.sexp_type == :block
|
|
79
|
-
end
|
|
80
|
-
statements = statements.map { process(_1) }
|
|
81
|
-
|
|
82
|
-
s(:statements, *statements)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
private
|
|
86
|
-
|
|
87
|
-
def first_item(container)
|
|
88
|
-
case container.sexp_type
|
|
89
|
-
when :statements
|
|
90
|
-
container.sexp_body.first
|
|
91
|
-
when :block
|
|
92
|
-
container.dig(4, 2, 1)
|
|
93
|
-
when :inverse
|
|
94
|
-
first_item container[2]
|
|
95
|
-
else
|
|
96
|
-
raise NotImplementedError
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def last_item(container)
|
|
101
|
-
return if container.nil?
|
|
102
|
-
|
|
103
|
-
case container.sexp_type
|
|
104
|
-
when :block
|
|
105
|
-
last_item(container[5] || container[4])
|
|
106
|
-
when :statements
|
|
107
|
-
container.sexp_body.last
|
|
108
|
-
when :inverse, :program
|
|
109
|
-
last_item container[2]
|
|
110
|
-
when :content
|
|
111
|
-
container
|
|
112
|
-
else
|
|
113
|
-
raise NotImplementedError
|
|
114
|
-
end
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
def strip_initial_whitespace(item, strip)
|
|
118
|
-
item[1] = item[1].sub(/^\s*/, "") if item.sexp_type == :content && strip[2]
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def strip_final_whitespace(item, strip)
|
|
122
|
-
item[1] = item[1].sub(/\s*$/, "") if item.sexp_type == :content && strip&.at(1)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def strip_standalone_whitespace(before, after)
|
|
126
|
-
return unless preceding_whitespace? before
|
|
127
|
-
return unless following_whitespace? after
|
|
128
|
-
|
|
129
|
-
clear_preceding_whitespace(before)
|
|
130
|
-
clear_following_whitespace(after)
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
def preceding_whitespace?(before)
|
|
134
|
-
before&.sexp_type == :content && before[1] =~ /\n\s*$/
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
def following_whitespace?(after)
|
|
138
|
-
after&.sexp_type == :content && after[1] =~ /^\s*\n/
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# Strip trailing whitespace before but leave the \n
|
|
142
|
-
def clear_preceding_whitespace(before)
|
|
143
|
-
return if @ignore_standalone
|
|
144
|
-
|
|
145
|
-
before[1] = before[1].sub(/\n[ \t]+$/, "\n")
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Strip leading whitespace after including the \n
|
|
149
|
-
def clear_following_whitespace(after)
|
|
150
|
-
return if @ignore_standalone
|
|
151
|
-
|
|
152
|
-
after[1] = after[1].sub(/^[ \t]*\n/, "")
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
def open_strip_for(item)
|
|
156
|
-
case item.sexp_type
|
|
157
|
-
when :block
|
|
158
|
-
item.at(-2)
|
|
159
|
-
else
|
|
160
|
-
item.last
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def close_strip_for(item)
|
|
165
|
-
item.last
|
|
166
|
-
end
|
|
167
|
-
end
|
|
168
|
-
end
|