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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "parslet"
3
+ require 'parslet'
4
4
 
5
5
  module Markdown
6
6
  module Merge
@@ -35,33 +35,33 @@ module Markdown
35
35
  rule(:spaces?) { space.repeat }
36
36
 
37
37
  # Bracket content: handles nested brackets recursively
38
- rule(:bracket_content) {
38
+ rule(:bracket_content) do
39
39
  (
40
- str("[") >> bracket_content.maybe >> str("]") |
41
- str("]").absent? >> any
40
+ str('[') >> bracket_content.maybe >> str(']') |
41
+ str(']').absent? >> any
42
42
  ).repeat
43
- }
43
+ end
44
44
 
45
- rule(:label) { str("[") >> bracket_content.as(:label) >> str("]") }
45
+ rule(:label) { str('[') >> bracket_content.as(:label) >> str(']') }
46
46
 
47
47
  rule(:url_char) { match('[^\s>]') }
48
48
  rule(:bare_url) { url_char.repeat(1) }
49
- rule(:angled_url_char) { match("[^>]") }
50
- rule(:angled_url) { str("<") >> angled_url_char.repeat(1) >> str(">") }
49
+ rule(:angled_url_char) { match('[^>]') }
50
+ rule(:angled_url) { str('<') >> angled_url_char.repeat(1) >> str('>') }
51
51
  rule(:url) { (angled_url | bare_url).as(:url) }
52
52
 
53
53
  rule(:title_content_double) { (str('"').absent? >> any).repeat }
54
54
  rule(:title_content_single) { (str("'").absent? >> any).repeat }
55
- rule(:title_content_paren) { (str(")").absent? >> any).repeat }
55
+ rule(:title_content_paren) { (str(')').absent? >> any).repeat }
56
56
 
57
57
  rule(:title_double) { str('"') >> title_content_double.as(:title) >> str('"') }
58
58
  rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
59
- rule(:title_paren) { str("(") >> title_content_paren.as(:title) >> str(")") }
59
+ rule(:title_paren) { str('(') >> title_content_paren.as(:title) >> str(')') }
60
60
  rule(:title) { title_double | title_single | title_paren }
61
61
 
62
- rule(:definition) {
63
- spaces? >> label >> str(":") >> spaces? >> url >> (spaces >> title).maybe >> spaces?
64
- }
62
+ rule(:definition) do
63
+ spaces? >> label >> str(':') >> spaces? >> url >> (spaces >> title).maybe >> spaces?
64
+ end
65
65
 
66
66
  root(:definition)
67
67
  end
@@ -72,22 +72,22 @@ module Markdown
72
72
  rule(:spaces) { space.repeat(1) }
73
73
 
74
74
  # Bracket content with recursive nesting
75
- rule(:bracket_content) {
75
+ rule(:bracket_content) do
76
76
  (
77
- str("[") >> bracket_content.maybe >> str("]") |
78
- str("]").absent? >> any
77
+ str('[') >> bracket_content.maybe >> str(']') |
78
+ str(']').absent? >> any
79
79
  ).repeat
80
- }
80
+ end
81
81
 
82
- rule(:link_text) { str("[") >> bracket_content.as(:text) >> str("]") }
82
+ rule(:link_text) { str('[') >> bracket_content.as(:text) >> str(']') }
83
83
 
84
84
  # URL content - handles balanced parens inside URLs
85
- rule(:paren_content) {
85
+ rule(:paren_content) do
86
86
  (
87
- str("(") >> paren_content.maybe >> str(")") |
87
+ str('(') >> paren_content.maybe >> str(')') |
88
88
  match('[^()\s"\']')
89
89
  ).repeat
90
- }
90
+ end
91
91
 
92
92
  rule(:url) { paren_content.as(:url) }
93
93
 
@@ -97,7 +97,7 @@ module Markdown
97
97
  rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
98
98
  rule(:title) { title_double | title_single }
99
99
 
100
- rule(:url_part) { str("(") >> url >> (spaces >> title).maybe >> str(")") }
100
+ rule(:url_part) { str('(') >> url >> (spaces >> title).maybe >> str(')') }
101
101
 
102
102
  rule(:inline_link) { link_text >> url_part }
103
103
 
@@ -109,21 +109,21 @@ module Markdown
109
109
  rule(:space) { match('[ \t]') }
110
110
  rule(:spaces) { space.repeat(1) }
111
111
 
