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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +673 -0
  5. data/lib/markdown/merge/backend_support.rb +200 -0
  6. data/lib/markdown/merge/cleanse/block_spacing.rb +248 -0
  7. data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
  8. data/lib/markdown/merge/cleanse/condensed_link_refs.rb +411 -0
  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 +44 -0
  12. data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
  13. data/lib/markdown/merge/code_block_merger.rb +742 -0
  14. data/lib/markdown/merge/comment_tracker.rb +42 -0
  15. data/lib/markdown/merge/conflict_resolver.rb +199 -0
  16. data/lib/markdown/merge/debug_logger.rb +26 -0
  17. data/lib/markdown/merge/document_problems.rb +190 -0
  18. data/lib/markdown/merge/file_aligner.rb +496 -0
  19. data/lib/markdown/merge/file_analysis.rb +689 -0
  20. data/lib/markdown/merge/file_analysis_base.rb +766 -0
  21. data/lib/markdown/merge/freeze_node.rb +93 -0
  22. data/lib/markdown/merge/gap_line_node.rb +142 -0
  23. data/lib/markdown/merge/link_definition_formatter.rb +49 -0
  24. data/lib/markdown/merge/link_definition_node.rb +157 -0
  25. data/lib/markdown/merge/link_parser.rb +421 -0
  26. data/lib/markdown/merge/link_reference_rehydrator.rb +320 -0
  27. data/lib/markdown/merge/list_match_refiner.rb +98 -0
  28. data/lib/markdown/merge/list_merger.rb +322 -0
  29. data/lib/markdown/merge/markdown_structure.rb +123 -0
  30. data/lib/markdown/merge/merge_result.rb +483 -0
  31. data/lib/markdown/merge/node_type_normalizer.rb +126 -0
  32. data/lib/markdown/merge/output_builder.rb +248 -0
  33. data/lib/markdown/merge/partial_template_merger.rb +555 -0
  34. data/lib/markdown/merge/preservation_support.rb +291 -0
  35. data/lib/markdown/merge/rspec/shared_examples/source_preserving_provider.rb +338 -0
  36. data/lib/markdown/merge/smart_merger.rb +269 -0
  37. data/lib/markdown/merge/smart_merger_base.rb +1490 -0
  38. data/lib/markdown/merge/source_preserving_provider.rb +814 -0
  39. data/lib/markdown/merge/table_match_algorithm.rb +499 -0
  40. data/lib/markdown/merge/table_match_refiner.rb +132 -0
  41. data/lib/markdown/merge/version.rb +5 -3
  42. data/lib/markdown/merge/whitespace_normalizer.rb +243 -0
  43. data/lib/markdown/merge/wrapper_support.rb +194 -0
  44. data/lib/markdown/merge.rb +271 -87
  45. data/lib/markdown-merge.rb +7 -1
  46. data/sig/markdown/merge.rbs +62 -0
  47. data.tar.gz.sig +0 -0
  48. metadata +289 -15
  49. metadata.gz.sig +0 -0
@@ -1,29 +1,134 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "tree_haver"
3
+ require 'ast/merge'
4
+ require 'tree_haver'
5
+ require_relative 'merge/version'
4
6
 
5
7
  module Markdown
6
8
  module Merge
7
- PACKAGE_NAME = "markdown-merge"
9
+ PACKAGE_NAME = 'markdown-merge'
8
10
  BACKEND_REFERENCES = {
9
- "kreuzberg-language-pack" => TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND
11
+ 'kreuzberg-language-pack' => TreeHaver::KREUZBERG_LANGUAGE_PACK_BACKEND,
12
+ 'commonmarker' => TreeHaver::BackendReference.new(id: 'commonmarker', family: 'native').freeze,
13
+ 'markly' => TreeHaver::BackendReference.new(id: 'markly', family: 'native').freeze,
14
+ 'kramdown' => TreeHaver::BackendReference.new(id: 'kramdown', family: 'native').freeze
10
15
  }.freeze
