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.
@@ -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
@@ -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, **runtime_options)
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 = HandlebarsProcessor.new(input, @helpers, **runtime_options)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  # Current Haparanda version
4
4
  module Haparanda
5
- VERSION = "0.0.1"
5
+ VERSION = "0.0.2"
6
6
  end
@@ -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.1
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/whitespace_handler.rb
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.6.9
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