112
- rule(:bracket_content) {
112
+ rule(:bracket_content) do
113
113
  (
114
- str("[") >> bracket_content.maybe >> str("]") |
115
- str("]").absent? >> any
114
+ str('[') >> bracket_content.maybe >> str(']') |
115
+ str(']').absent? >> any
116
116
  ).repeat
117
- }
117
+ end
118
118
 
119
- rule(:alt_text) { str("![") >> bracket_content.as(:alt) >> str("]") }
119
+ rule(:alt_text) { str('![') >> bracket_content.as(:alt) >> str(']') }
120
120
 
121
- rule(:paren_content) {
121
+ rule(:paren_content) do
122
122
  (
123
- str("(") >> paren_content.maybe >> str(")") |
123
+ str('(') >> paren_content.maybe >> str(')') |
124
124
  match('[^()\s"\']')
125
125
  ).repeat
126
- }
126
+ end
127
127
 
128
128
  rule(:url) { paren_content.as(:url) }
129
129
 
@@ -133,7 +133,7 @@ module Markdown
133
133
  rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
134
134
  rule(:title) { title_double | title_single }
135
135
 
136
- rule(:url_part) { str("(") >> url >> (spaces >> title).maybe >> str(")") }
136
+ rule(:url_part) { str('(') >> url >> (spaces >> title).maybe >> str(')') }
137
137
 
138
138
  rule(:inline_image) { alt_text >> url_part }
139
139
 
@@ -170,11 +170,11 @@ module Markdown
170
170
 
171
171
  url = result[:url].to_s
172
172
  # Strip angle brackets if present
173
- url = url[1..-2] if url.start_with?("<") && url.end_with?(">")
173
+ url = url[1..-2] if url.start_with?('<') && url.end_with?('>')
174
174
 
175
175
  definition = {
176
176
  label: result[:label].to_s,
177
- url: url,
177
+ url: url
178
178
  }
179
179
  definition[:title] = result[:title].to_s if result[:title]
180
180
  definition
@@ -239,7 +239,7 @@ module Markdown
239
239
  def build_link_tree(links, images)
240
240
  # Combine all items
241
241
  all_items = links.map { |l| l.merge(type: :link) } +
242
- images.map { |i| i.merge(type: :image) }
242
+ images.map { |i| i.merge(type: :image) }
243
243
 
244
244
  # Sort by start position
245
245
  sorted = all_items.sort_by { |item| item[:start_pos] }
@@ -297,15 +297,15 @@ module Markdown
297
297
  def find_constructs(content, type)
298
298
  results = []
299
299
  pos = 0
300
- grammar = (type == :image) ? @image_grammar : @link_grammar
301
- start_marker = (type == :image) ? "![" : "["
300
+ grammar = type == :image ? @image_grammar : @link_grammar
301
+ start_marker = type == :image ? '![' : '['
302
302
 
303
303
  while pos < content.length
304
304
  idx = content.index(start_marker, pos)
305
305
  break unless idx
306
306
 
307
307
  # For links, skip if preceded by ! (that's an image)
308
- if type == :link && idx > 0 && content[idx - 1] == "!"
308
+ if type == :link && idx > 0 && content[idx - 1] == '!'
309
309
  pos = idx + 1
310
310
  next
311
311
  end
@@ -327,12 +327,12 @@ module Markdown
327
327
  remaining = content[start_idx..]
328
328
 
329
329
  # Find the closing ) by tracking balanced brackets/parens
330
- bracket_end = find_bracket_end(remaining, (type == :image) ? 1 : 0)
330
+ bracket_end = find_bracket_end(remaining, type == :image ? 1 : 0)
331
331
  return unless bracket_end
332
332
 
333
333
  # Check for ( after ]
334
334
  return if bracket_end + 1 >= remaining.length
335
- return unless remaining[bracket_end + 1] == "("
335
+ return unless remaining[bracket_end + 1] == '('
336
336
 
337
337
  paren_end = find_paren_end(remaining, bracket_end + 1)
338
338
  return unless paren_end
@@ -344,22 +344,22 @@ module Markdown
344
344
  result = grammar.parse(substring)
345
345
 
346
346
  parsed = if type == :image
