markdown-merge 7.0.0 → 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 (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/LICENSE.md +13 -0
  4. data/README.md +681 -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/smart_merger.rb +269 -0
  36. data/lib/markdown/merge/smart_merger_base.rb +1490 -0
  37. data/lib/markdown/merge/table_match_algorithm.rb +499 -0
  38. data/lib/markdown/merge/table_match_refiner.rb +132 -0
  39. data/lib/markdown/merge/version.rb +5 -3
  40. data/lib/markdown/merge/whitespace_normalizer.rb +243 -0
  41. data/lib/markdown/merge/wrapper_support.rb +194 -0
  42. data/lib/markdown/merge.rb +233 -87
  43. data/lib/markdown-merge.rb +7 -1
  44. data/sig/markdown/merge.rbs +8 -0
  45. data.tar.gz.sig +0 -0
  46. metadata +287 -15
  47. metadata.gz.sig +0 -0
@@ -1,29 +1,98 @@
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 :SmartMerger, 'markdown/merge/smart_merger'
58
+ autoload :SmartMergerBase, 'markdown/merge/smart_merger_base'
59
+ autoload :TableMatchAlgorithm, 'markdown/merge/table_match_algorithm'
60
+ autoload :TableMatchRefiner, 'markdown/merge/table_match_refiner'
61
+ autoload :WhitespaceNormalizer, 'markdown/merge/whitespace_normalizer'
62
+ autoload :WrapperSupport, 'markdown/merge/wrapper_support'
63
+
64
+ def register_backend!
65
+ BACKEND_REGISTRY.mutex.synchronize do
66
+ return if BACKEND_REGISTRY.registered
67
+
68
+ TreeHaver::BackendRegistry.register(BACKEND_REFERENCES.fetch('kreuzberg-language-pack'))
69
+
70
+ grammar_finder = TreeHaver::GrammarFinder.new(:markdown)
71
+ grammar_finder.register! if grammar_finder.available?
72
+
73
+ BACKEND_REGISTRY.registered = true
74
+ end
75
+ end
11
76
 
12
77
  def markdown_feature_profile
13
78
  {
14
- family: "markdown",
15
- supported_dialects: ["markdown"],
79
+ family: 'markdown',
80
+ supported_dialects: ['markdown'],
16
81
  supported_policies: []
17
82
  }
18
83
  end
19
84
 
20
85
  def available_markdown_backends
21
- BACKEND_REFERENCES.values
86
+ BACKEND_REFERENCES.filter_map do |backend_id, reference|
87
+ reference if markdown_backend_available_for_analysis?(backend_id)
88
+ end
22
89
  end
23
90
 
24
91
  def markdown_backend_feature_profile(backend: nil)
25
92
  resolved_backend = resolve_backend(backend)
26
- return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.") unless BACKEND_REFERENCES.key?(resolved_backend)
93
+ unless BACKEND_REFERENCES.key?(resolved_backend)
94
+ return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.")
95
+ end
27
96
 
28
97
  markdown_feature_profile.merge(
29
98
  backend: resolved_backend,
@@ -46,37 +115,33 @@ module Markdown
46
115
  end
47
116
 
48
117
  def parse_markdown(source, dialect, backend: nil)
49
- return unsupported_feature_result("Unsupported Markdown dialect #{dialect}.") unless dialect == "markdown"
118
+ return unsupported_feature_result("Unsupported Markdown dialect #{dialect}.") unless dialect == 'markdown'
50
119
 
51
120
  resolved_backend = resolve_backend(backend)
52
- unless resolved_backend == "kreuzberg-language-pack"
121
+ unless BACKEND_REFERENCES.key?(resolved_backend)
53
122
  return unsupported_feature_result("Unsupported Markdown backend #{resolved_backend}.")
54
123
  end
55
124
 
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]
125
+ register_backend!
126
+ parser = TreeHaver.with_backend(resolved_backend) { TreeHaver.parser_for(:markdown) }
127
+ tree = parser.parse(source)
128
+ collect_parse_errors(tree.root_node)
60
129
 
61
130
  normalized_source = normalize_source(source)
62
131
  {
63
132
  ok: true,
64
133
  diagnostics: [],
65
134
  analysis: {
66
- kind: "markdown",
135
+ kind: 'markdown',
67
136
  dialect: dialect,
68
137
  normalized_source: normalized_source,
69
- root_kind: "document",
138
+ root_kind: 'document',
70
139
  owners: collect_markdown_owners(normalized_source)
71
140
  },
72
141
  policies: []
73
142
  }
74
- rescue StandardError => e
75
- {
76
- ok: false,
77
- diagnostics: [{ severity: "error", category: "parse_error", message: e.message }],
78
- policies: []
79
- }
143
+ rescue TreeHaver::Error, StandardError => e
144
+ parse_failure_result(e)
80
145
  end
81
146
 
82
147
  def match_markdown_owners(template, destination)
@@ -85,8 +150,8 @@ module Markdown
85
150
 
86
151
  {
87
152
  matched: template[:owners]
88
- .filter { |owner| destination_paths[owner[:path]] }
89
- .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
153
+ .filter { |owner| destination_paths[owner[:path]] }
154
+ .map { |owner| { template_path: owner[:path], destination_path: owner[:path] } },
90
155
  unmatched_template: template[:owners].map { |owner| owner[:path] }.reject { |path| destination_paths[path] },
91
156
  unmatched_destination: destination[:owners].map { |owner| owner[:path] }.reject { |path| template_paths[path] }
92
157
  }
@@ -109,9 +174,9 @@ module Markdown
109
174
  )