16
+ BACKEND_REGISTRY = Struct.new(:registered, :mutex).new(false, Mutex.new)
17
+
18
+ class Error < Ast::Merge::Error; end
19
+
20
+ class ParseError < Ast::Merge::ParseError
21
+ def initialize(message = nil, content: nil, errors: [])
22
+ super(message, errors: errors, content: content)
23
+ end
24
+ end
25
+
26
+ class TemplateParseError < ParseError; end
27
+
28
+ class DestinationParseError < ParseError; end
29
+
30
+ class CorruptionDetectedError < Error; end
31
+
32
+ autoload :BackendSupport, 'markdown/merge/backend_support'
33
+ autoload :Cleanse, 'markdown/merge/cleanse'
34
+ autoload :CodeBlockMatchRefiner, 'markdown/merge/code_block_match_refiner'
35
+ autoload :CodeBlockMerger, 'markdown/merge/code_block_merger'
36
+ autoload :CommentTracker, 'markdown/merge/comment_tracker'
37
+ autoload :ConflictResolver, 'markdown/merge/conflict_resolver'
38
+ autoload :DebugLogger, 'markdown/merge/debug_logger'
39
+ autoload :DocumentProblems, 'markdown/merge/document_problems'
40
+ autoload :FileAligner, 'markdown/merge/file_aligner'
41
+ autoload :FileAnalysis, 'markdown/merge/file_analysis'
42
+ autoload :FileAnalysisBase, 'markdown/merge/file_analysis_base'
43
+ autoload :FreezeNode, 'markdown/merge/freeze_node'
44
+ autoload :GapLineNode, 'markdown/merge/gap_line_node'
45
+ autoload :LinkDefinitionFormatter, 'markdown/merge/link_definition_formatter'
46
+ autoload :LinkDefinitionNode, 'markdown/merge/link_definition_node'
47
+ autoload :LinkParser, 'markdown/merge/link_parser'
48
+ autoload :LinkReferenceRehydrator, 'markdown/merge/link_reference_rehydrator'
49
+ autoload :ListMatchRefiner, 'markdown/merge/list_match_refiner'
50
+ autoload :ListMerger, 'markdown/merge/list_merger'
51
+ autoload :MarkdownStructure, 'markdown/merge/markdown_structure'
52
+ autoload :MergeResult, 'markdown/merge/merge_result'
53
+ autoload :NodeTypeNormalizer, 'markdown/merge/node_type_normalizer'
54
+ autoload :OutputBuilder, 'markdown/merge/output_builder'
55
+ autoload :PartialTemplateMerger, 'markdown/merge/partial_template_merger'
56
+ autoload :PreservationSupport, 'markdown/merge/preservation_support'
57
+ autoload :ProviderBackend, 'markdown/merge/source_preserving_provider'
58
+ autoload :SourcePreservingProvider, 'markdown/merge/source_preserving_provider'
59
+ autoload :SmartMerger, 'markdown/merge/smart_merger'
60
+ autoload :SmartMergerBase, 'markdown/merge/smart_merger_base'
61
+ autoload :TableMatchAlgorithm, 'markdown/merge/table_match_algorithm'
62
+ autoload :TableMatchRefiner, 'markdown/merge/table_match_refiner'
63
+ autoload :WhitespaceNormalizer, 'markdown/merge/whitespace_normalizer'
64
+ autoload :WrapperSupport, 'markdown/merge/wrapper_support'
65
+
66
+ # Default acyclic Markdown workflow provider backed by the language pack.
67
+ class Provider < SourcePreservingProvider
68
+ # rubocop:disable Metrics/MethodLength -- constructor declares the complete provider boundary
69
+ def initialize
70
+ super(
71
+ provider_id: 'ruby.markdown',
72
+ role: :workflow,
73
+ backend: ProviderBackend.new(
74
+ id: :'kreuzberg-language-pack',
75
+ package: PACKAGE_NAME,
76
+ dialects: %i[markdown],
77
+ parser: lambda { |source|
78
+ Markdown::Merge.register_backend!
79
+ TreeHaver.with_backend('kreuzberg-language-pack') { TreeHaver.parser_for(:markdown).parse(source) }
80
+ },
81
+ headings: ProviderBackend.method(:tree_sitter_headings)
82
+ )
83
+ )
84
+ end
85
+ # rubocop:enable Metrics/MethodLength
86
+ end
87
+
88
+ class << self
89
+ def merge_provider
90
+ @merge_provider ||= Provider.new
91
+ end
92
+
93
+ def register_provider!(replace: false)
94
+ return unless Ast::Merge.respond_to?(:register_provider)
95
+
96
+ Ast::Merge.register_provider(merge_provider, replace: replace)
97
+ end
98
+ end
99
+
100
+ def register_backend!
101
+ BACKEND_REGISTRY.mutex.synchronize do
102
+ return if BACKEND_REGISTRY.registered
103
+
104
+ TreeHaver::BackendRegistry.register(BACKEND_REFERENCES.fetch('kreuzberg-language-pack'))
105
+
106
+ grammar_finder = TreeHaver::GrammarFinder.new(:markdown)
107
+ grammar_finder.register! if grammar_finder.available?
108
+
109
+ BACKEND_REGISTRY.registered = true
110
+ end
111
+ end
11
112
 