347
- {
348
- alt: result[:alt].to_s,
349
- url: result[:url].to_s,
350
- start_pos: start_idx,
351
- end_pos: start_idx + substring.length,
352
- original: substring,
353
- }
354
- else
355
- {
356
- text: result[:text].to_s,
357
- url: result[:url].to_s,
358
- start_pos: start_idx,
359
- end_pos: start_idx + substring.length,
360
- original: substring,
361
- }
362
- end
347
+ {
348
+ alt: result[:alt].to_s,
349
+ url: result[:url].to_s,
350
+ start_pos: start_idx,
351
+ end_pos: start_idx + substring.length,
352
+ original: substring
353
+ }
354
+ else
355
+ {
356
+ text: result[:text].to_s,
357
+ url: result[:url].to_s,
358
+ start_pos: start_idx,
359
+ end_pos: start_idx + substring.length,
360
+ original: substring
361
+ }
362
+ end
363
363
 
364
364
  parsed[:title] = result[:title].to_s if result[:title]
365
365
  parsed
@@ -374,9 +374,9 @@ module Markdown
374
374
 
375
375
  while pos < text.length
376
376
  case text[pos]
377
- when "["
377
+ when '['
378
378
  depth += 1
379
- when "]"
379
+ when ']'
380
380
  depth -= 1
381
381
  return pos if depth == 0
382
382
  end
@@ -395,7 +395,7 @@ module Markdown
395
395
  while pos < text.length
396
396
  char = text[pos]
397
397
 
398
- if !in_quotes && (char == '"' || char == "'")
398
+ if !in_quotes && ['"', "'"].include?(char)
399
399
  in_quotes = true
400
400
  quote_char = char
401
401
  elsif in_quotes && char == quote_char
@@ -403,9 +403,9 @@ module Markdown
403
403
  quote_char = nil
404
404
  elsif !in_quotes
405
405
  case char
406
- when "("
406
+ when '('
407
407
  depth += 1
408
- when ")"
408
+ when ')'
409
409
  depth -= 1
410
410
  return pos if depth == 0
411
411
  end
@@ -105,8 +105,8 @@ module Markdown
105
105
  result = content.dup
106
106
  replacements.sort_by { |r| -r[:start_pos] }.each do |replacement|
107
107
  result = result[0...replacement[:start_pos]] +
108
- replacement[:replacement] +
109
- result[replacement[:end_pos]..]
108
+ replacement[:replacement] +
109
+ result[replacement[:end_pos]..]
110
110
  end
111
111
 
112
112
  result
@@ -158,10 +158,10 @@ module Markdown
158
158
  else
159
159
  # Leaf node - process directly
160
160
  replacement = if item[:type] == :image
161
- process_image(item)
162
- else
163
- process_link(item)
164
- end
161
+ process_image(item)
162
+ else
163
+ process_link(item)
164
+ end
165
165
  replacements << replacement if replacement
166
166
  end
167
167
  end
@@ -190,14 +190,14 @@ module Markdown
190
190
  severity: :info,
191
191
  text: item[:text],
192
192
  url: item[:url],
193
- title: item[:title],
193
+ title: item[:title]
194
194
  )
195
195
  return
196
196
  end
197
197
 
198
198
  # Build the new link text by applying child replacements to the original text
199
199
  # Extract the original "text" part of the link (between [ and ])
200
- original_text = item[:text] || ""
200
+ original_text = item[:text] || ''
201
201
 
202
202
  # Apply child replacements to build the new text content
203
203
  # Children positions are relative to the document, so we need to adjust
@@ -223,7 +223,7 @@ module Markdown
223
223
  {
224
224
  start_pos: item[:start_pos],
225
225
  end_pos: item[:end_pos],
226
- replacement: "[#{new_text}][#{label}]",
226
+ replacement: "[#{new_text}][#{label}]"
227
227
  }
228
228
  end
229
229
 
@@ -257,7 +257,7 @@ module Markdown
257
257
  severity: :warning,
258
258
  url: url,
259
259
  labels: labels,
260
- selected_label: @url_to_label[url],
260
+ selected_label: @url_to_label[url]
261
261
  )
262
262
  end
263
263
  end
@@ -273,7 +273,7 @@ module Markdown
273
273
  severity: :info,
274
274
  text: link_text,
275
275
  url: url,
276
- title: title,
276
+ title: title
277
277
  )
278
278
  return
279
279
  end
@@ -285,7 +285,7 @@ module Markdown
285
285
  {
286
286
  start_pos: link[:start_pos],
287
287
  end_pos: link[:end_pos],
288
- replacement: "[#{link_text}][#{label}]",
288
+ replacement: "[#{link_text}][#{label}]"
289
289
  }
