klenod-build 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/README.md +29 -0
- data/lib/klenod/build/asset.rb +235 -0
- data/lib/klenod/build/asset_compression.rb +58 -0
- data/lib/klenod/build/asset_generation_queue.rb +50 -0
- data/lib/klenod/build/cli/application.rb +108 -0
- data/lib/klenod/build/cli.rb +3 -0
- data/lib/klenod/build/config.rb +142 -0
- data/lib/klenod/build/context.rb +370 -0
- data/lib/klenod/build/dependency.rb +21 -0
- data/lib/klenod/build/errors.rb +175 -0
- data/lib/klenod/build/filesystem_resolver.rb +147 -0
- data/lib/klenod/build/graph/invalidator.rb +246 -0
- data/lib/klenod/build/graph.rb +864 -0
- data/lib/klenod/build/graphviz.rb +282 -0
- data/lib/klenod/build/hashing.rb +21 -0
- data/lib/klenod/build/invalidation_result.rb +55 -0
- data/lib/klenod/build/load_result.rb +7 -0
- data/lib/klenod/build/loaded_module.rb +38 -0
- data/lib/klenod/build/module_id.rb +100 -0
- data/lib/klenod/build/module_record.rb +22 -0
- data/lib/klenod/build/plugin.rb +35 -0
- data/lib/klenod/build/plugins/class_names_runtime.rb +125 -0
- data/lib/klenod/build/plugins/component_defaults.rb +12 -0
- data/lib/klenod/build/plugins/data_plugin.rb +117 -0
- data/lib/klenod/build/plugins/gem_import_plugin.rb +125 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.json +17687 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.rb +105 -0
- data/lib/klenod/build/plugins/google_fonts_plugin/font_metrics.txt +31 -0
- data/lib/klenod/build/plugins/google_fonts_plugin.rb +387 -0
- data/lib/klenod/build/plugins/haml_plugin/companions.rb +40 -0
- data/lib/klenod/build/plugins/haml_plugin/errors.rb +114 -0
- data/lib/klenod/build/plugins/haml_plugin/helper_source.rb +123 -0
- data/lib/klenod/build/plugins/haml_plugin/parser.rb +85 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +1039 -0
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +766 -0
- data/lib/klenod/build/plugins/haml_plugin.rb +369 -0
- data/lib/klenod/build/plugins/image_plugin.rb +401 -0
- data/lib/klenod/build/plugins/intl_plugin.rb +34 -0
- data/lib/klenod/build/plugins/markdown_compiler.rb +180 -0
- data/lib/klenod/build/plugins/markdown_plugin.rb +161 -0
- data/lib/klenod/build/plugins/router_plugin.rb +942 -0
- data/lib/klenod/build/plugins/ruby_plugin.rb +34 -0
- data/lib/klenod/build/plugins/svg_plugin.rb +197 -0
- data/lib/klenod/build/plugins.rb +22 -0
- data/lib/klenod/build/profiler.rb +79 -0
- data/lib/klenod/build/resolution_error_formatter.rb +42 -0
- data/lib/klenod/build/resolver.rb +95 -0
- data/lib/klenod/build/ruby_import_rewriter.rb +436 -0
- data/lib/klenod/build/source_map/editor.rb +110 -0
- data/lib/klenod/build/source_map/map.rb +168 -0
- data/lib/klenod/build/source_map/vlq.rb +68 -0
- data/lib/klenod/build/source_map.rb +12 -0
- data/lib/klenod/build/transform_result.rb +12 -0
- data/lib/klenod/build/version.rb +7 -0
- data/lib/klenod/build/watched_pattern.rb +11 -0
- data/lib/klenod/build/watcher.rb +141 -0
- data/lib/klenod/build.rb +15 -0
- metadata +310 -0
|
@@ -0,0 +1,1039 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ripper"
|
|
4
|
+
require "syntax_tree"
|
|
5
|
+
require "syntax_tree/dsl"
|
|
6
|
+
require "syntax_suggest/api"
|
|
7
|
+
require "syntax_suggest/explain_syntax"
|
|
8
|
+
|
|
9
|
+
module Klenod
|
|
10
|
+
module Build
|
|
11
|
+
module Plugins
|
|
12
|
+
module HamlPlugin
|
|
13
|
+
class Transformer
|
|
14
|
+
class RubyBuilder
|
|
15
|
+
include SyntaxTree::DSL
|
|
16
|
+
|
|
17
|
+
Fragment = Data.define(:source, :node) do
|
|
18
|
+
def node?
|
|
19
|
+
!node.nil?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def statement_body
|
|
23
|
+
node.is_a?(SyntaxTree::Statements) ? node.body : [node].compact
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
source
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(profiler: nil, global_variables: nil)
|
|
32
|
+
@profiler = profiler
|
|
33
|
+
@global_variables = global_variables
|
|
34
|
+
@expression_cache = {}
|
|
35
|
+
@statements_cache = {}
|
|
36
|
+
@program_cache = {}
|
|
37
|
+
@literal_cache = {}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def component_source(
|
|
41
|
+
component_class_name:,
|
|
42
|
+
component_base_class:,
|
|
43
|
+
translations_source:,
|
|
44
|
+
ruby_source:,
|
|
45
|
+
render_source:,
|
|
46
|
+
styles_source:,
|
|
47
|
+
haml_helper_source: nil,
|
|
48
|
+
static_constants: [],
|
|
49
|
+
i18n_source: nil
|
|
50
|
+
)
|
|
51
|
+
component_program(
|
|
52
|
+
component_class_name: component_class_name,
|
|
53
|
+
component_base_class: component_base_class,
|
|
54
|
+
translations_source: translations_source,
|
|
55
|
+
i18n_source: i18n_source,
|
|
56
|
+
ruby_source: ruby_source,
|
|
57
|
+
render_source: render_source,
|
|
58
|
+
styles_source: styles_source,
|
|
59
|
+
haml_helper_source: haml_helper_source,
|
|
60
|
+
static_constants: static_constants
|
|
61
|
+
).source
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def component_program(
|
|
65
|
+
component_class_name:,
|
|
66
|
+
component_base_class:,
|
|
67
|
+
translations_source:,
|
|
68
|
+
ruby_source:,
|
|
69
|
+
render_source:,
|
|
70
|
+
styles_source:,
|
|
71
|
+
haml_helper_source: nil,
|
|
72
|
+
static_constants: [],
|
|
73
|
+
i18n_source: nil
|
|
74
|
+
)
|
|
75
|
+
component_class_name = expression_fragment(component_class_name)
|
|
76
|
+
component_base_class = expression_fragment(component_base_class)
|
|
77
|
+
translations_source = expression_fragment(translations_source)
|
|
78
|
+
ruby_source = statements_fragment(ruby_source)
|
|
79
|
+
render_source = expression_fragment(render_source)
|
|
80
|
+
styles_source = expression_fragment(styles_source)
|
|
81
|
+
haml_helper_source = statements_fragment(haml_helper_source) if haml_helper_source
|
|
82
|
+
|
|
83
|
+
header = [
|
|
84
|
+
Fragment.new("# frozen_string_literal: true", comment_node("# frozen_string_literal: true")),
|
|
85
|
+
constant_assignment(
|
|
86
|
+
"KlenodImport",
|
|
87
|
+
call(receiver: nil, name: "method", arguments: [symbol("__klenod_import__")])
|
|
88
|
+
),
|
|
89
|
+
haml_helper_source
|
|
90
|
+
].compact
|
|
91
|
+
component_class =
|
|
92
|
+
component_class_fragment(
|
|
93
|
+
component_class_name: component_class_name,
|
|
94
|
+
component_base_class: component_base_class,
|
|
95
|
+
translations_source: translations_source,
|
|
96
|
+
styles_source: styles_source,
|
|
97
|
+
i18n_source: i18n_source,
|
|
98
|
+
ruby_source: ruby_source,
|
|
99
|
+
render_source: render_source,
|
|
100
|
+
static_constants: static_constants
|
|
101
|
+
)
|
|
102
|
+
footer = [
|
|
103
|
+
constant_assignment("Default", component_class_name),
|
|
104
|
+
constant_assignment("ClassNames", "Default::ClassNames"),
|
|
105
|
+
constant_assignment("Translations", "Default::Translations")
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
program_from_fragments(header, component_class, footer)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def component_class_fragment(
|
|
112
|
+
component_class_name:,
|
|
113
|
+
component_base_class:,
|
|
114
|
+
translations_source:,
|
|
115
|
+
styles_source:,
|
|
116
|
+
ruby_source:,
|
|
117
|
+
render_source:,
|
|
118
|
+
static_constants: [],
|
|
119
|
+
i18n_source: nil
|
|
120
|
+
)
|
|
121
|
+
skeleton = class_skeleton_fragment(component_class_name, component_base_class)
|
|
122
|
+
body_fragments =
|
|
123
|
+
[
|
|
124
|
+
method_definition("module_path", target: "self", body: file_expression),
|
|
125
|
+
constant_assignment("Self", "self"),
|
|
126
|
+
constant_assignment("Translations", translations_source),
|
|
127
|
+
i18n_source,
|
|
128
|
+
method_definition(
|
|
129
|
+
"__klenod_import__",
|
|
130
|
+
target: "self",
|
|
131
|
+
parameters: ["dependency_id"],
|
|
132
|
+
body: call(receiver: "KlenodImport", name: "call", arguments: ["dependency_id"])
|
|
133
|
+
),
|
|
134
|
+
method_definition(
|
|
135
|
+
"__klenod_import__",
|
|
136
|
+
parameters: ["dependency_id"],
|
|
137
|
+
body: call(receiver: "self.class", name: "__klenod_import__", arguments: ["dependency_id"])
|
|
138
|
+
),
|
|
139
|
+
constant_assignment("ClassNames", styles_source),
|
|
140
|
+
ruby_source,
|
|
141
|
+
*static_constants,
|
|
142
|
+
public_method_definition("render", body: render_source)
|
|
143
|
+
]
|
|
144
|
+
body =
|
|
145
|
+
body_fragments.flat_map { |fragment| statement_body_for(fragment) }
|
|
146
|
+
|
|
147
|
+
Fragment.new(
|
|
148
|
+
[
|
|
149
|
+
"class #{to_source(component_class_name)} < #{to_source(component_base_class)}",
|
|
150
|
+
indent(compact_join(body_fragments), 2),
|
|
151
|
+
"end"
|
|
152
|
+
].join("\n"),
|
|
153
|
+
skeleton.node.copy(
|
|
154
|
+
bodystmt: BodyStmt(
|
|
155
|
+
Statements(body),
|
|
156
|
+
nil,
|
|
157
|
+
nil,
|
|
158
|
+
nil,
|
|
159
|
+
nil
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def expressions(expressions)
|
|
166
|
+
source_expressions(expressions)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def expression(source, line_no: nil)
|
|
170
|
+
source = rewrite_ruby_source(source, line_no)
|
|
171
|
+
return Fragment.new(source, constant_path(source)) if source.match?(VALID_CONST_PATH)
|
|
172
|
+
|
|
173
|
+
node = parse_expression(source, context: :expression)
|
|
174
|
+
|
|
175
|
+
Fragment.new(source, node)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def statements(source, line_no: nil)
|
|
179
|
+
source = rewrite_ruby_source(source, line_no)
|
|
180
|
+
node = parse_statements(source)
|
|
181
|
+
|
|
182
|
+
Fragment.new(source, node)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def program(source)
|
|
186
|
+
node = parse_program(source)
|
|
187
|
+
|
|
188
|
+
Fragment.new(node ? format_node(node) : source, node)
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def program_from_fragments(*fragments)
|
|
192
|
+
fragments = fragments.flatten
|
|
193
|
+
body = fragments.flat_map { |fragment| statement_body_for(fragment) }
|
|
194
|
+
|
|
195
|
+
Fragment.new(compact_join(fragments), Program(Statements(body)))
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def fragment(node)
|
|
199
|
+
Fragment.new(format_node(node), node)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def node_fragment(source, node)
|
|
203
|
+
Fragment.new(source.to_s, node)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def expression_fragment(value)
|
|
207
|
+
value.is_a?(Fragment) ? value : expression(value.to_s)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
def statements_fragment(value)
|
|
211
|
+
value.is_a?(Fragment) ? value : statements(value.to_s)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def to_source(value)
|
|
215
|
+
value.is_a?(Fragment) ? value.source : value.to_s
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def literal(value)
|
|
219
|
+
if value.is_a?(String) && value.length <= 1
|
|
220
|
+
return @literal_cache[value] ||= literal_fragment(value)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
literal_fragment(value)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def literal_fragment(value)
|
|
227
|
+
Fragment.new(literal_source(value), literal_node(value))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def frozen_literal(value)
|
|
231
|
+
fragment(frozen_literal_node(value))
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def import_call(dependency_id)
|
|
235
|
+
Fragment.new(
|
|
236
|
+
"__klenod_import__(#{literal_source(dependency_id)})",
|
|
237
|
+
CallNode(
|
|
238
|
+
nil,
|
|
239
|
+
nil,
|
|
240
|
+
Ident("__klenod_import__"),
|
|
241
|
+
ArgParen(Args([literal_node(dependency_id)]))
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def constant_assignment(name, value)
|
|
247
|
+
value = expression_fragment(value)
|
|
248
|
+
node = Assign(VarField(Const(name.to_s)), node_for(value))
|
|
249
|
+
Fragment.new("#{name} = #{to_source(value)}", node)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def call(receiver:, name:, arguments:)
|
|
253
|
+
receiver = expression_fragment(receiver) unless receiver.nil?
|
|
254
|
+
arguments = arguments.map { |argument| expression_fragment(argument) }
|
|
255
|
+
receiver_node = receiver.nil? ? nil : node_for(receiver)
|
|
256
|
+
|
|
257
|
+
node =
|
|
258
|
+
CallNode(
|
|
259
|
+
receiver_node,
|
|
260
|
+
receiver_node ? Period(".") : nil,
|
|
261
|
+
Ident(name.to_s),
|
|
262
|
+
ArgParen(Args(arguments.map { |argument| node_for(argument) }))
|
|
263
|
+
)
|
|
264
|
+
receiver_prefix = receiver ? "#{to_source(receiver)}." : nil
|
|
265
|
+
source = "#{receiver_prefix}#{name}(#{arguments.map { |argument| to_source(argument) }.join(", ")})"
|
|
266
|
+
Fragment.new(source, node)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def method_definition(name, body:, target: nil, parameters: [])
|
|
270
|
+
body = Array(body)
|
|
271
|
+
body_source = compact_join(body)
|
|
272
|
+
node =
|
|
273
|
+
DefNode(
|
|
274
|
+
target && node_for(expression_fragment(target)),
|
|
275
|
+
target ? Period(".") : nil,
|
|
276
|
+
Ident(name.to_s),
|
|
277
|
+
Params(parameters.map { |parameter| Ident(parameter.to_s) }, [], nil, [], [], nil, nil),
|
|
278
|
+
body_statement(body)
|
|
279
|
+
)
|
|
280
|
+
target_source = target ? "#{to_source(expression_fragment(target))}." : ""
|
|
281
|
+
params_source = parameters.empty? ? "" : "(#{parameters.join(", ")})"
|
|
282
|
+
source = ["def #{target_source}#{name}#{params_source}", indent(body_source, 2), "end"].join("\n")
|
|
283
|
+
Fragment.new(source, node)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def public_method_definition(name, body:, parameters: [])
|
|
287
|
+
method = method_definition(name, parameters: parameters, body: body)
|
|
288
|
+
node =
|
|
289
|
+
Command(
|
|
290
|
+
Ident("public"),
|
|
291
|
+
Args([method.node]),
|
|
292
|
+
nil
|
|
293
|
+
)
|
|
294
|
+
Fragment.new("public #{method.source}", node)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def nil_expression
|
|
298
|
+
Fragment.new("nil", nil_node)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def file_expression
|
|
302
|
+
Fragment.new("__FILE__", VarRef(Kw("__FILE__")))
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def symbol(value)
|
|
306
|
+
value = value.to_s
|
|
307
|
+
Fragment.new(symbol_source(value), symbol_node(value))
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def symbol_fragment(value)
|
|
311
|
+
Fragment.new(symbol_source(value.to_s), nil)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def styles_lookup(name)
|
|
315
|
+
name = name.to_s
|
|
316
|
+
|
|
317
|
+
Fragment.new(
|
|
318
|
+
"ClassNames[#{symbol_source(name)}]",
|
|
319
|
+
ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
|
|
320
|
+
)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def class_name_lookup(name)
|
|
324
|
+
name = name.to_s
|
|
325
|
+
|
|
326
|
+
Fragment.new(
|
|
327
|
+
"ClassNames[#{symbol_source(name)}]",
|
|
328
|
+
ARef(VarRef(Const("ClassNames")), Args([symbol_node(name)]))
|
|
329
|
+
)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def class_names(values)
|
|
333
|
+
fragments = values.map { |value| expression_fragment(value) }
|
|
334
|
+
|
|
335
|
+
Fragment.new(
|
|
336
|
+
"ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
|
|
337
|
+
CallNode(
|
|
338
|
+
constant_path("ClassNames"),
|
|
339
|
+
Period("."),
|
|
340
|
+
Ident("class_name"),
|
|
341
|
+
ArgParen(Args(fragments.map { |fragment| node_for(fragment) }))
|
|
342
|
+
)
|
|
343
|
+
)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def scoped_class_name(values)
|
|
347
|
+
fragments = values.map { |value| expression_fragment(value) }
|
|
348
|
+
|
|
349
|
+
Fragment.new(
|
|
350
|
+
"ClassNames.class_name(#{fragments.map(&:source).join(", ")})",
|
|
351
|
+
nil
|
|
352
|
+
)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def parenthesized_expression(source, line_no: nil)
|
|
356
|
+
source = rewrite_ruby_source(source, line_no)
|
|
357
|
+
node = parse_expression(source, context: :parenthesized_expression)
|
|
358
|
+
return expression("(#{source})") unless node
|
|
359
|
+
|
|
360
|
+
fragment(Paren(LParen("("), Statements([node])))
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def hash_expression(source, line_no: nil)
|
|
364
|
+
source = rewrite_ruby_source(source, line_no)
|
|
365
|
+
node = parse_expression(source, context: :hash_expression)
|
|
366
|
+
return nil unless node.is_a?(SyntaxTree::HashLiteral)
|
|
367
|
+
|
|
368
|
+
Fragment.new(source, node)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def class_skeleton_fragment(component_class_name, component_base_class)
|
|
372
|
+
Fragment.new(
|
|
373
|
+
"",
|
|
374
|
+
ClassDeclaration(
|
|
375
|
+
constant_path(component_class_name, declaration: true),
|
|
376
|
+
constant_path(component_base_class),
|
|
377
|
+
BodyStmt(
|
|
378
|
+
Statements([]),
|
|
379
|
+
nil,
|
|
380
|
+
nil,
|
|
381
|
+
nil,
|
|
382
|
+
nil
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
def constant_path(value, declaration: false)
|
|
389
|
+
parts = to_source(value).split("::")
|
|
390
|
+
raise ArgumentError, "Expected constant path: #{to_source(value).inspect}" if parts.empty? || parts.any?(&:empty?)
|
|
391
|
+
|
|
392
|
+
return ConstRef(Const(parts.fetch(0))) if declaration && parts.length == 1
|
|
393
|
+
return VarRef(Const(parts.fetch(0))) if parts.length == 1
|
|
394
|
+
|
|
395
|
+
parts.drop(1).reduce(VarRef(Const(parts.fetch(0)))) do |parent, part|
|
|
396
|
+
ConstPathRef(parent, Const(part))
|
|
397
|
+
end
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def source_mark(line_no, _source)
|
|
401
|
+
"# #{Runtime::SourceMap::MARK_PREFIX}:#{line_no}"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def marked_expression(mark, expression)
|
|
405
|
+
source = to_source(expression)
|
|
406
|
+
|
|
407
|
+
source_marked_fragment(mark, source, node_for(expression))
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def factory_call(factory:, tag:, children:, props:, mark: nil)
|
|
411
|
+
source_factory_call(factory: factory, tag: tag, children: children, props: props, mark: mark)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def slot_call(name:, fallback:)
|
|
415
|
+
arguments = ["self", name ? argument_source(name) : "nil"]
|
|
416
|
+
arguments << argument_source(fallback) if fallback
|
|
417
|
+
expression("HamlHelper.render_slot(#{arguments.join(", ")})")
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def freeze_static(value)
|
|
421
|
+
expression("HamlHelper.freeze_static(#{argument_source(value)})")
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def script_block(source, body, line_no: nil)
|
|
425
|
+
source = rewrite_ruby_source(source, nil)
|
|
426
|
+
ast_script_block(source, body) || raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def silent_script_block(source, body, line_no: nil)
|
|
430
|
+
source = rewrite_ruby_source(source, nil)
|
|
431
|
+
ast_silent_script_block(source, body) || raise_ruby_parse_error(source, line_no: line_no, context: "Could not build Ruby block from Haml script")
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def silent_script(source)
|
|
435
|
+
source = rewrite_ruby_source(source, nil)
|
|
436
|
+
ast_silent_script(source) || raise(ArgumentError, "Could not build Ruby begin block from Haml script: #{source.inspect}")
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def branches(branches)
|
|
440
|
+
ast_branches(branches) || raise(ArgumentError, "Could not build Ruby branch from Haml scripts: #{branches.map(&:first).inspect}")
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def silent_branches(branches)
|
|
444
|
+
ast_silent_branches(branches) || raise(ArgumentError, "Could not build Ruby branch from Haml scripts: #{branches.map(&:first).inspect}")
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def ruby_filters(nodes)
|
|
448
|
+
return "" if nodes.empty?
|
|
449
|
+
|
|
450
|
+
ast_ruby_filters(nodes) || statements(nodes.map { |node| "begin\n#{indent(node, 2)}\nend" }.join("\n"))
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def render_ruby_filter(node)
|
|
454
|
+
source = to_source(node)
|
|
455
|
+
parsed = (node if node.is_a?(Fragment) && node.node?) || statements(source)
|
|
456
|
+
return statements("begin\n#{indent(source, 2)}\n nil\nend") unless parsed
|
|
457
|
+
|
|
458
|
+
fragment(
|
|
459
|
+
ast_begin([
|
|
460
|
+
*statement_body_for(parsed),
|
|
461
|
+
nil_node
|
|
462
|
+
])
|
|
463
|
+
)
|
|
464
|
+
rescue SyntaxTree::Parser::ParseError
|
|
465
|
+
statements("begin\n#{indent(source, 2)}\n nil\nend")
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def format_node(node)
|
|
469
|
+
if @profiler
|
|
470
|
+
@profiler.measure(:haml_format_node, node: node.class.name) do
|
|
471
|
+
SyntaxTree::Formatter.format(+"", node, 0)
|
|
472
|
+
end
|
|
473
|
+
else
|
|
474
|
+
SyntaxTree::Formatter.format(+"", node, 0)
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def indent(value, spaces)
|
|
479
|
+
to_source(value).lines.map { |line| "#{" " * spaces}#{line}" }.join
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def compact_join(fragments)
|
|
483
|
+
Array(fragments)
|
|
484
|
+
.map { |fragment| to_source(fragment).to_s }
|
|
485
|
+
.reject(&:empty?)
|
|
486
|
+
.join("\n")
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def line_rewritten_source(source, line_no)
|
|
490
|
+
rewrite_ruby_source(source, line_no)
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def ruby_parse_error(source, line_no:, context:)
|
|
494
|
+
raise_ruby_parse_error(source, line_no: line_no, context: context)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def block_script?(source)
|
|
498
|
+
fixed_source = fix_syntax_by_adding_missing_pairs(source)
|
|
499
|
+
node = parse_expression(fixed_source, context: :block_script_predicate)
|
|
500
|
+
|
|
501
|
+
node.is_a?(SyntaxTree::MethodAddBlock)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
private
|
|
505
|
+
|
|
506
|
+
def rewrite_ruby_source(source, line_no)
|
|
507
|
+
source = rewrite_line_constant(source, line_no)
|
|
508
|
+
rewrite_global_variables(source)
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
def rewrite_line_constant(source, line_no)
|
|
512
|
+
return source.to_s unless line_no
|
|
513
|
+
|
|
514
|
+
source = source.to_s
|
|
515
|
+
return source unless source.include?("__LINE__")
|
|
516
|
+
|
|
517
|
+
line_offsets = [0]
|
|
518
|
+
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.length }
|
|
519
|
+
|
|
520
|
+
Ripper
|
|
521
|
+
.lex(source)
|
|
522
|
+
.select { |(_line, _column), type, token, _state| type == :on_kw && token == "__LINE__" }
|
|
523
|
+
.reverse_each
|
|
524
|
+
.each_with_object(source.dup) do |((line, column), _type, token, _state), rewritten|
|
|
525
|
+
offset = line_offsets.fetch(line - 1) + column
|
|
526
|
+
rewritten[offset, token.length] = line_no.to_s
|
|
527
|
+
end
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def rewrite_global_variables(source)
|
|
531
|
+
return source unless @global_variables
|
|
532
|
+
|
|
533
|
+
line_offsets = [0]
|
|
534
|
+
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.length }
|
|
535
|
+
|
|
536
|
+
Ripper
|
|
537
|
+
.lex(source)
|
|
538
|
+
.select { |(_line, _column), type, token, _state| type == :on_gvar && prop_global_variable?(token) }
|
|
539
|
+
.reverse_each
|
|
540
|
+
.each_with_object(source.dup) do |((line, column), _type, token, _state), rewritten|
|
|
541
|
+
name = token.delete_prefix("$")
|
|
542
|
+
offset = line_offsets.fetch(line - 1) + column
|
|
543
|
+
rewritten[offset, token.length] = "#{@global_variables}[#{symbol_source(name)}]"
|
|
544
|
+
end
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def prop_global_variable?(token)
|
|
548
|
+
token.match?(/\A\$[a-z]\w*\z/)
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def source_expressions(expressions)
|
|
552
|
+
case expressions.length
|
|
553
|
+
when 0
|
|
554
|
+
nil_expression
|
|
555
|
+
when 1
|
|
556
|
+
expression = expressions.fetch(0)
|
|
557
|
+
|
|
558
|
+
expression.is_a?(Fragment) ? expression : self.expression(to_source(expression))
|
|
559
|
+
else
|
|
560
|
+
Fragment.new("[#{expressions.map { |item| argument_source(item) }.join(", ")}]", nil)
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
|
|
564
|
+
def source_factory_call(factory:, tag:, children:, props:, mark:)
|
|
565
|
+
factory = expression_fragment(factory)
|
|
566
|
+
tag = expression_fragment(tag)
|
|
567
|
+
children = children.map { |child| expression_fragment(child) }
|
|
568
|
+
|
|
569
|
+
source_parts = [
|
|
570
|
+
to_source(tag),
|
|
571
|
+
*children.map { |child| argument_source(child) },
|
|
572
|
+
*keyword_props_source(props, mark: mark)
|
|
573
|
+
].compact
|
|
574
|
+
Fragment.new("#{to_source(factory)}[#{source_parts.join(", ")}]", nil)
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def ast_silent_script(source)
|
|
578
|
+
statements = parse_statements(source)
|
|
579
|
+
return nil unless statements
|
|
580
|
+
|
|
581
|
+
node = ast_begin([*statement_body_for(statements), nil_node])
|
|
582
|
+
|
|
583
|
+
Fragment.new(["begin", indent(source, 2), " nil", "end"].join("\n"), node)
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
def ast_script_block(source, body)
|
|
587
|
+
node = block_script_node(source, body)
|
|
588
|
+
return nil unless node
|
|
589
|
+
|
|
590
|
+
Fragment.new(block_source(source, body), node)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def ast_silent_script_block(source, body)
|
|
594
|
+
node = block_script_node(source, body)
|
|
595
|
+
return nil unless node
|
|
596
|
+
|
|
597
|
+
node = ast_begin([node, nil_node])
|
|
598
|
+
|
|
599
|
+
Fragment.new(["begin", indent(block_source(source, body), 2), " nil", "end"].join("\n"), node)
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def ast_branches(branches)
|
|
603
|
+
node = branch_node(branches)
|
|
604
|
+
return nil unless node
|
|
605
|
+
|
|
606
|
+
Fragment.new(branch_source(branches), node)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def ast_silent_branches(branches)
|
|
610
|
+
node = branch_node(branches)
|
|
611
|
+
return nil unless node
|
|
612
|
+
|
|
613
|
+
node = ast_begin([node, nil_node])
|
|
614
|
+
|
|
615
|
+
Fragment.new(["begin", indent(branch_source(branches), 2), " nil", "end"].join("\n"), node)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def ast_ruby_filters(nodes)
|
|
619
|
+
begins =
|
|
620
|
+
nodes.map do |node|
|
|
621
|
+
statements =
|
|
622
|
+
if node.is_a?(Fragment)
|
|
623
|
+
node
|
|
624
|
+
else
|
|
625
|
+
parse_statements(node)
|
|
626
|
+
end
|
|
627
|
+
return nil unless statements
|
|
628
|
+
|
|
629
|
+
ast_begin(statement_body_for(statements))
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
source = nodes.map { |node| ["begin", indent(to_source(node), 2), "end"].join("\n") }.join("\n")
|
|
633
|
+
|
|
634
|
+
Fragment.new(source, Statements(begins))
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def ast_begin(statement_nodes)
|
|
638
|
+
Begin(
|
|
639
|
+
BodyStmt(
|
|
640
|
+
Statements(statement_nodes),
|
|
641
|
+
nil,
|
|
642
|
+
nil,
|
|
643
|
+
nil,
|
|
644
|
+
nil
|
|
645
|
+
)
|
|
646
|
+
)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def block_source(source, body)
|
|
650
|
+
body_source = to_source(body)
|
|
651
|
+
|
|
652
|
+
if source.include?("{")
|
|
653
|
+
"#{source} #{body_source} }"
|
|
654
|
+
else
|
|
655
|
+
[source, indent(body_source, 2), "end"].join("\n")
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def branch_source(branches)
|
|
660
|
+
body =
|
|
661
|
+
branches
|
|
662
|
+
.map do |source, body|
|
|
663
|
+
if source == "else"
|
|
664
|
+
["else", indent(to_source(body), 2)].join("\n")
|
|
665
|
+
else
|
|
666
|
+
[source, indent(to_source(body), 2)].join("\n")
|
|
667
|
+
end
|
|
668
|
+
end
|
|
669
|
+
.join("\n")
|
|
670
|
+
|
|
671
|
+
"#{body}\nend"
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def body_statement(body)
|
|
675
|
+
BodyStmt(
|
|
676
|
+
Statements(Array(body).flat_map { |statement| statement_body_for(statement) }),
|
|
677
|
+
nil,
|
|
678
|
+
nil,
|
|
679
|
+
nil,
|
|
680
|
+
nil
|
|
681
|
+
)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def branch_node(branches)
|
|
685
|
+
first_source, first_body = branches.fetch(0)
|
|
686
|
+
if first_source.match?(/\Acase\b/) && nil_fragment?(first_body)
|
|
687
|
+
case_node(first_source, branches.drop(1))
|
|
688
|
+
else
|
|
689
|
+
if_node(branches)
|
|
690
|
+
end
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def if_node(branches)
|
|
694
|
+
source, body = branches.fetch(0)
|
|
695
|
+
return else_node(source, body) if source == "else"
|
|
696
|
+
|
|
697
|
+
predicate_source =
|
|
698
|
+
case source
|
|
699
|
+
when /\Aif\s+(.+)\z/ then $1
|
|
700
|
+
when /\Aelsif\s+(.+)\z/ then $1
|
|
701
|
+
else return nil
|
|
702
|
+
end
|
|
703
|
+
predicate = parse_expression(predicate_source, context: :branch_predicate)
|
|
704
|
+
return nil unless predicate
|
|
705
|
+
|
|
706
|
+
consequent =
|
|
707
|
+
if branches.length > 1
|
|
708
|
+
if_node(branches.drop(1))
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
if source.start_with?("elsif")
|
|
712
|
+
Elsif(predicate, Statements(statement_body_for(body)), consequent)
|
|
713
|
+
else
|
|
714
|
+
IfNode(predicate, Statements(statement_body_for(body)), consequent)
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def else_node(source, body)
|
|
719
|
+
return nil unless source == "else"
|
|
720
|
+
|
|
721
|
+
Else(Kw("else"), Statements(statement_body_for(body)))
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def case_node(source, branches)
|
|
725
|
+
value_source = source[/\Acase\s*(.*)\z/, 1]
|
|
726
|
+
return nil unless value_source
|
|
727
|
+
|
|
728
|
+
value = value_source.empty? ? nil : parse_expression(value_source, context: :case_value)
|
|
729
|
+
consequent = when_node(branches)
|
|
730
|
+
return nil unless consequent
|
|
731
|
+
|
|
732
|
+
Case(Kw("case"), value, consequent)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
def when_node(branches)
|
|
736
|
+
source, body = branches.fetch(0)
|
|
737
|
+
return else_node(source, body) if source == "else"
|
|
738
|
+
return nil unless source.start_with?("when ")
|
|
739
|
+
|
|
740
|
+
arguments =
|
|
741
|
+
source
|
|
742
|
+
.delete_prefix("when ")
|
|
743
|
+
.split(",")
|
|
744
|
+
.map { |argument| parse_expression(argument.strip, context: :when_argument) }
|
|
745
|
+
return nil if arguments.any?(&:nil?)
|
|
746
|
+
|
|
747
|
+
consequent =
|
|
748
|
+
if branches.length > 1
|
|
749
|
+
when_node(branches.drop(1))
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
When(Args(arguments), Statements(statement_body_for(body)), consequent)
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
def block_script_node(source, body)
|
|
756
|
+
node = parse_expression(fix_syntax_by_adding_missing_pairs(source), context: :block_script)
|
|
757
|
+
return nil unless node.is_a?(SyntaxTree::MethodAddBlock)
|
|
758
|
+
|
|
759
|
+
MethodAddBlock(
|
|
760
|
+
node.call,
|
|
761
|
+
SyntaxTree::BlockNode.new(
|
|
762
|
+
opening: node.block.opening,
|
|
763
|
+
block_var: node.block.block_var,
|
|
764
|
+
bodystmt: block_body_for(node.block, body),
|
|
765
|
+
location: node.block.location
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
end
|
|
769
|
+
|
|
770
|
+
def block_body_for(block, body)
|
|
771
|
+
statements = Statements(Array(body).flat_map { |statement| statement_body_for(statement) })
|
|
772
|
+
|
|
773
|
+
block.opening.is_a?(SyntaxTree::LBrace) ? statements : body_statement(body)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def ast_keyword_props(props, mark:)
|
|
777
|
+
return nil if props.empty?
|
|
778
|
+
|
|
779
|
+
AssocSplat(
|
|
780
|
+
HashLiteral(
|
|
781
|
+
LBrace("{"),
|
|
782
|
+
props.map { |name, value| Assoc(prop_key_node(name), argument_node(value, mark: mark)) }
|
|
783
|
+
)
|
|
784
|
+
)
|
|
785
|
+
end
|
|
786
|
+
|
|
787
|
+
def keyword_props_source(props, mark:)
|
|
788
|
+
return [] if props.empty?
|
|
789
|
+
|
|
790
|
+
props.map do |name, value|
|
|
791
|
+
"#{prop_key_source(name)} #{argument_source(value, mark: mark)}"
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
def prop_key_source(name)
|
|
796
|
+
name = name.to_s
|
|
797
|
+
return "#{name}:" if name.match?(/\A[a-zA-Z_]\w*\z/)
|
|
798
|
+
|
|
799
|
+
"#{symbol_source(name)} =>"
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
def frozen_literal_node(value)
|
|
803
|
+
case value
|
|
804
|
+
when Hash
|
|
805
|
+
freeze_node(
|
|
806
|
+
HashLiteral(
|
|
807
|
+
LBrace("{"),
|
|
808
|
+
value.map { |key, child| Assoc(literal_node(key), frozen_literal_node(child)) }
|
|
809
|
+
)
|
|
810
|
+
)
|
|
811
|
+
when Array
|
|
812
|
+
freeze_node(ArrayLiteral(LBracket("["), Args(value.map { |child| frozen_literal_node(child) })))
|
|
813
|
+
else
|
|
814
|
+
literal_node(value)
|
|
815
|
+
end
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def literal_node(value)
|
|
819
|
+
case value
|
|
820
|
+
when String
|
|
821
|
+
if value.match?(/\\|#[@${]/)
|
|
822
|
+
expression_node(value.inspect)
|
|
823
|
+
else
|
|
824
|
+
StringLiteral([TStringContent(value)], "\"")
|
|
825
|
+
end
|
|
826
|
+
when Integer
|
|
827
|
+
Int(value.to_s)
|
|
828
|
+
when Float
|
|
829
|
+
FloatLiteral(value.to_s)
|
|
830
|
+
when true
|
|
831
|
+
VarRef(Kw("true"))
|
|
832
|
+
when false
|
|
833
|
+
VarRef(Kw("false"))
|
|
834
|
+
when nil
|
|
835
|
+
nil_node
|
|
836
|
+
else
|
|
837
|
+
expression_node(value.inspect)
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
|
|
841
|
+
def literal_source(value)
|
|
842
|
+
case value
|
|
843
|
+
when String
|
|
844
|
+
value.inspect
|
|
845
|
+
when Integer, Float
|
|
846
|
+
value.to_s
|
|
847
|
+
when true
|
|
848
|
+
"true"
|
|
849
|
+
when false
|
|
850
|
+
"false"
|
|
851
|
+
when nil
|
|
852
|
+
"nil"
|
|
853
|
+
else
|
|
854
|
+
value.inspect
|
|
855
|
+
end
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def freeze_node(node)
|
|
859
|
+
CallNode(node, Period("."), Ident("freeze"), nil)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
def nil_node
|
|
863
|
+
VarRef(Kw("nil"))
|
|
864
|
+
end
|
|
865
|
+
|
|
866
|
+
def symbol_node(value)
|
|
867
|
+
if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
|
|
868
|
+
SymbolLiteral(Ident(value))
|
|
869
|
+
else
|
|
870
|
+
DynaSymbol([TStringContent(value)], ":\"")
|
|
871
|
+
end
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def symbol_source(value)
|
|
875
|
+
if value.match?(/\A[a-zA-Z_]\w*[!?=]?\z/)
|
|
876
|
+
":#{value}"
|
|
877
|
+
else
|
|
878
|
+
":#{value.inspect}"
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def prop_key_node(name)
|
|
883
|
+
name = name.to_s
|
|
884
|
+
return Label("#{name}:") if name.match?(/\A[a-zA-Z_]\w*\z/)
|
|
885
|
+
|
|
886
|
+
symbol_node(name)
|
|
887
|
+
end
|
|
888
|
+
|
|
889
|
+
def source_marked_fragment(mark, source, node)
|
|
890
|
+
marked_source = "#{mark}\n#{source}"
|
|
891
|
+
return Fragment.new(marked_source, nil) unless node
|
|
892
|
+
|
|
893
|
+
node = Statements([comment_node(mark), node])
|
|
894
|
+
|
|
895
|
+
Fragment.new(marked_source, node)
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def comment_node(value)
|
|
899
|
+
Comment(value, false)
|
|
900
|
+
end
|
|
901
|
+
|
|
902
|
+
def nil_fragment?(value)
|
|
903
|
+
value.is_a?(Fragment) && value.node.is_a?(SyntaxTree::VarRef) && to_source(value) == "nil"
|
|
904
|
+
end
|
|
905
|
+
|
|
906
|
+
def node_for(value)
|
|
907
|
+
return value.node if value.is_a?(Fragment)
|
|
908
|
+
|
|
909
|
+
parse_expression(to_source(value), context: :node_for)
|
|
910
|
+
end
|
|
911
|
+
|
|
912
|
+
def argument_node(value, mark: nil)
|
|
913
|
+
fragment = expression_fragment(value)
|
|
914
|
+
statements = []
|
|
915
|
+
statements << comment_node(mark) if mark
|
|
916
|
+
|
|
917
|
+
if fragment.node.is_a?(SyntaxTree::Statements)
|
|
918
|
+
statements.concat(fragment.node.body)
|
|
919
|
+
elsif mark
|
|
920
|
+
node = node_for(fragment)
|
|
921
|
+
return nil unless node
|
|
922
|
+
|
|
923
|
+
statements << node
|
|
924
|
+
else
|
|
925
|
+
return node_for(fragment)
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
ast_begin(statements)
|
|
929
|
+
end
|
|
930
|
+
|
|
931
|
+
def argument_source(value, mark: nil)
|
|
932
|
+
fragment = expression_fragment(value)
|
|
933
|
+
source = to_source(fragment)
|
|
934
|
+
|
|
935
|
+
if mark
|
|
936
|
+
source = "#{mark}\n#{source}"
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
if fragment.node.is_a?(SyntaxTree::Statements) || mark || (source.include?("\n") && !multiline_argument_expression?(source))
|
|
940
|
+
["begin", indent(source, 2), "end"].join("\n")
|
|
941
|
+
else
|
|
942
|
+
source
|
|
943
|
+
end
|
|
944
|
+
end
|
|
945
|
+
|
|
946
|
+
def multiline_argument_expression?(source)
|
|
947
|
+
source.start_with?("if ", "unless ", "case", "begin")
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
def statement_body_for(value)
|
|
951
|
+
return value.statement_body if value.is_a?(Fragment)
|
|
952
|
+
|
|
953
|
+
value.is_a?(SyntaxTree::Statements) ? value.body : [value].compact
|
|
954
|
+
end
|
|
955
|
+
|
|
956
|
+
def expression_node(source)
|
|
957
|
+
source = source.to_s
|
|
958
|
+
return constant_path(source) if source.match?(VALID_CONST_PATH)
|
|
959
|
+
|
|
960
|
+
parse_expression(source, context: :expression_node) || raise(ArgumentError, "Could not parse Ruby expression: #{source.inspect}")
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
def parse_expression(source, context:)
|
|
964
|
+
cached_parse(@expression_cache, source, :"haml_parse_expression:#{context}") do
|
|
965
|
+
SyntaxTree
|
|
966
|
+
.parse(source)
|
|
967
|
+
&.statements
|
|
968
|
+
&.body
|
|
969
|
+
&.find { |node| !node.instance_of?(SyntaxTree::Comment) }
|
|
970
|
+
end
|
|
971
|
+
rescue SyntaxTree::Parser::ParseError
|
|
972
|
+
nil
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def parse_statements(source)
|
|
976
|
+
cached_parse(@statements_cache, source, :haml_parse_statements) { SyntaxTree.parse(source)&.statements }
|
|
977
|
+
rescue SyntaxTree::Parser::ParseError
|
|
978
|
+
nil
|
|
979
|
+
end
|
|
980
|
+
|
|
981
|
+
def parse_program(source)
|
|
982
|
+
cached_parse(@program_cache, source, :haml_parse_program) { SyntaxTree.parse(source) }
|
|
983
|
+
rescue SyntaxTree::Parser::ParseError
|
|
984
|
+
nil
|
|
985
|
+
end
|
|
986
|
+
|
|
987
|
+
def cached_parse(cache, source, event_name)
|
|
988
|
+
source = source.to_s
|
|
989
|
+
return cache.fetch(source) if cache.key?(source)
|
|
990
|
+
|
|
991
|
+
cache[source] =
|
|
992
|
+
if @profiler
|
|
993
|
+
@profiler.measure(event_name) { yield }
|
|
994
|
+
else
|
|
995
|
+
yield
|
|
996
|
+
end
|
|
997
|
+
end
|
|
998
|
+
|
|
999
|
+
def fix_syntax_by_adding_missing_pairs(source)
|
|
1000
|
+
left_right = SyntaxSuggest::LeftRightLexCount.new
|
|
1001
|
+
SyntaxSuggest::LexAll.new(source: source).each { |lex| left_right.count_lex(lex) }
|
|
1002
|
+
|
|
1003
|
+
[source, *left_right.missing].join("\n")
|
|
1004
|
+
end
|
|
1005
|
+
|
|
1006
|
+
def raise_ruby_parse_error(source, line_no:, context:)
|
|
1007
|
+
parse_error = syntax_tree_parse_error(source)
|
|
1008
|
+
explain =
|
|
1009
|
+
SyntaxSuggest::ExplainSyntax.new(
|
|
1010
|
+
code_lines: SyntaxSuggest::CodeLine.from_source(source)
|
|
1011
|
+
).call
|
|
1012
|
+
errors = explain.errors
|
|
1013
|
+
missing = explain.missing.map { |item| explain.why(item) } - errors
|
|
1014
|
+
|
|
1015
|
+
message = [context]
|
|
1016
|
+
message << "Errors:\n #{errors.join("\n ")}" unless errors.empty?
|
|
1017
|
+
message << "Missing:\n #{missing.join("\n ")}" unless missing.empty?
|
|
1018
|
+
|
|
1019
|
+
raise RubyParseError.new(message.join("\n\n"), line: source_line_for_parse_error(line_no, parse_error))
|
|
1020
|
+
end
|
|
1021
|
+
|
|
1022
|
+
def syntax_tree_parse_error(source)
|
|
1023
|
+
SyntaxTree.parse(source)
|
|
1024
|
+
nil
|
|
1025
|
+
rescue SyntaxTree::Parser::ParseError => error
|
|
1026
|
+
error
|
|
1027
|
+
end
|
|
1028
|
+
|
|
1029
|
+
def source_line_for_parse_error(line_no, parse_error)
|
|
1030
|
+
return line_no unless line_no && parse_error&.lineno
|
|
1031
|
+
|
|
1032
|
+
line_no + parse_error.lineno - 1
|
|
1033
|
+
end
|
|
1034
|
+
end
|
|
1035
|
+
end
|
|
1036
|
+
end
|
|
1037
|
+
end
|
|
1038
|
+
end
|
|
1039
|
+
end
|