markdown-merge 1.0.3 → 7.1.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +99 -482
  5. data/lib/markdown/merge/backend_support.rb +200 -0
  6. data/lib/markdown/merge/cleanse/block_spacing.rb +18 -23
  7. data/lib/markdown/merge/cleanse/code_fence_spacing.rb +16 -16
  8. data/lib/markdown/merge/cleanse/condensed_link_refs.rb +36 -30
  9. data/lib/markdown/merge/cleanse/list_marker_duplication.rb +66 -0
  10. data/lib/markdown/merge/cleanse/templating_corruption.rb +86 -0
  11. data/lib/markdown/merge/cleanse.rb +5 -3
  12. data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
  13. data/lib/markdown/merge/code_block_merger.rb +489 -47
  14. data/lib/markdown/merge/comment_tracker.rb +42 -0
  15. data/lib/markdown/merge/conflict_resolver.rb +77 -6
  16. data/lib/markdown/merge/debug_logger.rb +2 -2
  17. data/lib/markdown/merge/document_problems.rb +3 -3
  18. data/lib/markdown/merge/file_aligner.rb +433 -133
  19. data/lib/markdown/merge/file_analysis.rb +387 -51
  20. data/lib/markdown/merge/file_analysis_base.rb +188 -51
  21. data/lib/markdown/merge/gap_line_node.rb +14 -8
  22. data/lib/markdown/merge/link_definition_node.rb +5 -5
  23. data/lib/markdown/merge/link_parser.rb +60 -60
  24. data/lib/markdown/merge/link_reference_rehydrator.rb +14 -14
  25. data/lib/markdown/merge/list_match_refiner.rb +98 -0
  26. data/lib/markdown/merge/list_merger.rb +322 -0
  27. data/lib/markdown/merge/markdown_structure.rb +3 -3
  28. data/lib/markdown/merge/merge_result.rb +321 -4
  29. data/lib/markdown/merge/node_type_normalizer.rb +6 -6
  30. data/lib/markdown/merge/output_builder.rb +101 -19
  31. data/lib/markdown/merge/partial_template_merger.rb +248 -27
  32. data/lib/markdown/merge/preservation_support.rb +291 -0
  33. data/lib/markdown/merge/smart_merger.rb +62 -14
  34. data/lib/markdown/merge/smart_merger_base.rb +929 -60
  35. data/lib/markdown/merge/table_match_algorithm.rb +22 -27
  36. data/lib/markdown/merge/table_match_refiner.rb +6 -10
  37. data/lib/markdown/merge/version.rb +5 -4
  38. data/lib/markdown/merge/whitespace_normalizer.rb +25 -33
  39. data/lib/markdown/merge/wrapper_support.rb +194 -0
  40. data/lib/markdown/merge.rb +669 -122
  41. data/lib/markdown-merge.rb +9 -4
  42. data/sig/markdown/merge.rbs +3 -336
  43. data.tar.gz.sig +0 -0
  44. metadata +104 -93
  45. metadata.gz.sig +0 -0
  46. data/CHANGELOG.md +0 -308
  47. data/CITATION.cff +0 -20
  48. data/CODE_OF_CONDUCT.md +0 -134
  49. data/CONTRIBUTING.md +0 -227
  50. data/FUNDING.md +0 -74
  51. data/LICENSE.txt +0 -21
  52. data/REEK +0 -0
  53. data/RUBOCOP.md +0 -71
  54. data/SECURITY.md +0 -21
@@ -33,41 +33,51 @@ module Markdown
33
33
  # Default language-to-merger mapping
34
34
  # Each merger is a lambda that takes (template_content, dest_content, preference)
35
35
  # and returns { merged: true/false, content: String, stats: Hash }