290
290
  end
291
291
 
@@ -300,7 +300,7 @@ module Markdown
300
300
  severity: :info,
301
301
  alt: alt_text,
302
302
  url: url,
303
- title: title,
303
+ title: title
304
304
  )
305
305
  return
306
306
  end
@@ -312,7 +312,7 @@ module Markdown
312
312
  {
313
313
  start_pos: image[:start_pos],
314
314
  end_pos: image[:end_pos],
315
- replacement: "![#{alt_text}][#{label}]",
315
+ replacement: "![#{alt_text}][#{label}]"
316
316
  }
317
317
  end
318
318
  end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ # Fuzzy matches markdown list nodes by item-token overlap so inner list merging
6
+ # can repair previously-corrupted lists that no longer share an exact signature.
7
+ class ListMatchRefiner < Ast::Merge::MatchRefinerBase
8
+ include Ast::Merge::JaccardSimilarity
9
+
10
+ DEFAULT_THRESHOLD = 0.45
11
+
12
+ def initialize(threshold: DEFAULT_THRESHOLD, **options)
13
+ super(threshold: threshold, node_types: [:list], **options)
14
+ end
15
+
16
+ def call(template_nodes, dest_nodes, context = {})
17
+ template_lists = template_nodes.select { |node| node_type(node).to_s == 'list' }
18
+ dest_lists = dest_nodes.select { |node| node_type(node).to_s == 'list' }
19
+ return [] if template_lists.empty? || dest_lists.empty?
20
+
21
+ greedy_match(template_lists, dest_lists) do |template_node, dest_node|
22
+ compute_similarity(template_node, dest_node, context)
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def compute_similarity(template_node, dest_node, context)
29
+ template_items = list_item_anchors(template_node)
30
+ dest_items = list_item_anchors(dest_node)
31
+ return 0.0 if template_items.empty? || dest_items.empty?
32
+
33
+ containment = template_item_containment(template_items, dest_items)
34
+ token_overlap = jaccard(list_tokens(template_node), list_tokens(dest_node))
35
+ first_item_score = template_items.first == dest_items.first ? 1.0 : 0.0
36
+ context_score = context_similarity(
37
+ template_node,
38
+ context[:template_analysis],
39
+ dest_node,
40
+ context[:dest_analysis]
41
+ )
42
+
43
+ (containment * 0.35) + (token_overlap * 0.35) + (context_score * 0.2) + (first_item_score * 0.1)
44
+ end
45
+
46
+ def list_item_anchors(list_node)
47
+ raw = Ast::Merge::NodeTyping.unwrap(list_node)
48
+
49
+ raw.each_with_object([]) do |child, anchors|
50
+ next unless child.respond_to?(:type) && %w[list_item item].include?(child.type.to_s)
51
+
52
+ anchors << normalize_anchor(child.text.to_s)
53
+ end
54
+ end
55
+
56
+ def list_tokens(list_node)
57
+ extract_tokens(list_node.text.to_s)
58
+ end
59
+
60
+ def template_item_containment(template_items, dest_items)
61
+ template_set = template_items.to_set
62
+ dest_set = dest_items.to_set
63
+ return 0.0 if template_set.empty?
64
+
65
+ (template_set & dest_set).size.to_f / template_set.size
66
+ end
67
+
68
+ def context_similarity(template_node, template_analysis, dest_node, dest_analysis)
69
+ template_context = preceding_context_text(template_node, template_analysis)
70
+ dest_context = preceding_context_text(dest_node, dest_analysis)
71
+ return 0.0 if template_context.empty? || dest_context.empty?
72
+
73
+ jaccard(extract_tokens(template_context), extract_tokens(dest_context))
74
+ end
75
+
76
+ def preceding_context_text(node, analysis)
77
+ return '' unless analysis
78
+
79
+ index = analysis.statements.index(node)
80
+ return '' unless index
81
+
82
+ (index - 1).downto(0) do |current_index|
83
+ candidate = analysis.statements[current_index]
84
+ signature = analysis.signature_at(current_index)
85
+ next unless signature.is_a?(Array) && %i[heading paragraph code_block].include?(signature.first)
86
+
87
+ return candidate.text.to_s
88
+ end
89
+
90
+ ''
91
+ end
92
+
93
+ def normalize_anchor(text)
94
+ text.to_s.strip.gsub(/\s+/, ' ').downcase
95
+ end
96
+ end
97
+ end
98
+ end