12
113
  def markdown_feature_profile
13
114
  {
14
- family: "markdown",
15
- supported_dialects: ["markdown"],
115
+ family: 'markdown',
116
+ supported_dialects: ['markdown'],
16
117
  supported_policies: []
17
118
  }
18
119
  end
19
120
 
20
121
  def available_markdown_backends
21
- BACKEND_REFERENCES.values
122
+ BACKEND_REFERENCES.filter_map do |backend_id, reference|
123
+ reference if markdown_backend_available_for_analysis?(backend_id)
124
+ end
22
125
  end
23
126
 
24
127
  def markdown_backend_feature_profile(backend: nil)
25
128
  resolved_backend = resolve_backend(backend)
26
- return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.") unless BACKEND_REFERENCES.key?(resolved_backend)
129
+ unless BACKEND_REFERENCES.key?(resolved_backend)
130
+ return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.")
131
+ end
27
132
 
28
133
  markdown_feature_profile.merge(
29
134
  backend: resolved_backend,
@@ -46,37 +151,33 @@ module Markdown
46
151
  end
47
152
 
48
153
  def parse_markdown(source, dialect, backend: nil)
49
- return unsupported_feature_result("Unsupported Markdown dialect #{dialect}.") unless dialect == "markdown"
154
+ return unsupported_feature_result("Unsupported Markdown dialect #{dialect}.") unless dialect == 'markdown'
50
155
 
51
156
  resolved_backend = resolve_backend(backend)
52
- unless resolved_backend == "kreuzberg-language-pack"
157
+ unless BACKEND_REFERENCES.key?(resolved_backend)
53
158
  return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.")
54
159
  end
55
160
 
56
- syntax = TreeHaver.parse_with_language_pack(
57
- TreeHaver::ParserRequest.new(source: source, language: "markdown", dialect: dialect)
58
- )
59
- return { ok: false, diagnostics: syntax[:diagnostics], policies: [] } unless syntax[:ok]
161
+ register_backend!
162
+ parser = TreeHaver.with_backend(resolved_backend) { TreeHaver.parser_for(:markdown) }
163
+ tree = parser.parse(source)
164
+ collect_parse_errors(tree.root_node)
60
165
 
61
166
  normalized_source = normalize_source(source)
62
167
  {
63
168
  ok: true,
64
169
  diagnostics: [],
65
170
  analysis: {
66
- kind: "markdown",
171
+ kind: 'markdown',
67
172
  dialect: dialect,
68
173
  normalized_source: normalized_source,
69
- root_kind: "document",
174
+ root_kind: 'document',
70
175
  owners: collect_markdown_owners(normalized_source)
71
176
  },
72
177
  policies: []
73
178
  }
74
- rescue StandardError => e
75
- {
76
- ok: false,
77
- diagnostics: [{ severity: "error", category: "parse_error", message: e.message }],
78
- policies: []
79
- }
179
+ rescue TreeHaver::Error, StandardError => e
180
+ parse_failure_result(e)
80
181
  end
81
182
 
82
183
  def match_markdown_owners(template, destination)
@@ -85,8 +186,8 @@ module Markdown
85
186
 
86
187
  {
87
188
  matched: template[:owners]
88
- .filter { |owner| destination_paths[owner[:path]] }
89
- .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
189
+ .filter { |owner| destination_paths[owner[:path]] }
190
+ .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
90
191
  unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
91
192
  unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
92
193
  }
@@ -109,9 +210,9 @@ module Markdown
109
210
  )