36
- # :nocov: integration - DEFAULT_MERGERS lambdas require external gems
36
+ # simplecov:disable integration - DEFAULT_MERGERS lambdas require external gems
37
37
  DEFAULT_MERGERS = {
38
38
  # Ruby code blocks
39
- "ruby" => ->(template, dest, preference, **opts) {
40
- require "prism/merge"
39
+ 'ruby' => lambda { |template, dest, preference, **opts|
40
+ require 'prism/merge'
41
41
  CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
42
42
  },
43
- "rb" => ->(template, dest, preference, **opts) {
44
- require "prism/merge"
43
+ 'rb' => lambda { |template, dest, preference, **opts|
44
+ require 'prism/merge'
45
45
  CodeBlockMerger.merge_with_prism(template, dest, preference, **opts)
46
46
  },
47
47
 
48
48
  # YAML code blocks
49
- "yaml" => ->(template, dest, preference, **opts) {
50
- require "psych/merge"
49
+ 'yaml' => lambda { |template, dest, preference, **opts|
50
+ require 'psych/merge'
51
51
  CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
52
52
  },
53
- "yml" => ->(template, dest, preference, **opts) {
54
- require "psych/merge"
53
+ 'yml' => lambda { |template, dest, preference, **opts|
54
+ require 'psych/merge'
55
55
  CodeBlockMerger.merge_with_psych(template, dest, preference, **opts)
56
56
  },
57
57
 
58
58
  # JSON code blocks
59
- "json" => ->(template, dest, preference, **opts) {
60
- require "json/merge"
59
+ 'json' => lambda { |template, dest, preference, **opts|
60
+ require 'json/merge'
61
61
  CodeBlockMerger.merge_with_json(template, dest, preference, **opts)
62
62
  },
63
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
+
64
74
  # TOML code blocks
65
- "toml" => ->(template, dest, preference, **opts) {
66
- require "toml/merge"
75
+ 'toml' => lambda { |template, dest, preference, **opts|
76
+ require 'toml/merge'
67
77
  CodeBlockMerger.merge_with_toml(template, dest, preference, **opts)
68
- },
78
+ }
69
79
  }.freeze
70
- # :nocov:
80
+ # simplecov:enable
71
81
 
72
82
  # @return [Hash<String, Proc>] Language to merger mapping
73
83
  attr_reader :mergers
@@ -75,6 +85,9 @@ module Markdown
75
85
  # @return [Boolean] Whether inner-merge is enabled
76
86
  attr_reader :enabled
77
87
 
88
+ # @return [Array<Ast::Merge::Runtime::Delegate>] Runtime delegates exposed by this merger
89
+ attr_reader :runtime_delegates
90
+
78
91
  # Creates a new CodeBlockMerger.
79
92
  #
80
93
  # @param mergers [Hash<String, Proc>] Custom language-to-merger mapping.
@@ -83,6 +96,7 @@ module Markdown
83
96
  def initialize(mergers: {}, enabled: true)
84
97
  @mergers = DEFAULT_MERGERS.merge(mergers)
85
98
  @enabled = enabled
99
+ @runtime_delegates = build_runtime_delegates.freeze
86
100
  end
87
101
 
88
102
  # Check if inner-merge is available for a language.
@@ -103,67 +117,410 @@ module Markdown
103
117
  # @param preference [Symbol] :destination or :template
104
118
  # @param opts [Hash] Additional options passed to the merger
105
119
  # @return [Hash] { merged: Boolean, content: String, stats: Hash }
106
- def merge_code_blocks(template_node, dest_node, preference:, **opts)
107
- return not_merged("inner-merge disabled") unless @enabled
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
108
131
 
109
- language = extract_language(template_node) || extract_language(dest_node)
110
- return not_merged("no language specified") unless language
132
+ merge_code_blocks_without_runtime(template_node, dest_node, preference: preference, **opts)
133
+ end
111
134
 
112
- merger = @mergers[language.downcase]
113
- return not_merged("no merger for language: #{language}") unless merger
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
114
140
 
115
141
  template_content = extract_content(template_node)
116
142
  dest_content = extract_content(dest_node)
117
143
 
118
- # If content is identical, no need to merge
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
+
119
371
  if template_content == dest_content
120
372
  return {
121
373
  merged: true,
122
- content: rebuild_code_block(language, dest_content, dest_node),
123
- stats: {decision: :identical},
374
+ content: rebuild_code_block(language, dest_content, reference_node),
375
+ stats: { decision: :identical }
124
376
  }
125
377
  end
126
378
 
127
379
  begin
128
- result = merger.call(template_content, dest_content, preference, **opts)
380
+ result = merger.call(template_content, dest_content, preference, nested_mergers: @mergers, **opts)
129
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
+ )
130
402
  {
131
403
  merged: true,
132
- content: rebuild_code_block(language, result[:content], dest_node),
404
+ content: rebuild_code_block(language, result[:content], reference_node),
133
405
  stats: result[:stats] || {},
406
+ unresolved_cases: result[:unresolved_cases] || [],
407
+ metadata: metadata
134
408
  }
135
409
  else
136
- not_merged(result[:reason] || "merger declined")
410
+ not_merged(result[:reason] || 'merger declined').merge(
411
+ stats: result[:stats] || {},
412
+ metadata: result[:metadata] || {}
413
+ )
137
414
  end
138
415
  rescue LoadError => e
139
416
  not_merged("merger gem not available: #{e.message}")
140
417
  rescue TreeHaver::Error => e
141
- # TreeHaver::NotAvailable and TreeHaver::Error inherit from Exception (not StandardError)
142
- # for safety reasons related to backend conflicts. We catch them here to handle
143
- # gracefully when a backend isn't properly configured.
144
418
  not_merged("backend not available: #{e.message}")
145
419
  rescue StandardError => e
146
- # :nocov: defensive - Prism::Merge::ParseError handling when prism/merge is loaded
147
- # Check for Prism::Merge::ParseError if prism/merge is loaded
148
420
  if defined?(::Prism::Merge::ParseError) && e.is_a?(::Prism::Merge::ParseError)
149
421
  not_merged("Ruby parse error: #{e.message}")
150
422
  else
151
423
  not_merged("merge failed: #{e.class}: #{e.message}")
152
424
  end
153
- # :nocov:
154
425
  end
155
426
  end
156
427
 
157
- private
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
158
516
 
159
517
  # Extract language from a code block node.
160
518
  #
161
519
  # @param node [Object] The code block node
162
520
  # @return [String, nil] The language identifier
163
521
  def extract_language(node)
164
- return unless node.respond_to?(:fence_info)
165
-
166
- info = node.fence_info
522
+ raw_node = unwrap_code_block_node(node)
523
+ info = safe_code_block_value(raw_node, :fence_info)
167
524
  return if info.nil? || info.empty?
168
525
 
169
526
  # fence_info may contain additional info after the language (e.g., "ruby linenos")
@@ -175,7 +532,8 @@ module Markdown
175
532
  # @param node [Object] The code block node
176
533
  # @return [String] The code content
177
534
  def extract_content(node)
178
- node.string_content || ""
535
+ raw_node = unwrap_code_block_node(node)
536
+ safe_code_block_value(raw_node, :string_content, :text).to_s
179
537
  end
180
538
 
181
539
  # Rebuild a fenced code block with merged content.
@@ -184,14 +542,14 @@ module Markdown
184
542
  # @param content [String] The merged content
185
543
  # @param reference_node [Object] Node to copy fence style from
186
544
  # @return [String] The reconstructed code block
187
- def rebuild_code_block(language, content, reference_node)
545
+ def rebuild_code_block(language, content, _reference_node)
188
546
  # Ensure content ends with newline for proper fence closing
189
- content = content.chomp + "\n" unless content.end_with?("\n")
547
+ content = "#{content.chomp}\n" unless content.end_with?("\n")
190
548
 
191
549
  # Use backticks as default fence
192
- fence = "```"
550
+ fence = '```'
193
551
 
194
- "#{fence}#{language}\n#{content}#{fence}"
552
+ "#{fence}#{language}\n#{content}#{fence}\n"
195
553
  end
196
554
 
197
555
  # Return a not-merged result.
@@ -199,7 +557,27 @@ module Markdown
199
557
  # @param reason [String] Why merge was not performed
200
558
  # @return [Hash] Not-merged result hash
201
559
  def not_merged(reason)
202
- {merged: false, reason: 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
203
581
  end
204
582
 
205
583
  class << self
@@ -217,12 +595,19 @@ module Markdown
217
595
  dest,
218
596
  preference: preference,
219
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]
220
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
221
605
 
222
606
  {
223
607
  merged: true,
224
- content: merger.merge,
608
+ content: merge_result.to_s,
225
609
  stats: merger.stats,
610
+ unresolved_cases: merge_result.unresolved_cases
226
611
  }
227
612
  end
228
613
 
@@ -240,12 +625,19 @@ module Markdown
240
625
  dest,
241
626
  preference: preference,
242
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]
243
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
244
635
 
245
636
  {
246
637
  merged: true,
247
- content: merger.merge,
638
+ content: merge_result.to_yaml,
248
639
  stats: merger.stats,
640
+ unresolved_cases: merge_result.unresolved_cases
249
641
  }
250
642
  end
251
643
 
@@ -263,12 +655,55 @@ module Markdown
263
655
  dest,
264
656
  preference: preference,
265
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]
266
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
267
665
 
268
666
  {
269
667
  merged: true,
270
- content: merger.merge,
668
+ content: merge_result.to_json,
271
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
+ }
272
707
  }
273
708
  end
274
709
 
@@ -286,12 +721,19 @@ module Markdown
286
721
  dest,
287
722
  preference: preference,
288
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]
289
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
290
731
 
291
732
  {
292
733
  merged: true,
293
- content: merger.merge,
734
+ content: merge_result.to_toml,
294
735
  stats: merger.stats,
736
+ unresolved_cases: merge_result.unresolved_cases
295
737
  }
296
738
  end
297
739
  end