110
175
  destination_paths = destination_sections.to_h { |section| [section[:path], true] }
111
176
  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] }
177
+ template_sections
178
+ .reject { |section| destination_paths[section[:path]] || section[:text].empty? }
179
+ .map { |section| section[:text] }
115
180
 
116
181
  {
117
182
  ok: true,
@@ -123,7 +188,7 @@ module Markdown
123
188
 
124
189
  def markdown_embedded_families(analysis)
125
190
  analysis[:owners].filter_map do |owner|
126
- next unless owner[:owner_kind] == "code_fence"
191
+ next unless owner[:owner_kind] == 'code_fence'
127
192
  next if owner[:info_string].to_s.empty?
128
193
 
129
194
  family = code_fence_family(owner[:info_string])
@@ -142,13 +207,13 @@ module Markdown
142
207
  def markdown_discovered_surfaces(analysis)
143
208
  markdown_embedded_families(analysis).map do |candidate|
144
209
  Ast::Merge.discovered_surface(
145
- surface_kind: "markdown_fenced_code_block",
210
+ surface_kind: 'markdown_fenced_code_block',
146
211
  declared_language: candidate[:language],
147
212
  effective_language: candidate[:dialect],
148
213
  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",
214
+ parent_address: 'document[0]',
215
+ owner: Ast::Merge.surface_owner_ref(kind: 'structural_owner', address: candidate[:path]),
216
+ reconstruction_strategy: 'portable_write',
152
217
  metadata: {
153
218
  family: candidate[:family],
154
219
  dialect: candidate[:dialect],
@@ -158,13 +223,13 @@ module Markdown
158
223
  end
159
224
  end
160
225
 
161
- def markdown_delegated_child_operations(analysis, parent_operation_id: "markdown-document-0")
226
+ def markdown_delegated_child_operations(analysis, parent_operation_id: 'markdown-document-0')
162
227
  markdown_discovered_surfaces(analysis).each_with_index.map do |surface, index|
163
228
  Ast::Merge.delegated_child_operation(
164
229
  operation_id: "markdown-fence-#{index}",
165
230
  parent_operation_id: parent_operation_id,
166
- requested_strategy: "delegate_child_surface",
167
- language_chain: ["markdown", surface[:effective_language]],
231
+ requested_strategy: 'delegate_child_surface',
232
+ language_chain: ['markdown', surface[:effective_language]],
168
233
  surface: surface
169
234
  )
170
235
  end
@@ -183,24 +248,27 @@ module Markdown
183
248
 
184
249
  owner_path = operation.dig(:surface, :owner, :address)
185
250
  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?
251
+ if range.nil?
252
+ return {
253
+ ok: false,
254
+ diagnostics: [{ severity: 'error', category: 'configuration_error',
255
+ message: "missing fenced-code range for #{owner_path}" }],
256
+ policies: []
257
+ }
258
+ end
191
259
 
192
260
  { range: range, output: output }
193
261
  end
194
262
 
195
263
  replacements.sort_by { |entry| -entry[:range][:start] }.each do |entry|
196
- body_lines = entry[:output].empty? ? [] : entry[:output].sub(/\n\z/, "").split("\n")
264
+ body_lines = entry[:output].empty? ? [] : entry[:output].sub(/\n\z/, '').split("\n")
197
265
  lines[entry[:range][:start] + 1...entry[:range][:end]] = body_lines
198
266
  end
199
267
 
200
268
  {
201
269
  ok: true,
202
270
  diagnostics: [],
203
- output: "#{lines.join("\n").sub(/\n+\z/, "")}\n",
271
+ output: "#{lines.join("\n").sub(/\n+\z/, '')}\n",
204
272
  policies: []
205
273
  }
206
274
  end
@@ -208,8 +276,8 @@ module Markdown
208
276
  def merge_markdown_with_nested_outputs(template_source, destination_source, dialect, nested_outputs, backend: nil)
209
277
  Ast::Merge.execute_nested_merge(
210
278
  nested_outputs,
211
- default_family: "markdown",
212
- request_id_prefix: "nested_markdown_child",
279
+ default_family: 'markdown',
280
+ request_id_prefix: 'nested_markdown_child',
213
281
  merge_parent: -> { merge_markdown(template_source, destination_source, dialect, backend: backend) },
214
282
  discover_operations: lambda { |merged_output|
215
283
  analysis = parse_markdown(merged_output, dialect, backend: backend)
@@ -232,10 +300,11 @@ module Markdown
232
300
  )
233
301
  end
234
302
 
235
- def merge_markdown_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state, applied_children, backend: nil)
303
+ def merge_markdown_with_reviewed_nested_outputs(template_source, destination_source, dialect, review_state,
304
+ applied_children, backend: nil)
236
305
  Ast::Merge.execute_reviewed_nested_merge(
237
306
  review_state,
238
- "markdown",
307
+ 'markdown',
239
308
  applied_children,
240
309
  merge_parent: -> { merge_markdown(template_source, destination_source, dialect, backend: backend) },
241
310
  discover_operations: lambda { |merged_output|
@@ -259,9 +328,13 @@ module Markdown
259
328
  )
260
329
  end
261
330
 
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
331
+ def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle(template_source, destination_source, dialect,
332
+ replay_bundle, backend: nil)
333
+ execution = Array(replay_bundle[:reviewed_nested_executions]).find { |entry| entry[:family] == 'markdown' }
334
+ unless execution
335
+ return { ok: false,
336
+ diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review replay bundle does not include a reviewed nested execution for markdown.' }], policies: [] }
337
+ end
265
338
 
266
339
  merge_markdown_with_reviewed_nested_outputs(
267
340
  template_source,
@@ -273,9 +346,13 @@ module Markdown
273
346
  )
274
347
  end
275
348
 
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
349
+ def merge_markdown_with_reviewed_nested_outputs_from_review_state(template_source, destination_source, dialect,
350
+ review_state, backend: nil)
351
+ execution = Array(review_state[:reviewed_nested_executions]).find { |entry| entry[:family] == 'markdown' }
352
+ unless execution
353
+ return { ok: false,
354
+ diagnostics: [{ severity: 'error', category: 'configuration_error', message: 'review state does not include a reviewed nested execution for markdown.' }], policies: [] }
355
+ end
279
356
 
280
357
  merge_markdown_with_reviewed_nested_outputs(
281
358
  template_source,
@@ -287,9 +364,13 @@ module Markdown
287
364
  )
288
365
  end
289
366
 
290
- def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source, dialect, envelope, backend: nil)
367
+ def merge_markdown_with_reviewed_nested_outputs_from_replay_bundle_envelope(template_source, destination_source,
368
+ dialect, envelope, backend: nil)
291
369
  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
370
+ if import_error
371
+ return { ok: false,
372
+ diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
373
+ end
293
374
 
294
375
  merge_markdown_with_reviewed_nested_outputs_from_replay_bundle(
295
376
  template_source,
@@ -300,9 +381,13 @@ module Markdown
300
381
  )
301
382
  end
302
383
 
303
- def merge_markdown_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source, dialect, envelope, backend: nil)
384
+ def merge_markdown_with_reviewed_nested_outputs_from_review_state_envelope(template_source, destination_source,
385
+ dialect, envelope, backend: nil)
304
386
  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
387
+ if import_error
388
+ return { ok: false,
389
+ diagnostics: [{ severity: 'error', category: import_error[:category], message: import_error[:message] }], policies: [] }
390
+ end
306
391
 
307
392
  merge_markdown_with_reviewed_nested_outputs_from_review_state(
308
393
  template_source,
@@ -319,12 +404,12 @@ module Markdown
319
404
 
320
405
  def slugify(value)
321
406
  slug = value
322
- .strip
323
- .downcase
324
- .gsub(/[`*_~\[\]()<>]/, "")
325
- .gsub(/[^a-z0-9]+/, "-")
326
- .gsub(/\A-+|-+\z/, "")
327
- slug.empty? ? "section" : slug
407
+ .strip
408
+ .downcase
409
+ .gsub(/[`*_~\[\]()<>]/, '')
410
+ .gsub(/[^a-z0-9]+/, '-')
411
+ .gsub(/\A-+|-+\z/, '')
412
+ slug.empty? ? 'section' : slug
328
413
  end
329
414
 
330
415
  def collect_markdown_owners(source)
@@ -340,7 +425,7 @@ module Markdown
340
425
  level = heading[1].length
341
426
  owners << {
342
427
  path: "/heading/#{heading_index}",
343
- owner_kind: "heading",
428
+ owner_kind: 'heading',
344
429
  match_key: "h#{level}:#{slugify(heading[2])}",
345
430
  level: level
346
431
  }
@@ -356,8 +441,8 @@ module Markdown
356
441
  info_string = fence[2].strip.split(/\s+/).first.to_s
357
442
  owners << {
358
443
  path: "/code_fence/#{code_fence_index}",
359
- owner_kind: "code_fence",
360
- match_key: "fence:#{info_string.empty? ? "plain" : info_string}",
444
+ owner_kind: 'code_fence',
445
+ match_key: "fence:#{info_string.empty? ? 'plain' : info_string}",
361
446
  **(info_string.empty? ? {} : { info_string: info_string })
362
447
  }
363
448
  code_fence_index += 1
@@ -366,8 +451,8 @@ module Markdown
366
451
  while index < lines.length
367
452
  trimmed = lines[index].strip
368
453
  break if trimmed.length >= marker_length &&
369
- trimmed.start_with?(marker_char * marker_length) &&
370
- trimmed.delete(marker_char).empty?
454
+ trimmed.start_with?(marker_char * marker_length) &&
455
+ trimmed.delete(marker_char).empty?
371
456
 
372
457
  index += 1
373
458
  end
@@ -407,8 +492,8 @@ module Markdown
407
492
  while index < lines.length
408
493
  trimmed = lines[index].strip
409
494
  break if trimmed.length >= marker_length &&
410
- trimmed.start_with?(marker_char * marker_length) &&
411
- trimmed.delete(marker_char).empty?
495
+ trimmed.start_with?(marker_char * marker_length) &&
496
+ trimmed.delete(marker_char).empty?
412
497
 
413
498
  index += 1
414
499
  end
@@ -458,8 +543,8 @@ module Markdown
458
543
  while cursor < lines.length
459
544
  trimmed = lines[cursor].strip
460
545
  if trimmed.length >= marker_length &&
461
- trimmed.start_with?(marker_char * marker_length) &&
462
- trimmed.delete(marker_char).empty?
546
+ trimmed.start_with?(marker_char * marker_length) &&
547
+ trimmed.delete(marker_char).empty?
463
548
  closing_index = cursor
464
549
  break
465
550
  end
@@ -481,43 +566,81 @@ module Markdown
481
566
 
482
567
  def code_fence_family(info_string)
483
568
  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"
569
+ when 'ts', 'typescript'
570
+ 'typescript'
571
+ when 'rust', 'rs'
572
+ 'rust'
573
+ when 'go'
574
+ 'go'
575
+ when 'json', 'jsonc', 'json5'
576
+ 'json'
577
+ when 'yaml', 'yml'
578
+ 'yaml'
579
+ when 'toml'
580
+ 'toml'
496
581
  end
497
582
  end
498
583
 
499
584
  def code_fence_dialect(info_string, family)
500
585
  case family
501
- when "typescript", "rust", "go", "yaml", "toml"
586
+ when 'typescript', 'rust', 'go', 'yaml', 'toml'
502
587
  family
503
- when "json"
504
- info_string.to_s.downcase == "jsonc" ? "jsonc" : "json"
588
+ when 'json'
589
+ %w[json jsonc json5].include?(info_string.to_s.downcase) ? info_string.to_s.downcase : 'json'
505
590
  end
506
591
  end
507
592
 
508
593
  def resolve_backend(backend)
509
- backend.to_s.empty? ? "kreuzberg-language-pack" : backend.to_s
594
+ return backend.to_s unless backend.to_s.empty?
595
+
596
+ current = TreeHaver.current_backend_id
597
+ return current if BACKEND_REFERENCES.key?(current.to_s) && markdown_backend_available_for_analysis?(current)
598
+
599
+ BACKEND_REFERENCES.keys.find { |backend_id| markdown_backend_available_for_analysis?(backend_id) } ||
600
+ 'kreuzberg-language-pack'
601
+ end
602
+
603
+ def markdown_backend_available_for_analysis?(backend_id)
604
+ register_backend!
605
+ registrations = TreeHaver.registered_languages(:markdown)
606
+ case backend_id.to_s
607
+ when 'commonmarker', 'markly', 'kramdown'
608
+ registrations.dig(backend_id.to_sym, :backend_module)&.then do |backend_module|
609
+ !backend_module.respond_to?(:available?) || backend_module.available?
610
+ end
611
+ when 'kreuzberg-language-pack'
612
+ registrations.key?(:tree_sitter) || registrations.key?(:tslp)
613
+ else
614
+ false
615
+ end
616
+ end
617
+
618
+ def collect_parse_errors(node)
619
+ raise TreeHaver::NotAvailable, 'Markdown parse returned no root node' unless node
620
+ return unless node.respond_to?(:has_error?) && node.has_error?
621
+
622
+ raise TreeHaver::NotAvailable,
623
+ 'Markdown parse contains syntax errors'
624
+ end
625
+
626
+ def parse_failure_result(error)
627
+ {
628
+ ok: false,
629
+ diagnostics: [{ severity: 'error', category: 'parse_error', message: error.message }],
630
+ policies: []
631
+ }
510
632
  end
511
633
 
512
634
  def unsupported_feature_result(message)
513
635
  {
514
636
  ok: false,
515
- diagnostics: [{ severity: "error", category: "unsupported_feature", message: message }],
637
+ diagnostics: [{ severity: 'error', category: 'unsupported_feature', message: message }],
516
638
  policies: []
517
639
  }
518
640
  end
519
641
 
520
642
  module_function(
643
+ :register_backend!,
521
644
  :markdown_feature_profile,
522
645
  :available_markdown_backends,
523
646
  :markdown_backend_feature_profile,
@@ -544,7 +667,30 @@ module Markdown
544
667
  :code_fence_family,
545
668
  :code_fence_dialect,
546
669
  :resolve_backend,
670
+ :markdown_backend_available_for_analysis?,
671
+ :collect_parse_errors,
672
+ :parse_failure_result,
547
673
  :unsupported_feature_result
548
674
  )
549
675
  end
550
676
  end
677
+
678
+ %w[
679
+ commonmarker/merge/backend
680
+ markly/merge/backend
681
+ ].each do |feature|
682
+ require feature
683
+ rescue LoadError
684
+ nil
685
+ end
686
+
687
+ if defined?(Ast::Merge::RSpec::MergeGemRegistry)
688
+ Ast::Merge::RSpec::MergeGemRegistry.register(
689
+ :markdown_merge,
690
+ require_path: 'markdown/merge',
691
+ merger_class: 'Markdown::Merge::SmartMerger',
692
+ test_source: "# Test\n\nParagraph",
693
+ category: :markdown,
694
+ skip_instantiation: true
695
+ )
696
+ 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,8 @@
1
+ module Markdown
2
+ module Merge
3
+ module Version
4
+ VERSION: String
5
+ end
6
+ VERSION: String
7
+ end
8
+ end
data.tar.gz.sig CHANGED
Binary file