110
211
  destination_paths = destination_sections.to_h { |section| [section[:path], true] }
111
212
  merged_sections = destination_sections.map { |section| section[:text] }.reject(&:empty?) +
112
- template_sections
113
- .reject { |section| destination_paths[section[:path]] || section[:text].empty? }
114
- .map { |section| section[:text] }
213
+ template_sections
214
+ .reject { |section| destination_paths[section[:path]] || section[:text].empty? }
215
+ .map { |section| section[:text] }
115
216
 
116
217
  {
117
218
  ok: true,
@@ -123,7 +224,7 @@ module Markdown
123
224
 
124
225
  def markdown_embedded_families(analysis)
125
226
  analysis[:owners].filter_map do |owner|
126
- next unless owner[:owner_kind] == "code_fence"
227
+ next unless owner[:owner_kind] == 'code_fence'
127
228
  next if owner[:info_string].to_s.empty?
128
229
 
129
230
  family = code_fence_family(owner[:info_string])
@@ -142,13 +243,13 @@ module Markdown
142
243
  def markdown_discovered_surfaces(analysis)
143
244
  markdown_embedded_families(analysis).map do |candidate|
144
245
  Ast::Merge.discovered_surface(
145
- surface_kind: "markdown_fenced_code_block",
246
+ surface_kind: 'markdown_fenced_code_block',
146
247
  declared_language: candidate[:language],
147
248
  effective_language: candidate[:dialect],
148
249
  address: "document[0] > fenced_code_block[#{candidate[:path]}]",
149
- parent_address: "document[0]",
150
- owner: Ast::Merge.surface_owner_ref(kind: "structural_owner", address: candidate[:path]),
151
- reconstruction_strategy: "portable_write",
250
+ parent_address: 'document[0]',
251
+ owner: Ast::Merge.surface_owner_ref(kind: 'structural_owner', address: candidate[:path]),
252
+ reconstruction_strategy: 'portable_write',
152
253
  metadata: {
153
254
  family: candidate[:family],
154
255
  dialect: candidate[:dialect],
@@ -158,13 +259,13 @@ module Markdown
158
259
  end
159
260
  end
160
261
 
161
- def markdown_delegated_child_operations(analysis, parent_operation_id: "markdown-document-0")
262
+ def markdown_delegated_child_operations(analysis, parent_operation_id: 'markdown-document-0')
162
263
  markdown_discovered_surfaces(analysis).each_with_index.map do |surface, index|
163
264
  Ast::Merge.delegated_child_operation(
164
265
  operation_id: "markdown-fence-#{index}",
165
266
  parent_operation_id: parent_operation_id,
166
- requested_strategy: "delegate_child_surface",
167
- language_chain: ["markdown", surface[:effective_language]],
267
+ requested_strategy: 'delegate_child_surface',
268
+ language_chain: ['markdown', surface[:effective_language]],
168
269
  surface: surface
169
270
  )
170
271
  end
@@ -183,24 +284,27 @@ module Markdown
183
284
 
184
285
  owner_path = operation.dig(:surface, :owner, :address)
185
286
  range = ranges[owner_path]
186
- return {
187
- ok: false,
188
- diagnostics: [{ severity: "error", category: "configuration_error", message: "missing fenced-code range for #{owner_path}" }],
189
- policies: []
190
- } if range.nil?
287
+ if range.nil?
288
+ return {
289
+ ok: false,
290
+ diagnostics: [{ severity: 'error', category: 'configuration_error',
291
+ message: "missing fenced-code range for #{owner_path}" }],
292
+ policies: []
293
+ }
294
+ end
191
295
 
192
296
  { range: range, output: output }
193
297
  end
194
298
 
195
299
  replacements.sort_by { |entry| -entry[:range][:start] }.each do |entry|
196
- body_lines = entry[:output].empty? ? [] : entry[:output].sub(/\n\z/, "").split("\n")
300
+ body_lines = entry[:output].empty? ? [] : entry[:output].sub(/\n\z/, '').split("\n")
197
301
  lines[entry[:range][:start] + 1...entry[:range][:end]] = body_lines
198
302
  end
199
303
 
200
304
  {
201
305
  ok: true,
202
306
  diagnostics: [],
203
- output: "#{lines.join("\n").sub(/\n+\z/, "")}\n",
307
+ output: "#{lines.join("\n").sub(/\n+\z/, '')}\n",
204
308
  policies: []
205
309
  }
206
310
  end
@@ -208,8 +312,8 @@ module Markdown
208
312
  def merge_markdown_with_nested_outputs(template_source, destination_source, dialect, nested_outputs, backend: nil)
209
313
  Ast::Merge.execute_nested_merge(
210
314
  nested_outputs,
211
- default_family: "markdown",
212
- request_id_prefix: "nested_markdown_child",
315
+ default_family: 'markdown',
316
+ request_id_prefix: 'nested_markdown_child',
213
317
  merge_parent: -> { merge_markdown(template_source, destination_source, dialect, backend: backend) },
214
318
  discover_operations: lambda { |merged_output|
215
319
  analysis = parse_markdown(merged_output, dialect, backend: backend)
@@ -232,10 +336,11 @@ module Markdown
232
336
  )
233
337
  end
234
338
 
235
- def merge_markdown_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state, applied_children, backend: nil)
339
+ def merge_markdown_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
340
+ applied_children, backend: nil)
236
341
  Ast::Merge.execute_reviewed_nested_merge(
237
342
  review_state,
238
- "markdown",
343
+ 'markdown',
239
344
  applied_children,
240
345
  merge_parent: -> { merge_markdown(template_source, destination_source, dialect, backend: backend) },
241
346
  discover_operations: lambda { |merged_output|
@@ -259,9 +364,13 @@ module Markdown
259
364
  )
260
365
  end
261
366
 
262
- def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect, replay_bundle, backend: nil)
263
- execution = Array(replay_bundle[:reviewed_nested_executions]).find { |entry| entry[:family] == "markdown" }
264
- return { ok: false, diagnostics: [{ severity: "error", category: "configuration_error", message: "review replay bundle does not include a reviewed nested execution for markdown." }], policies: [] } unless execution
367
+ def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
368
+ replay_bundle, backend: nil)
369
+ execution = Array(replay_bundle[:reviewed_nested_executions]).find { |entry| entry[:family] == 'markdown' }
370
+ unless execution
371
+ return { ok: false,
372
+ diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review replay bundle does not include a reviewed nested execution for markdown.' }], policies: [] }
373
+ end
265
374
 
266
375
  merge_markdown_with_reviewed_nested_outputs(
267
376
  template_source,
@@ -273,9 +382,13 @@ module Markdown
273
382
  )
274
383
  end
275
384
 
276
- def merge_markdown_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect, review_state, backend: nil)
277
- execution = Array(review_state[:reviewed_nested_executions]).find { |entry| entry[:family] == "markdown" }
278
- return { ok: false, diagnostics: [{ severity: "error", category: "configuration_error", message: "review state does not include a reviewed nested execution for markdown." }], policies: [] } unless execution
385
+ def merge_markdown_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect,
386
+ review_state, backend: nil)
387
+ execution = Array(review_state[:reviewed_nested_executions]).find { |entry| entry[:family] == 'markdown' }
388
+ unless execution
389
+ return { ok: false,
390
+ diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review state does not include a reviewed nested execution for markdown.' }], policies: [] }
391
+ end
279
392
 
280
393
  merge_markdown_with_reviewed_nested_outputs(
281
394
  template_source,
@@ -287,9 +400,13 @@ module Markdown
287
400
  )
288
401
  end
289
402
 
290
- def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source, dialect, envelope, backend: nil)
403
+ def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source,
404
+ dialect, envelope, backend: nil)
291
405
  replay_bundle, import_error = Ast::Merge.import_review_replay_bundle_envelope(envelope)
