markdown-merge 7.0.0 → 7.1.3
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
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +673 -0
- data/lib/markdown/merge/backend_support.rb +200 -0
- data/lib/markdown/merge/cleanse/block_spacing.rb +248 -0
- data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
- data/lib/markdown/merge/cleanse/condensed_link_refs.rb +411 -0
- data/lib/markdown/merge/cleanse/list_marker_duplication.rb +66 -0
- data/lib/markdown/merge/cleanse/templating_corruption.rb +86 -0
- data/lib/markdown/merge/cleanse.rb +44 -0
- data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
- data/lib/markdown/merge/code_block_merger.rb +742 -0
- data/lib/markdown/merge/comment_tracker.rb +42 -0
- data/lib/markdown/merge/conflict_resolver.rb +199 -0
- data/lib/markdown/merge/debug_logger.rb +26 -0
- data/lib/markdown/merge/document_problems.rb +190 -0
- data/lib/markdown/merge/file_aligner.rb +496 -0
- data/lib/markdown/merge/file_analysis.rb +689 -0
- data/lib/markdown/merge/file_analysis_base.rb +766 -0
- data/lib/markdown/merge/freeze_node.rb +93 -0
- data/lib/markdown/merge/gap_line_node.rb +142 -0
- data/lib/markdown/merge/link_definition_formatter.rb +49 -0
- data/lib/markdown/merge/link_definition_node.rb +157 -0
- data/lib/markdown/merge/link_parser.rb +421 -0
- data/lib/markdown/merge/link_reference_rehydrator.rb +320 -0
- data/lib/markdown/merge/list_match_refiner.rb +98 -0
- data/lib/markdown/merge/list_merger.rb +322 -0
- data/lib/markdown/merge/markdown_structure.rb +123 -0
- data/lib/markdown/merge/merge_result.rb +483 -0
- data/lib/markdown/merge/node_type_normalizer.rb +126 -0
- data/lib/markdown/merge/output_builder.rb +248 -0
- data/lib/markdown/merge/partial_template_merger.rb +555 -0
- data/lib/markdown/merge/preservation_support.rb +291 -0
- data/lib/markdown/merge/rspec/shared_examples/source_preserving_provider.rb +338 -0
- data/lib/markdown/merge/smart_merger.rb +269 -0
- data/lib/markdown/merge/smart_merger_base.rb +1490 -0
- data/lib/markdown/merge/source_preserving_provider.rb +814 -0
- data/lib/markdown/merge/table_match_algorithm.rb +499 -0
- data/lib/markdown/merge/table_match_refiner.rb +132 -0
- data/lib/markdown/merge/version.rb +5 -3
- data/lib/markdown/merge/whitespace_normalizer.rb +243 -0
- data/lib/markdown/merge/wrapper_support.rb +194 -0
- data/lib/markdown/merge.rb +271 -87
- data/lib/markdown-merge.rb +7 -1
- data/sig/markdown/merge.rbs +62 -0
- data.tar.gz.sig +0 -0
- metadata +289 -15
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Merges fenced code blocks using language-specific *-merge gems.
|
|
6
|
+
#
|
|
7
|
+
# When two code blocks with the same signature are matched, this class
|
|
8
|
+
# delegates the merge to the appropriate language-specific merger:
|
|
9
|
+
# - Ruby code → prism-merge
|
|
10
|
+
# - YAML code → psych-merge
|
|
11
|
+
# - JSON code → json-merge
|
|
12
|
+
# - TOML code → toml-merge
|
|
13
|
+
#
|
|
14
|
+
# @example Basic usage
|
|
15
|
+
# merger = CodeBlockMerger.new
|
|
16
|
+
# result = merger.merge_code_blocks(template_node, dest_node, preference: :destination)
|
|
17
|
+
# if result[:merged]
|
|
18
|
+
# puts result[:content]
|
|
19
|
+
# else
|
|
20
|
+
# # Fall back to standard resolution
|
|
21
|
+
# end
|
|
22
|
+
#
|
|
23
|
+
# @example With custom mergers
|
|
24
|
+
# merger = CodeBlockMerger.new(
|
|
25
|
+
# mergers: {
|
|
26
|
+
# "ruby" => ->(template, dest, pref) { MyCustomRubyMerger.merge(template, dest, pref) },
|
|
27
|
+
# }
|
|
28
|
+
# )
|
|
29
|
+
#
|
|
30
|
+
# @see SmartMergerBase
|
|
31
|
+
# @api public
|
|
32
|
+
class CodeBlockMerger
|
|
33
|
+
# Default language-to-merger mapping
|
|
34
|
+
# Each merger is a lambda that takes (template_content, dest_content, preference)
|
|
35
|
+
# and returns { merged: true/false, content: String, stats: Hash }
|
|
36
|
+
# simplecov:disable integration - DEFAULT_MERGERS lambdas require external gems
|
|
37
|
+
DEFAULT_MERGERS = {
|
|
38
|
+
# Ruby code blocks
|
|
39
|
+
'ruby' => lambda { |template, dest, preference, **opts|
|
|
40
|
+
require 'prism/merge'
|
|
41
|
+
CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
|
|
42
|
+
},
|
|
43
|
+
'rb' => lambda { |template, dest, preference, **opts|
|
|
44
|
+
require 'prism/merge'
|
|
45
|
+
CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
# YAML code blocks
|
|
49
|
+
'yaml' => lambda { |template, dest, preference, **opts|
|
|
50
|
+
require 'psych/merge'
|
|
51
|
+
CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
|
|
52
|
+
},
|
|
53
|
+
'yml' => lambda { |template, dest, preference, **opts|
|
|
54
|
+
require 'psych/merge'
|
|
55
|
+
CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
# JSON code blocks
|
|
59
|
+
'json' => lambda { |template, dest, preference, **opts|
|
|
60
|
+
require 'json/merge'
|
|
61
|
+
CodeBlockMerger.merge_with_json(template, dest, preference, **opts)
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
# Markdown code blocks
|
|
65
|
+
'markdown' => lambda { |template, dest, preference, **opts|
|
|
66
|
+
require 'markdown/merge'
|
|
67
|
+
CodeBlockMerger.merge_with_markdown(template, dest, preference, **opts)
|
|
68
|
+
},
|
|
69
|
+
'md' => lambda { |template, dest, preference, **opts|
|
|
70
|
+
require 'markdown/merge'
|
|
71
|
+
CodeBlockMerger.merge_with_markdown(template, dest, preference, **opts)
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
# TOML code blocks
|
|
75
|
+
'toml' => lambda { |template, dest, preference, **opts|
|
|
76
|
+
require 'toml/merge'
|
|
77
|
+
CodeBlockMerger.merge_with_toml(template, dest, preference, **opts)
|
|
78
|
+
}
|
|
79
|
+
}.freeze
|
|
80
|
+
# simplecov:enable
|
|
81
|
+
|
|
82
|
+
# @return [Hash<String, Proc>] Language to merger mapping
|
|
83
|
+
attr_reader :mergers
|
|
84
|
+
|
|
85
|
+
# @return [Boolean] Whether inner-merge is enabled
|
|
86
|
+
attr_reader :enabled
|
|
87
|
+
|
|
88
|
+
# @return [Array<Ast::Merge::Runtime::Delegate>] Runtime delegates exposed by this merger
|
|
89
|
+
attr_reader :runtime_delegates
|
|
90
|
+
|
|
91
|
+
# Creates a new CodeBlockMerger.
|
|
92
|
+
#
|
|
93
|
+
# @param mergers [Hash<String, Proc>] Custom language-to-merger mapping.
|
|
94
|
+
# Mergers are merged with defaults, allowing selective overrides.
|
|
95
|
+
# @param enabled [Boolean] Whether to enable inner-merge (default: true)
|
|
96
|
+
def initialize(mergers: {}, enabled: true)
|
|
97
|
+
@mergers = DEFAULT_MERGERS.merge(mergers)
|
|
98
|
+
@enabled = enabled
|
|
99
|
+
@runtime_delegates = build_runtime_delegates.freeze
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if inner-merge is available for a language.
|
|
103
|
+
#
|
|
104
|
+
# @param language [String] The language identifier from fence_info
|
|
105
|
+
# @return [Boolean] true if a merger exists for this language
|
|
106
|
+
def supports_language?(language)
|
|
107
|
+
return false unless @enabled
|
|
108
|
+
return false if language.nil? || language.empty?
|
|
109
|
+
|
|
110
|
+
@mergers.key?(language.downcase)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Merge two code blocks using the appropriate language-specific merger.
|
|
114
|
+
#
|
|
115
|
+
# @param template_node [Object] Template code block node
|
|
116
|
+
# @param dest_node [Object] Destination code block node
|
|
117
|
+
# @param preference [Symbol] :destination or :template
|
|
118
|
+
# @param opts [Hash] Additional options passed to the merger
|
|
119
|
+
# @return [Hash] { merged: Boolean, content: String, stats: Hash }
|
|
120
|
+
def merge_code_blocks(template_node, dest_node, preference:, runtime_session: nil, parent_operation: nil, **opts)
|
|
121
|
+
if runtime_session && parent_operation
|
|
122
|
+
return merge_code_blocks_with_runtime(
|
|
123
|
+
template_node,
|
|
124
|
+
dest_node,
|
|
125
|
+
preference: preference,
|
|
126
|
+
runtime_session: runtime_session,
|
|
127
|
+
parent_operation: parent_operation,
|
|
128
|
+
**opts
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
merge_code_blocks_without_runtime(template_node, dest_node, preference: preference, **opts)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def merge_code_blocks_without_runtime(template_node, dest_node, preference:, **opts)
|
|
136
|
+
return not_merged('inner-merge disabled') unless @enabled
|
|
137
|
+
|
|
138
|
+
language = extract_language(template_node) || extract_language(dest_node)
|
|
139
|
+
return not_merged('no language specified') unless language
|
|
140
|
+
|
|
141
|
+
template_content = extract_content(template_node)
|
|
142
|
+
dest_content = extract_content(dest_node)
|
|
143
|
+
|
|
144
|
+
perform_code_block_merge(
|
|
145
|
+
language: language,
|
|
146
|
+
template_content: template_content,
|
|
147
|
+
dest_content: dest_content,
|
|
148
|
+
preference: preference,
|
|
149
|
+
reference_node: dest_node,
|
|
150
|
+
**opts
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
def merge_code_blocks_with_runtime(template_node, dest_node, preference:, runtime_session:, parent_operation:,
|
|
157
|
+
**opts)
|
|
158
|
+
operation = build_runtime_operation(
|
|
159
|
+
template_node: template_node,
|
|
160
|
+
dest_node: dest_node,
|
|
161
|
+
preference: preference,
|
|
162
|
+
runtime_session: runtime_session,
|
|
163
|
+
parent_operation: parent_operation,
|
|
164
|
+
**opts
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
parent_operation.add_child(operation)
|
|
168
|
+
parent_frame = runtime_session.frame_for(parent_operation.operation_id)
|
|
169
|
+
delegate = runtime_session.resolve_delegate_for(operation.surface, capability: :merge)
|
|
170
|
+
runtime_session.register(
|
|
171
|
+
operation,
|
|
172
|
+
frame: Ast::Merge::Runtime::Frame.new(
|
|
173
|
+
parent_operation_id: parent_operation.operation_id,
|
|
174
|
+
operation_id: operation.operation_id,
|
|
175
|
+
depth: parent_frame ? parent_frame.depth + 1 : 1,
|
|
176
|
+
surface_path: operation.surface.address,
|
|
177
|
+
language_chain: [*(parent_frame&.language_chain || [:markdown]),
|
|
178
|
+
operation.surface.effective_language].compact
|
|
179
|
+
),
|
|
180
|
+
delegate: delegate
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
unless delegate
|
|
184
|
+
reason = unsupported_runtime_reason_for(operation.surface)
|
|
185
|
+
operation.fail!(
|
|
186
|
+
diagnostic: Ast::Merge::Runtime::Diagnostic.new(
|
|
187
|
+
severity: :warn,
|
|
188
|
+
kind: :unsupported_capability,
|
|
189
|
+
operation_id: operation.operation_id,
|
|
190
|
+
surface_path: operation.surface.address,
|
|
191
|
+
message: reason,
|
|
192
|
+
metadata: {
|
|
193
|
+
capability: :merge,
|
|
194
|
+
language: operation.surface.effective_language
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
return not_merged(reason).merge(runtime_operation_id: operation.operation_id)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
operation.running!
|
|
202
|
+
child_result = delegate.merge(operation: operation, session: runtime_session)
|
|
203
|
+
if child_result.unresolved?
|
|
204
|
+
operation.unresolved!(result: child_result)
|
|
205
|
+
else
|
|
206
|
+
operation.complete!(result: child_result)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
if child_result.metadata[:merged]
|
|
210
|
+
{
|
|
211
|
+
merged: true,
|
|
212
|
+
content: child_result.replacement_text,
|
|
213
|
+
stats: child_result.metadata[:stats] || {},
|
|
214
|
+
runtime_operation_id: operation.operation_id,
|
|
215
|
+
runtime_surface_path: operation.surface.address,
|
|
216
|
+
unresolved_cases: child_result.unresolved_cases,
|
|
217
|
+
metadata: child_result.metadata
|
|
218
|
+
}
|
|
219
|
+
else
|
|
220
|
+
not_merged(child_result.metadata[:reason] || 'merger declined').merge(
|
|
221
|
+
stats: child_result.metadata[:stats] || {},
|
|
222
|
+
runtime_operation_id: operation.operation_id
|
|
223
|
+
)
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def build_runtime_operation(template_node:, dest_node:, preference:, runtime_session:, parent_operation:, **opts)
|
|
228
|
+
language = extract_language(template_node) || extract_language(dest_node)
|
|
229
|
+
reference_node = dest_node || template_node
|
|
230
|
+
surface = Ast::Merge::Runtime::Surface.new(
|
|
231
|
+
surface_kind: :markdown_fenced_code_block,
|
|
232
|
+
declared_language: language,
|
|
233
|
+
effective_language: language,
|
|
234
|
+
address: runtime_surface_address(reference_node, runtime_session),
|
|
235
|
+
parent_address: parent_operation.surface.address,
|
|
236
|
+
span: runtime_surface_span(reference_node),
|
|
237
|
+
reconstruction_strategy: :portable_write,
|
|
238
|
+
metadata: {
|
|
239
|
+
fence_info: reference_node.respond_to?(:fence_info) ? reference_node.fence_info : nil,
|
|
240
|
+
language: language
|
|
241
|
+
}.compact
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
Ast::Merge::Runtime::Operation.new(
|
|
245
|
+
operation_id: "markdown-code-block-#{runtime_session.operations.count}",
|
|
246
|
+
surface: surface,
|
|
247
|
+
template_fragment: rebuild_code_block(language, extract_content(template_node), template_node),
|
|
248
|
+
destination_fragment: rebuild_code_block(language, extract_content(dest_node), dest_node),
|
|
249
|
+
requested_strategy: :delegate_child_surface,
|
|
250
|
+
options: {
|
|
251
|
+
preference: preference,
|
|
252
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
253
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
254
|
+
unresolved_policy: Ast::Merge::UnresolvedPolicy.coerce(opts[:unresolved_policy]).to_h,
|
|
255
|
+
template_content: extract_content(template_node),
|
|
256
|
+
destination_content: extract_content(dest_node),
|
|
257
|
+
reference_node: reference_node
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def runtime_surface_address(reference_node, runtime_session)
|
|
263
|
+
span = runtime_surface_span(reference_node)
|
|
264
|
+
suffix =
|
|
265
|
+
if span
|
|
266
|
+
"L#{span.begin}-L#{span.end}"
|
|
267
|
+
else
|
|
268
|
+
"operation-#{runtime_session.operations.count}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
"document[0] > fenced_code_block[#{suffix}]"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def runtime_surface_span(node)
|
|
275
|
+
position = node_source_position(node)
|
|
276
|
+
start_line = position&.dig(:start_line)
|
|
277
|
+
end_line = position&.dig(:end_line)
|
|
278
|
+
return unless start_line && end_line
|
|
279
|
+
|
|
280
|
+
start_line..end_line
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def node_source_position(node)
|
|
284
|
+
raw_node = unwrap_code_block_node(node)
|
|
285
|
+
raw_node&.source_position
|
|
286
|
+
rescue NoMethodError
|
|
287
|
+
nil
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def build_runtime_delegates
|
|
291
|
+
[
|
|
292
|
+
Ast::Merge::Runtime::Delegate.new(
|
|
293
|
+
name: 'markdown-code-block-inner-merge',
|
|
294
|
+
priority: 100,
|
|
295
|
+
surface_kinds: [:markdown_fenced_code_block],
|
|
296
|
+
languages: mergers.keys,
|
|
297
|
+
capabilities: { merge: [:markdown_fenced_code_block] },
|
|
298
|
+
merge: method(:merge_runtime_surface),
|
|
299
|
+
metadata: {
|
|
300
|
+
source: :markdown_merge,
|
|
301
|
+
languages: mergers.keys.sort
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
]
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def merge_runtime_surface(operation:, session:)
|
|
308
|
+
result = perform_code_block_merge(
|
|
309
|
+
language: operation.surface.effective_language,
|
|
310
|
+
template_content: operation.options[:template_content].to_s,
|
|
311
|
+
dest_content: operation.options[:destination_content].to_s,
|
|
312
|
+
preference: operation.options[:preference],
|
|
313
|
+
reference_node: operation.options[:reference_node],
|
|
314
|
+
add_template_only_nodes: operation.options.fetch(:add_template_only_nodes, false),
|
|
315
|
+
resolution_mode: operation.options[:resolution_mode],
|
|
316
|
+
unresolved_policy: operation.options[:unresolved_policy]
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if result[:merged]
|
|
320
|
+
Ast::Merge::Runtime::ChildResult.new(
|
|
321
|
+
replacement_text: result[:content],
|
|
322
|
+
diagnostics: operation.diagnostics,
|
|
323
|
+
capabilities_used: %i[delegated_child_merge language_specific_merge],
|
|
324
|
+
capabilities_missing: [],
|
|
325
|
+
unresolved_cases: result[:unresolved_cases] || [],
|
|
326
|
+
metadata: {
|
|
327
|
+
merged: true,
|
|
328
|
+
stats: result[:stats] || {},
|
|
329
|
+
language: operation.surface.effective_language,
|
|
330
|
+
delegate_name: operation.delegate_name,
|
|
331
|
+
session_policy: session.policy_context
|
|
332
|
+
}.merge(result[:metadata] || {})
|
|
333
|
+
)
|
|
334
|
+
else
|
|
335
|
+
operation.add_diagnostic(
|
|
336
|
+
Ast::Merge::Runtime::Diagnostic.new(
|
|
337
|
+
severity: :warn,
|
|
338
|
+
kind: :delegated_merge_declined,
|
|
339
|
+
operation_id: operation.operation_id,
|
|
340
|
+
surface_path: operation.surface.address,
|
|
341
|
+
message: result[:reason].to_s,
|
|
342
|
+
metadata: {
|
|
343
|
+
language: operation.surface.effective_language
|
|
344
|
+
}
|
|
345
|
+
)
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
Ast::Merge::Runtime::ChildResult.new(
|
|
349
|
+
replacement_text: '',
|
|
350
|
+
diagnostics: operation.diagnostics,
|
|
351
|
+
capabilities_used: [:delegated_child_merge],
|
|
352
|
+
capabilities_missing: [:language_specific_merge],
|
|
353
|
+
metadata: {
|
|
354
|
+
merged: false,
|
|
355
|
+
reason: result[:reason],
|
|
356
|
+
stats: result[:stats] || {},
|
|
357
|
+
language: operation.surface.effective_language,
|
|
358
|
+
delegate_name: operation.delegate_name,
|
|
359
|
+
session_policy: session.policy_context
|
|
360
|
+
}.merge(result[:metadata] || {})
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def perform_code_block_merge(language:, template_content:, dest_content:, preference:, reference_node:, **opts)
|
|
366
|
+
return not_merged('no language specified') unless language
|
|
367
|
+
|
|
368
|
+
merger = @mergers[language.to_s.downcase]
|
|
369
|
+
return not_merged("no merger for language: #{language}") unless merger
|
|
370
|
+
|
|
371
|
+
if template_content == dest_content
|
|
372
|
+
return {
|
|
373
|
+
merged: true,
|
|
374
|
+
content: rebuild_code_block(language, dest_content, reference_node),
|
|
375
|
+
stats: { decision: :identical }
|
|
376
|
+
}
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
begin
|
|
380
|
+
result = merger.call(template_content, dest_content, preference, nested_mergers: @mergers, **opts)
|
|
381
|
+
if result[:merged]
|
|
382
|
+
metadata = (result[:metadata] || {}).dup
|
|
383
|
+
metadata[:root_apply_candidates_by_case_id] = root_apply_candidates_by_case_id(
|
|
384
|
+
merger: merger,
|
|
385
|
+
unresolved_cases: result[:unresolved_cases],
|
|
386
|
+
language: language,
|
|
387
|
+
template_content: template_content,
|
|
388
|
+
dest_content: dest_content,
|
|
389
|
+
preference: preference,
|
|
390
|
+
reference_node: reference_node,
|
|
391
|
+
**opts
|
|
392
|
+
)
|
|
393
|
+
metadata[:delegated_apply_renderer] = delegated_apply_renderer(
|
|
394
|
+
merger: merger,
|
|
395
|
+
language: language,
|
|
396
|
+
template_content: template_content,
|
|
397
|
+
dest_content: dest_content,
|
|
398
|
+
preference: preference,
|
|
399
|
+
reference_node: reference_node,
|
|
400
|
+
**opts
|
|
401
|
+
)
|
|
402
|
+
{
|
|
403
|
+
merged: true,
|
|
404
|
+
content: rebuild_code_block(language, result[:content], reference_node),
|
|
405
|
+
stats: result[:stats] || {},
|
|
406
|
+
unresolved_cases: result[:unresolved_cases] || [],
|
|
407
|
+
metadata: metadata
|
|
408
|
+
}
|
|
409
|
+
else
|
|
410
|
+
not_merged(result[:reason] || 'merger declined').merge(
|
|
411
|
+
stats: result[:stats] || {},
|
|
412
|
+
metadata: result[:metadata] || {}
|
|
413
|
+
)
|
|
414
|
+
end
|
|
415
|
+
rescue LoadError => e
|
|
416
|
+
not_merged("merger gem not available: #{e.message}")
|
|
417
|
+
rescue TreeHaver::Error => e
|
|
418
|
+
not_merged("backend not available: #{e.message}")
|
|
419
|
+
rescue StandardError => e
|
|
420
|
+
if defined?(::Prism::Merge::ParseError) && e.is_a?(::Prism::Merge::ParseError)
|
|
421
|
+
not_merged("Ruby parse error: #{e.message}")
|
|
422
|
+
else
|
|
423
|
+
not_merged("merge failed: #{e.class}: #{e.message}")
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def unsupported_runtime_reason_for(surface)
|
|
429
|
+
language = surface.effective_language || surface.declared_language
|
|
430
|
+
return 'no language specified' unless language
|
|
431
|
+
|
|
432
|
+
"no merger for language: #{language}"
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def root_apply_candidates_by_case_id(merger:, unresolved_cases:, language:, template_content:, dest_content:,
|
|
436
|
+
preference:, reference_node:, **opts)
|
|
437
|
+
cases = Array(unresolved_cases)
|
|
438
|
+
return {} unless cases.one?
|
|
439
|
+
|
|
440
|
+
resolution_case = cases.first
|
|
441
|
+
{
|
|
442
|
+
resolution_case.case_id => {
|
|
443
|
+
template: rebuild_code_block(
|
|
444
|
+
language,
|
|
445
|
+
merged_delegate_content_for(
|
|
446
|
+
merger: merger,
|
|
447
|
+
template_content: template_content,
|
|
448
|
+
dest_content: dest_content,
|
|
449
|
+
preference: preference,
|
|
450
|
+
case_id: resolution_case.case_id,
|
|
451
|
+
selection: :template,
|
|
452
|
+
**opts
|
|
453
|
+
),
|
|
454
|
+
reference_node
|
|
455
|
+
),
|
|
456
|
+
destination: rebuild_code_block(
|
|
457
|
+
language,
|
|
458
|
+
merged_delegate_content_for(
|
|
459
|
+
merger: merger,
|
|
460
|
+
template_content: template_content,
|
|
461
|
+
dest_content: dest_content,
|
|
462
|
+
preference: preference,
|
|
463
|
+
case_id: resolution_case.case_id,
|
|
464
|
+
selection: :destination,
|
|
465
|
+
**opts
|
|
466
|
+
),
|
|
467
|
+
reference_node
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def delegated_apply_renderer(merger:, language:, template_content:, dest_content:, preference:, reference_node:,
|
|
474
|
+
**opts)
|
|
475
|
+
lambda do |selections|
|
|
476
|
+
delegated_result = delegated_merge_result_for(
|
|
477
|
+
merger: merger,
|
|
478
|
+
template_content: template_content,
|
|
479
|
+
dest_content: dest_content,
|
|
480
|
+
preference: preference,
|
|
481
|
+
apply_unresolved_resolutions: selections,
|
|
482
|
+
**opts
|
|
483
|
+
)
|
|
484
|
+
{
|
|
485
|
+
content: rebuild_code_block(language, delegated_result[:content].to_s, reference_node),
|
|
486
|
+
unresolved_cases: delegated_result[:unresolved_cases] || [],
|
|
487
|
+
metadata: delegated_result[:metadata] || {}
|
|
488
|
+
}
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
def merged_delegate_content_for(merger:, template_content:, dest_content:, preference:, case_id: nil, selection: nil,
|
|
493
|
+
apply_unresolved_resolutions: nil, **opts)
|
|
494
|
+
delegated_merge_result_for(
|
|
495
|
+
merger: merger,
|
|
496
|
+
template_content: template_content,
|
|
497
|
+
dest_content: dest_content,
|
|
498
|
+
preference: preference,
|
|
499
|
+
case_id: case_id,
|
|
500
|
+
selection: selection,
|
|
501
|
+
apply_unresolved_resolutions: apply_unresolved_resolutions,
|
|
502
|
+
**opts
|
|
503
|
+
)[:content].to_s
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def delegated_merge_result_for(merger:, template_content:, dest_content:, preference:, case_id: nil, selection: nil,
|
|
507
|
+
apply_unresolved_resolutions: nil, **opts)
|
|
508
|
+
merger.call(
|
|
509
|
+
template_content,
|
|
510
|
+
dest_content,
|
|
511
|
+
preference,
|
|
512
|
+
**opts,
|
|
513
|
+
apply_unresolved_resolutions: apply_unresolved_resolutions || { case_id => selection }
|
|
514
|
+
)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# Extract language from a code block node.
|
|
518
|
+
#
|
|
519
|
+
# @param node [Object] The code block node
|
|
520
|
+
# @return [String, nil] The language identifier
|
|
521
|
+
def extract_language(node)
|
|
522
|
+
raw_node = unwrap_code_block_node(node)
|
|
523
|
+
info = safe_code_block_value(raw_node, :fence_info)
|
|
524
|
+
return if info.nil? || info.empty?
|
|
525
|
+
|
|
526
|
+
# fence_info may contain additional info after the language (e.g., "ruby linenos")
|
|
527
|
+
info.split(/\s+/).first
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
# Extract content from a code block node.
|
|
531
|
+
#
|
|
532
|
+
# @param node [Object] The code block node
|
|
533
|
+
# @return [String] The code content
|
|
534
|
+
def extract_content(node)
|
|
535
|
+
raw_node = unwrap_code_block_node(node)
|
|
536
|
+
safe_code_block_value(raw_node, :string_content, :text).to_s
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
# Rebuild a fenced code block with merged content.
|
|
540
|
+
#
|
|
541
|
+
# @param language [String] The language identifier
|
|
542
|
+
# @param content [String] The merged content
|
|
543
|
+
# @param reference_node [Object] Node to copy fence style from
|
|
544
|
+
# @return [String] The reconstructed code block
|
|
545
|
+
def rebuild_code_block(language, content, _reference_node)
|
|
546
|
+
# Ensure content ends with newline for proper fence closing
|
|
547
|
+
content = "#{content.chomp}\n" unless content.end_with?("\n")
|
|
548
|
+
|
|
549
|
+
# Use backticks as default fence
|
|
550
|
+
fence = '```'
|
|
551
|
+
|
|
552
|
+
"#{fence}#{language}\n#{content}#{fence}\n"
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Return a not-merged result.
|
|
556
|
+
#
|
|
557
|
+
# @param reason [String] Why merge was not performed
|
|
558
|
+
# @return [Hash] Not-merged result hash
|
|
559
|
+
def not_merged(reason)
|
|
560
|
+
{ merged: false, reason: reason }
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def unwrap_code_block_node(node)
|
|
564
|
+
return node unless defined?(Ast::Merge::NodeTyping::Wrapper) && node.is_a?(Ast::Merge::NodeTyping::Wrapper)
|
|
565
|
+
|
|
566
|
+
Ast::Merge::NodeTyping.unwrap(node)
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def safe_code_block_value(node, *methods)
|
|
570
|
+
methods.each do |method_name|
|
|
571
|
+
return node.public_send(method_name) if node
|
|
572
|
+
rescue NoMethodError
|
|
573
|
+
next
|
|
574
|
+
rescue Exception => e
|
|
575
|
+
next if e.instance_of?(::RSpec::Mocks::MockExpectationError)
|
|
576
|
+
|
|
577
|
+
raise
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
nil
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
class << self
|
|
584
|
+
# Merge Ruby code using prism-merge.
|
|
585
|
+
#
|
|
586
|
+
# @param template [String] Template Ruby code
|
|
587
|
+
# @param dest [String] Destination Ruby code
|
|
588
|
+
# @param preference [Symbol] :destination or :template
|
|
589
|
+
# @return [Hash] Merge result
|
|
590
|
+
# @raise [Prism::Merge::ParseError] If template or dest has syntax errors
|
|
591
|
+
# @note Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS
|
|
592
|
+
def merge_with_prism(template, dest, preference, **opts)
|
|
593
|
+
merger = ::Prism::Merge::SmartMerger.new(
|
|
594
|
+
template,
|
|
595
|
+
dest,
|
|
596
|
+
preference: preference,
|
|
597
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
598
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
599
|
+
unresolved_policy: opts[:unresolved_policy]
|
|
600
|
+
)
|
|
601
|
+
merge_result = merger.merge_result
|
|
602
|
+
if opts[:apply_unresolved_resolutions]
|
|
603
|
+
merge_result.apply_unresolved_resolutions!(opts[:apply_unresolved_resolutions])
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
{
|
|
607
|
+
merged: true,
|
|
608
|
+
content: merge_result.to_s,
|
|
609
|
+
stats: merger.stats,
|
|
610
|
+
unresolved_cases: merge_result.unresolved_cases
|
|
611
|
+
}
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# Merge YAML code using psych-merge.
|
|
615
|
+
#
|
|
616
|
+
# @param template [String] Template YAML code
|
|
617
|
+
# @param dest [String] Destination YAML code
|
|
618
|
+
# @param preference [Symbol] :destination or :template
|
|
619
|
+
# @return [Hash] Merge result
|
|
620
|
+
# @raise [Psych::Merge::ParseError] If template or dest has syntax errors
|
|
621
|
+
# @note Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS
|
|
622
|
+
def merge_with_psych(template, dest, preference, **opts)
|
|
623
|
+
merger = ::Psych::Merge::SmartMerger.new(
|
|
624
|
+
template,
|
|
625
|
+
dest,
|
|
626
|
+
preference: preference,
|
|
627
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
628
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
629
|
+
unresolved_policy: opts[:unresolved_policy]
|
|
630
|
+
)
|
|
631
|
+
merge_result = merger.merge_result
|
|
632
|
+
if opts[:apply_unresolved_resolutions]
|
|
633
|
+
merge_result.apply_unresolved_resolutions!(opts[:apply_unresolved_resolutions])
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
{
|
|
637
|
+
merged: true,
|
|
638
|
+
content: merge_result.to_yaml,
|
|
639
|
+
stats: merger.stats,
|
|
640
|
+
unresolved_cases: merge_result.unresolved_cases
|
|
641
|
+
}
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Merge JSON code using json-merge.
|
|
645
|
+
#
|
|
646
|
+
# @param template [String] Template JSON code
|
|
647
|
+
# @param dest [String] Destination JSON code
|
|
648
|
+
# @param preference [Symbol] :destination or :template
|
|
649
|
+
# @return [Hash] Merge result
|
|
650
|
+
# @raise [Json::Merge::ParseError] If template or dest has syntax errors
|
|
651
|
+
# @note Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS
|
|
652
|
+
def merge_with_json(template, dest, preference, **opts)
|
|
653
|
+
merger = ::Json::Merge::SmartMerger.new(
|
|
654
|
+
template,
|
|
655
|
+
dest,
|
|
656
|
+
preference: preference,
|
|
657
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
658
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
659
|
+
unresolved_policy: opts[:unresolved_policy]
|
|
660
|
+
)
|
|
661
|
+
merge_result = merger.merge_result
|
|
662
|
+
if opts[:apply_unresolved_resolutions]
|
|
663
|
+
merge_result.apply_unresolved_resolutions!(opts[:apply_unresolved_resolutions])
|
|
664
|
+
end
|
|
665
|
+
|
|
666
|
+
{
|
|
667
|
+
merged: true,
|
|
668
|
+
content: merge_result.to_json,
|
|
669
|
+
stats: merger.stats,
|
|
670
|
+
unresolved_cases: merge_result.unresolved_cases
|
|
671
|
+
}
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
# Merge Markdown code using markdown-merge.
|
|
675
|
+
#
|
|
676
|
+
# @param template [String] Template Markdown code
|
|
677
|
+
# @param dest [String] Destination Markdown code
|
|
678
|
+
# @param preference [Symbol] :destination or :template
|
|
679
|
+
# @return [Hash] Merge result
|
|
680
|
+
def merge_with_markdown(template, dest, preference, **opts)
|
|
681
|
+
nested_code_block_merger = CodeBlockMerger.new(
|
|
682
|
+
mergers: opts.fetch(:nested_mergers, {}),
|
|
683
|
+
enabled: opts.fetch(:inner_merge_code_blocks, true)
|
|
684
|
+
)
|
|
685
|
+
merger = ::Markdown::Merge::SmartMerger.new(
|
|
686
|
+
template,
|
|
687
|
+
dest,
|
|
688
|
+
preference: preference,
|
|
689
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
690
|
+
inner_merge_code_blocks: nested_code_block_merger,
|
|
691
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
692
|
+
unresolved_policy: opts[:unresolved_policy]
|
|
693
|
+
)
|
|
694
|
+
merge_result = merger.merge_result
|
|
695
|
+
if opts[:apply_unresolved_resolutions]
|
|
696
|
+
merge_result.apply_unresolved_resolutions!(opts[:apply_unresolved_resolutions])
|
|
697
|
+
end
|
|
698
|
+
|
|
699
|
+
{
|
|
700
|
+
merged: true,
|
|
701
|
+
content: merge_result.to_s,
|
|
702
|
+
stats: merger.stats,
|
|
703
|
+
unresolved_cases: merge_result.unresolved_cases,
|
|
704
|
+
metadata: {
|
|
705
|
+
nested_runtime_session: merger.runtime_session&.to_h
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
# Merge TOML code using toml-merge.
|
|
711
|
+
#
|
|
712
|
+
# @param template [String] Template TOML code
|
|
713
|
+
# @param dest [String] Destination TOML code
|
|
714
|
+
# @param preference [Symbol] :destination or :template
|
|
715
|
+
# @return [Hash] Merge result
|
|
716
|
+
# @raise [Toml::Merge::ParseError] If template or dest has syntax errors
|
|
717
|
+
# @note Errors are handled by merge_code_blocks when called via DEFAULT_MERGERS
|
|
718
|
+
def merge_with_toml(template, dest, preference, **opts)
|
|
719
|
+
merger = ::Toml::Merge::SmartMerger.new(
|
|
720
|
+
template,
|
|
721
|
+
dest,
|
|
722
|
+
preference: preference,
|
|
723
|
+
add_template_only_nodes: opts.fetch(:add_template_only_nodes, false),
|
|
724
|
+
resolution_mode: opts.fetch(:resolution_mode, :eager),
|
|
725
|
+
unresolved_policy: opts[:unresolved_policy]
|
|
726
|
+
)
|
|
727
|
+
merge_result = merger.merge_result
|
|
728
|
+
if opts[:apply_unresolved_resolutions]
|
|
729
|
+
merge_result.apply_unresolved_resolutions!(opts[:apply_unresolved_resolutions])
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
{
|
|
733
|
+
merged: true,
|
|
734
|
+
content: merge_result.to_toml,
|
|
735
|
+
stats: merger.stats,
|
|
736
|
+
unresolved_cases: merge_result.unresolved_cases
|
|
737
|
+
}
|
|
738
|
+
end
|
|
739
|
+
end
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
end
|