292
- return { ok: false, diagnostics: [{ severity: "error", category: import_error[:category], message: import_error[:message] }], policies: [] } if import_error
406
+ if import_error
407
+ return { ok: false,
408
+ diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
409
+ end
293
410
 
294
411
  merge_markdown_with_reviewed_nested_outputs_from_replay_bundle(
295
412
  template_source,
@@ -300,9 +417,13 @@ module Markdown
300
417
  )
301
418
  end
302
419
 
303
- def merge_markdown_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source, dialect, envelope, backend: nil)
420
+ def merge_markdown_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source,
421
+ dialect, envelope, backend: nil)
304
422
  review_state, import_error = Ast::Merge.import_conformance_manifest_review_state_envelope(envelope)
305
- return { ok: false, diagnostics: [{ severity: "error", category: import_error[:category], message: import_error[:message] }], policies: [] } if import_error
423
+ if import_error
424
+ return { ok: false,
425
+ diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
426
+ end
306
427
 
307
428
  merge_markdown_with_reviewed_nested_outputs_from_review_state(
308
429
  template_source,
@@ -319,12 +440,12 @@ module Markdown
319
440
 
320
441
  def slugify(value)
321
442
  slug = value
322
- .strip
323
- .downcase
324
- .gsub(/[`*_~\[\]()<>]/, "")
325
- .gsub(/[^a-z0-9]+/, "-")
326
- .gsub(/\A-+|-+\z/, "")
327
- slug.empty? ? "section" : slug
443
+ .strip
444
+ .downcase
445
+ .gsub(/[`*_~\[\]()<>]/, '')
446
+ .gsub(/[^a-z0-9]+/, '-')
447
+ .gsub(/\A-+|-+\z/, '')
448
+ slug.empty? ? 'section' : slug
328
449
  end
329
450
 
330
451
  def collect_markdown_owners(source)
@@ -340,7 +461,7 @@ module Markdown
340
461
  level = heading[1].length
341
462
  owners << {
342
463
  path: "/heading/#{heading_index}",
343
- owner_kind: "heading",
464
+ owner_kind: 'heading',
344
465
  match_key: "h#{level}:#{slugify(heading[2])}",
345
466
  level: level
346
467
  }
@@ -356,8 +477,8 @@ module Markdown
356
477
  info_string = fence[2].strip.split(/\s+/).first.to_s
357
478
  owners << {
358
479
  path: "/code_fence/#{code_fence_index}",
359
- owner_kind: "code_fence",
360
- match_key: "fence:#{info_string.empty? ? "plain" : info_string}",
480
+ owner_kind: 'code_fence',
481
+ match_key: "fence:#{info_string.empty? ? 'plain' : info_string}",
361
482
  **(info_string.empty? ? {} : { info_string: info_string })
362
483
  }
363
484
  code_fence_index += 1
@@ -366,8 +487,8 @@ module Markdown
366
487
  while index < lines.length
367
488
  trimmed = lines[index].strip
368
489
  break if trimmed.length >= marker_length &&
369
- trimmed.start_with?(marker_char * marker_length) &&
370
- trimmed.delete(marker_char).empty?
490
+ trimmed.start_with?(marker_char * marker_length) &&
491
+ trimmed.delete(marker_char).empty?
371
492
 
372
493
  index += 1
373
494
  end
@@ -407,8 +528,8 @@ module Markdown
407
528
  while index < lines.length
408
529
  trimmed = lines[index].strip
409
530
  break if trimmed.length >= marker_length &&
410
- trimmed.start_with?(marker_char * marker_length) &&
411
- trimmed.delete(marker_char).empty?
531
+ trimmed.start_with?(marker_char * marker_length) &&
532
+ trimmed.delete(marker_char).empty?
412
533
 
413
534
  index += 1
414
535
  end
@@ -458,8 +579,8 @@ module Markdown
458
579
  while cursor < lines.length
459
580
  trimmed = lines[cursor].strip
460
581
  if trimmed.length >= marker_length &&
461
- trimmed.start_with?(marker_char * marker_length) &&
462
- trimmed.delete(marker_char).empty?
582
+ trimmed.start_with?(marker_char * marker_length) &&
583
+ trimmed.delete(marker_char).empty?
463
584
  closing_index = cursor
464
585
  break
465
586
  end
@@ -481,43 +602,81 @@ module Markdown
481
602
 
482
603
  def code_fence_family(info_string)
483
604
  case info_string.to_s.downcase
484
- when "ts", "typescript"
485
- "typescript"
486
- when "rust", "rs"
487
- "rust"
488
- when "go"
489
- "go"
490
- when "json", "jsonc"
491
- "json"
492
- when "yaml", "yml"
493
- "yaml"
494
- when "toml"
495
- "toml"
605
+ when 'ts', 'typescript'
606
+ 'typescript'
607
+ when 'rust', 'rs'
608
+ 'rust'
609
+ when 'go'
610
+ 'go'
611
+ when 'json', 'jsonc', 'json5'
612
+ 'json'
613
+ when 'yaml', 'yml'
614
+ 'yaml'
615
+ when 'toml'
616
+ 'toml'
496
617
  end
497
618
  end
498
619
 
499
620
  def code_fence_dialect(info_string, family)
500
621
  case family
501
- when "typescript", "rust", "go", "yaml", "toml"
622
+ when 'typescript', 'rust', 'go', 'yaml', 'toml'
502
623
  family
503
- when "json"
504
- info_string.to_s.downcase == "jsonc" ? "jsonc" : "json"
624
+ when 'json'
625
+ %w[json jsonc json5].include?(info_string.to_s.downcase) ? info_string.to_s.downcase : 'json'
505
626
  end
506
627
  end
507
628
 
508
629
  def resolve_backend(backend)
509
- backend.to_s.empty? ? "kreuzberg-language-pack" : backend.to_s
630
+ return backend.to_s unless backend.to_s.empty?
631
+
632
+ current = TreeHaver.current_backend_id
633
+ return current if BACKEND_REFERENCES.key?(current.to_s) && markdown_backend_available_for_analysis?(current)
634
+
635
+ BACKEND_REFERENCES.keys.find { |backend_id| markdown_backend_available_for_analysis?(backend_id) } ||
636
+ 'kreuzberg-language-pack'
637
+ end
638
+
639
+ def markdown_backend_available_for_analysis?(backend_id)
640
+ register_backend!
641
+ registrations = TreeHaver.registered_languages(:markdown)
642
+ case backend_id.to_s
643
+ when 'commonmarker', 'markly', 'kramdown'
644
+ registrations.dig(backend_id.to_sym, :backend_module)&.then do |backend_module|
645
+ !backend_module.respond_to?(:available?) || backend_module.available?
646
+ end
647
+ when 'kreuzberg-language-pack'
648
+ registrations.key?(:tree_sitter) || registrations.key?(:tslp)
649
+ else
650
+ false
651
+ end
652
+ end
653
+
654
+ def collect_parse_errors(node)
655
+ raise TreeHaver::NotAvailable, 'Markdown parse returned no root node' unless node
656
+ return unless node.respond_to?(:has_error?) && node.has_error?
657
+
658
+ raise TreeHaver::NotAvailable,
659
+ 'Markdown parse contains syntax errors'
660
+ end
661
+
662
+ def parse_failure_result(error)
663
+ {
664
+ ok: false,
665
+ diagnostics: [{ severity: 'error', category: 'parse_error', message: error.message }],
666
+ policies: []
667
+ }
510
668
  end
511
669
 
512
670
  def unsupported_feature_result(message)
513
671
  {
514
672
  ok: false,
515
- diagnostics: [{ severity: "error", category: "unsupported_feature", message: message }],
673
+ diagnostics: [{ severity: 'error', category: 'unsupported_feature', message: message }],
516
674
  policies: []
517
675
  }
518
676
  end
519
677
 
520
678
  module_function(
679
+ :register_backend!,
521
680
  :markdown_feature_profile,
522
681
  :available_markdown_backends,
523
682
  :markdown_backend_feature_profile,
@@ -544,7 +703,32 @@ module Markdown
544
703
  :code_fence_family,
545
704
  :code_fence_dialect,
546
705
  :resolve_backend,
706
+ :markdown_backend_available_for_analysis?,
707
+ :collect_parse_errors,
708
+ :parse_failure_result,
547
709
  :unsupported_feature_result
548
710
  )
549
711
  end
550
712
  end
713
+
714
+ Markdown::Merge.register_provider!
715
+
716
+ %w[
717
+ commonmarker/merge/backend
718
+ markly/merge/backend
719
+ ].each do |feature|
720
+ require feature
721
+ rescue LoadError
722
+ nil
723
+ end
724
+
725
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
726
+ Ast::Merge::RSpec::MergeGemRegistry.register(
727
+ :markdown_merge,
728
+ require_path: 'markdown/merge',
729
+ merger_class: 'Markdown::Merge::SmartMerger',
730
+ test_source: "# Test\n\nParagraph",
731
+ category: :markdown,
732
+ skip_instantiation: true
733
+ )
734
+ end
@@ -1,3 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "markdown/merge"
3
+ require_relative 'markdown/merge'
4
+ require 'version_gem'
5
+ require_relative 'markdown/merge/version'
6
+
7
+ Markdown::Merge::Version.class_eval do
8
+ extend VersionGem::Basic
9
+ end
@@ -0,0 +1,62 @@
1
+ module Markdown
2
+ module Merge
3
+ module Version
4
+ VERSION: String
5
+ end
6
+ VERSION: String
7
+
8
+ class ProviderBackend
9
+ attr_reader id: Symbol
10
+ attr_reader package: String
11
+ attr_reader dialects: Array[Symbol]
12
+
13
+ def initialize: (
14
+ id: Symbol | String,
15
+ package: String,
16
+ dialects: Array[Symbol | String],
17
+ parser: Proc,
18
+ headings: Proc
19
+ ) -> void
20
+ def parse: (String source) -> [untyped, Array[ProviderBackend::Heading]]
21
+
22
+ def self.native_headings: (untyped root, String source, ?kramdown: bool) -> Array[ProviderBackend::Heading]
23
+ def self.tree_sitter_headings: (untyped root, String source) -> Array[ProviderBackend::Heading]
24
+
25
+ class Heading
26
+ attr_reader level: Integer
27
+ attr_reader text: String
28
+ attr_reader start_line: Integer
29
+ attr_reader end_line: Integer
30
+ attr_reader style: Symbol
31
+
32
+ def initialize: (
33
+ level: Integer,
34
+ text: String,
35
+ start_line: Integer,
36
+ end_line: Integer,
37
+ style: Symbol
38
+ ) -> void
39
+ end
40
+ end
41
+
42
+ class SourcePreservingProvider
43
+ attr_reader backend: ProviderBackend
44
+
45
+ def initialize: (provider_id: String, role: Symbol, backend: ProviderBackend) -> void
46
+ def provider_id: () -> String
47
+ def family: () -> String
48
+ def capabilities: () -> Hash[Symbol, untyped]
49
+ def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
50
+ def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
51
+ def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
52
+ def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
53
+ end
54
+
55
+ class Provider < SourcePreservingProvider
56
+ def initialize: () -> void
57
+ end
58
+
59
+ def self.merge_provider: () -> Provider
60
+ def self.register_provider!: (?replace: bool) -> untyped
61
+ end
62
+ end
data.tar.gz.sig CHANGED
Binary file