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
@@ -35,7 +35,7 @@ module Markdown
35
35
  first_column: 0.20, # (B) First column matching
36
36
  row_content: 0.25, # (C) Content in matching rows
37
37
  total_cells: 0.15, # (D) Overall cell matching
38
- position: 0.15, # (E) Position distance
38
+ position: 0.15 # (E) Position distance
39
39
  }.freeze
40
40
 
41
41
  # Minimum similarity threshold to consider cells as potentially matching
@@ -68,7 +68,8 @@ module Markdown
68
68
  # @param total_tables_b [Integer] Total tables in second document (default: 1)
69
69
  # @param weights [Hash] Custom weights for scoring factors
70
70
  # @param backend [Symbol] Markdown backend for type normalization (default: :commonmarker)
71
- def initialize(position_a: nil, position_b: nil, total_tables_a: 1, total_tables_b: 1, weights: {}, backend: :commonmarker)
71
+ def initialize(position_a: nil, position_b: nil, total_tables_a: 1, total_tables_b: 1, weights: {},
72
+ backend: :commonmarker)
72
73
  @position_a = position_a
73
74
  @position_b = position_b
74
75
  @total_tables_a = [total_tables_a, 1].max
@@ -93,7 +94,7 @@ module Markdown
93
94
  first_column: compute_first_column_match(rows_a, rows_b),
94
95
  row_content: compute_row_content_match(rows_a, rows_b),
95
96
  total_cells: compute_total_cells_match(rows_a, rows_b),
96
- position: compute_position_score,
97
+ position: compute_position_score
97
98
  }
98
99
 
99
100
  weighted_average(scores)
@@ -113,9 +114,7 @@ module Markdown
113
114
  return str_a.length if str_b.empty?
114
115
 
115
116
  # Ensure str_a is the shorter string for space optimization
116
- if str_a.length > str_b.length
117
- str_a, str_b = str_b, str_a
118
- end
117
+ str_a, str_b = str_b, str_a if str_a.length > str_b.length
119
118
 
120
119
  m = str_a.length
121
120
  n = str_b.length
@@ -128,11 +127,11 @@ module Markdown
128
127
  curr_row[0] = j
129
128
 
130
129
  (1..m).each do |i|
131
- cost = (str_a[i - 1] == str_b[j - 1]) ? 0 : 1
130
+ cost = str_a[i - 1] == str_b[j - 1] ? 0 : 1
132
131
  curr_row[i] = [
133
132
  curr_row[i - 1] + 1, # insertion
134
133
  prev_row[i] + 1, # deletion
135
- prev_row[i - 1] + cost, # substitution
134
+ prev_row[i - 1] + cost # substitution
136
135
  ].min
137
136
  end
138
137
 
@@ -171,9 +170,7 @@ module Markdown
171
170
  rows = []
172
171
  child = table.first_child
173
172
  while child
174
- if table_row_type?(child)
175
- rows << extract_cells(child)
176
- end
173
+ rows << extract_cells(child) if table_row_type?(child)
177
174
  child = next_sibling(child)
178
175
  end
179
176
  rows
@@ -196,7 +193,7 @@ module Markdown
196
193
 
197
194
  # Normalize the type using NodeTypeNormalizer for backend portability
198
195
  canonical = NodeTypeNormalizer.canonical_type(node.type, @backend || :commonmarker)
199
- canonical == :table_row || canonical == :table_header
196
+ %i[table_row table_header].include?(canonical)
200
197
  end
201
198
 
202
199
  # Get the next sibling of a node.
@@ -231,9 +228,7 @@ module Markdown
231
228
  while child
232
229
  if child.respond_to?(:type)
233
230
  canonical = NodeTypeNormalizer.canonical_type(child.type, @backend || :commonmarker)
234
- if canonical == :table_cell
235
- cells << extract_text_content(child)
236
- end
231
+ cells << extract_text_content(child) if canonical == :table_cell
237
232
  end
238
233
  child = next_sibling(child)
239
234
  end
@@ -271,14 +266,14 @@ module Markdown
271
266
  canonical_type = NodeTypeNormalizer.canonical_type(node.type, @backend || :commonmarker)
272
267
 
273
268
  # Collect text from text and code nodes
274
- if canonical_type == :text || canonical_type == :code
269
+ if %i[text code].include?(canonical_type)
275
270
  content = if node.respond_to?(:string_content)
276
- node.string_content.to_s
277
- elsif node.respond_to?(:text)
278
- node.text.to_s
279
- else
280
- ""
281
- end
271
+ node.string_content.to_s
272
+ elsif node.respond_to?(:text)
273
+ node.text.to_s
274
+ else
275
+ ''
276
+ end
282
277
  text_parts << content unless content.empty?
283
278
  end
284
279
 
@@ -292,10 +287,10 @@ module Markdown
292
287
  while child
293
288
  collect_text_recursive(child, text_parts)
294
289
  child = if child.respond_to?(:next_sibling)
295
- child.next_sibling
296
- else
297
- (child.respond_to?(:next) ? child.next : nil)
298
- end
290
+ child.next_sibling
291
+ else
292
+ (child.respond_to?(:next) ? child.next : nil)
293
+ end
299
294
  end
300
295
  end
301
296
  end
@@ -354,7 +349,7 @@ module Markdown
354
349
 
355
350
  # Average over total cells
356
351
  total_cells = col_a.size + col_b.size
357
- (total_cells > 0) ? total_similarity / total_cells : 0.0
352
+ total_cells > 0 ? total_similarity / total_cells : 0.0
358
353
  end
359
354
 
360
355
  # (C) Compute average match percentage for rows with matching first column.
@@ -53,7 +53,7 @@ module Markdown
53
53
  # @param dest_nodes [Array] Unmatched nodes from destination
54
54
  # @param context [Hash] Additional context (may contain :template_analysis, :dest_analysis)
55
55
  # @return [Array<MatchResult>] Array of table matches
56
- def call(template_nodes, dest_nodes, context = {})
56
+ def call(template_nodes, dest_nodes, _context = {})
57
57
  template_tables = extract_tables(template_nodes)
58
58
  dest_tables = extract_tables(dest_nodes)
59
59
 
@@ -89,23 +89,19 @@ module Markdown
89
89
  # @return [Boolean]
90
90
  def table_node?(node)
91
91
  # Check if it's a typed wrapper node first
92
- if Ast::Merge::NodeTyping.typed_node?(node)
93
- return Ast::Merge::NodeTyping.merge_type_for(node) == :table
94
- end
92
+ return Ast::Merge::NodeTyping.merge_type_for(node) == :table if Ast::Merge::NodeTyping.typed_node?(node)
95
93
 
96
94
  # Check merge_type directly (wrapped nodes from NodeTypeNormalizer)
97
- if node.respond_to?(:merge_type) && node.merge_type
98
- return node.merge_type == :table
99
- end
95
+ return node.merge_type == :table if node.respond_to?(:merge_type) && node.merge_type
100
96
 
101
97
  # Check raw type (string comparison for tree_haver nodes)
102
98
  if node.respond_to?(:type)
103
99
  node_type = node.type
104
- return node_type == :table || node_type == "table" || node_type.to_s == "table"
100
+ return [:table, 'table'].include?(node_type) || node_type.to_s == 'table'
105
101
  end
106
102
 
107
103
  # Fallback: class name check
108
- return true if node.class.name.to_s.include?("Table")
104
+ return true if node.class.name.to_s.include?('Table')
109
105
 
110
106
  false
111
107
  end
@@ -126,7 +122,7 @@ module Markdown
126
122
  total_tables_a: total_t,
127
123
  total_tables_b: total_d,
128
124
  backend: @backend,
129
- **algorithm_options,
125
+ **algorithm_options
130
126
  )
131
127
 
132
128
  algorithm.call(t_table, d_table)
@@ -2,11 +2,12 @@
2
2
 
3
3
  module Markdown
4
4
  module Merge
5
- # Version information for Markdown::Merge
5
+ # Version namespace for this gem.
6
6
  module Version
7
- # Current version of the markdown-merge gem
8
- VERSION = "1.0.3"
7
+ # Current gem version.
8
+ VERSION = '7.1.1'
9
9
  end
10
- VERSION = Version::VERSION # traditional location
10
+ # Current gem version exposed at the traditional constant location.
11
+ VERSION = Version::VERSION # Traditional Constant Location
11
12
  end
12
13
  end
@@ -75,9 +75,7 @@ module Markdown
75
75
  result = collapse_excessive_blank_lines(result)
76
76
 
77
77
  # Remove blank lines between link refs if mode requires it
78
- if @mode == :link_refs || @mode == :strict
79
- result = remove_blank_lines_between_link_refs(result)
80
- end
78
+ result = remove_blank_lines_between_link_refs(result) if @mode == :link_refs || @mode == :strict
81
79
 
82
80
  result
83
81
  end
@@ -107,9 +105,9 @@ module Markdown
107
105
  when true
108
106
  :basic
109
107
  when false
110
- :basic # Still do basic normalization
108
+ :basic # Still do basic normalization
111
109
  when Symbol
112
- raise ArgumentError, "Unknown mode: #{mode}. Valid modes: #{MODES.join(", ")}" unless MODES.include?(mode)
110
+ raise ArgumentError, "Unknown mode: #{mode}. Valid modes: #{MODES.join(', ')}" unless MODES.include?(mode)
113
111
 
114
112
  mode
115
113
  else
@@ -142,9 +140,7 @@ module Markdown
142
140
  problem_start_line ||= line_number - 1
143
141
 
144
142
  # Only add up to 1 blank line (which creates the standard paragraph gap)
145
- if consecutive_blank_count <= 1
146
- result << line
147
- end
143
+ result << line if consecutive_blank_count <= 1
148
144
  # Skip adding lines when consecutive_blank_count >= 2
149
145
  else
150
146
  # Record problem if we had 2+ blank lines (which means 3+ newlines)
@@ -155,7 +151,7 @@ module Markdown
155
151
  severity: :warning,
156
152
  line: problem_start_line,
157
153
  newline_count: consecutive_blank_count + 1, # +1 because first line ends with \n too
158
- collapsed_to: 2,
154
+ collapsed_to: 2
159
155
  )
160
156
  end
161
157
 
@@ -172,7 +168,7 @@ module Markdown
172
168
  severity: :warning,
173
169
  line: problem_start_line,
174
170
  newline_count: consecutive_blank_count + 1,
175
- collapsed_to: 2,
171
+ collapsed_to: 2
176
172
  )
177
173
  end
178
174
 
@@ -204,29 +200,25 @@ module Markdown
204
200
  j = i + 1
205
201
  while j < lines.length
206
202
  next_line = lines[j]
207
- if next_line.chomp.empty?
208
- # Check if there's a link ref definition after the blank line(s)
209
- k = j + 1
210
- while k < lines.length && lines[k].chomp.empty?
211
- k += 1
212
- end
213
- if k < lines.length && link_definition_line?(lines[k])
214
- # Skip all blank lines between link refs
215
- blanks_skipped = k - j
216
- @problems.add(
217
- :link_ref_spacing,
218
- severity: :info,
219
- line: j + 1,
220
- blank_lines_removed: blanks_skipped,
221
- )
222
- j = k
223
- else
224
- # Not followed by a link ref, keep the blank line
225
- break
226
- end
227
- else
228
- break
229
- end
203
+ break unless next_line.chomp.empty?
204
+
205
+ # Check if there's a link ref definition after the blank line(s)
206
+ k = j + 1
207
+ k += 1 while k < lines.length && lines[k].chomp.empty?
208
+ break unless k < lines.length && link_definition_line?(lines[k])
209
+
210
+ # Skip all blank lines between link refs
211
+ blanks_skipped = k - j
212
+ @problems.add(
213
+ :link_ref_spacing,
214
+ severity: :info,
215
+ line: j + 1,
216
+ blank_lines_removed: blanks_skipped
217
+ )
218
+ j = k
219
+
220
+ # Not followed by a link ref, keep the blank line
221
+
230
222
  end
231
223
  i = j
232
224
  else
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ # Shared bootstrap helpers for backend-specific Markdown wrapper gems.
6
+ module WrapperSupport
7
+ SHARED_REEXPORTS = {
8
+ FileAligner: Markdown::Merge::FileAligner,
9
+ ConflictResolver: Markdown::Merge::ConflictResolver,
10
+ MergeResult: Markdown::Merge::MergeResult,
11
+ TableMatchAlgorithm: Markdown::Merge::TableMatchAlgorithm,
12
+ TableMatchRefiner: Markdown::Merge::TableMatchRefiner,
13
+ CodeBlockMerger: Markdown::Merge::CodeBlockMerger,
14
+ NodeTypeNormalizer: Markdown::Merge::NodeTypeNormalizer
15
+ }.freeze
16
+
17
+ WRAPPER_AUTOLOADS = {
18
+ DebugLogger: 'debug_logger',
19
+ CommentTracker: 'comment_tracker',
20
+ FreezeNode: 'freeze_node',
21
+ FileAnalysis: 'file_analysis',
22
+ PartialTemplateMerger: 'partial_template_merger',
23
+ SmartMerger: 'smart_merger',
24
+ Backend: 'backend'
25
+ }.freeze
26
+
27
+ module_function
28
+
29
+ def install!(
30
+ wrapper_module:,
31
+ require_prefix:,
32
+ default_freeze_token:,
33
+ default_inner_merge_code_blocks:,
34
+ registry_tag:,
35
+ merger_class:,
36
+ test_source: "# Test\n\nParagraph",
37
+ category: :markdown
38
+ )
39
+ define_error_classes!(wrapper_module)
40
+ define_constant_unless_present(wrapper_module, :DEFAULT_FREEZE_TOKEN, default_freeze_token)
41
+ define_constant_unless_present(wrapper_module, :DEFAULT_INNER_MERGE_CODE_BLOCKS,
42
+ default_inner_merge_code_blocks)
43
+
44
+ SHARED_REEXPORTS.each do |name, value|
45
+ define_constant_unless_present(wrapper_module, name, value)
46
+ end
47
+
48
+ WRAPPER_AUTOLOADS.each do |name, suffix|
49
+ next if wrapper_module.const_defined?(name, false) || wrapper_module.autoload?(name)
50
+
51
+ wrapper_module.send(:autoload, name, "#{require_prefix}/#{suffix}")
52
+ end
53
+
54
+ install_backend_loader!(wrapper_module)
55
+ register_merge_gem!(
56
+ registry_tag: registry_tag,
57
+ require_path: require_prefix,
58
+ merger_class: merger_class,
59
+ test_source: test_source,
60
+ category: category
61
+ )
62
+ end
63
+
64
+ def configure_debug_logger!(debug_logger_module:, env_var_name:, log_prefix:)
65
+ debug_logger_module.extend(Ast::Merge::DebugLogger)
66
+ debug_logger_module.env_var_name = env_var_name
67
+ debug_logger_module.log_prefix = log_prefix
68
+ end
69
+
70
+ def configure_file_analysis_subclass!(klass, default_backend:, default_parser_options: nil)
71
+ install_singleton_value_method!(klass, :default_backend, default_backend)
72
+ return unless default_parser_options
73
+
74
+ install_singleton_value_method!(klass, :default_parser_options,
75
+ default_parser_options)
76
+ end
77
+
78
+ def configure_smart_merger_subclass!(
79
+ klass,
80
+ default_backend:,
81
+ default_freeze_token: nil,
82
+ default_inner_merge_code_blocks: nil,
83
+ default_parser_options: nil,
84
+ file_analysis_class: nil,
85
+ template_parse_error_class: nil,
86
+ destination_parse_error_class: nil
87
+ )
88
+ install_singleton_value_method!(klass, :default_backend, default_backend)
89
+ install_singleton_value_method!(klass, :default_freeze_token, default_freeze_token) if default_freeze_token
90
+ unless default_inner_merge_code_blocks.nil?
91
+ install_singleton_value_method!(klass, :default_inner_merge_code_blocks,
92
+ default_inner_merge_code_blocks)
93
+ end
94
+ if default_parser_options
95
+ install_singleton_value_method!(klass, :default_parser_options,
96
+ default_parser_options)
97
+ end
98
+ install_singleton_value_method!(klass, :file_analysis_class, file_analysis_class) if file_analysis_class
99
+ if template_parse_error_class
100
+ install_singleton_value_method!(klass, :template_parse_error_class,
101
+ template_parse_error_class)
102
+ end
103
+ return unless destination_parse_error_class
104
+
105
+ install_singleton_value_method!(klass, :destination_parse_error_class,
106
+ destination_parse_error_class)
107
+ end
108
+
109
+ def configure_partial_template_merger_subclass!(klass, default_backend:, file_analysis_class:,
110
+ smart_merger_class:)
111
+ install_singleton_value_method!(klass, :default_backend, default_backend)
112
+ install_singleton_value_method!(klass, :file_analysis_class, file_analysis_class)
113
+ install_singleton_value_method!(klass, :smart_merger_class, smart_merger_class)
114
+ end
115
+
116
+ def install_backend_loader!(wrapper_module)
117
+ return if wrapper_module.respond_to?(:ensure_backend_loaded!)
118
+
119
+ wrapper_module.singleton_class.class_eval do
120
+ define_method(:ensure_backend_loaded!) do
121
+ const_get(:Backend)
122
+ end
123
+ end
124
+ end
125
+ private_class_method :install_backend_loader!
126
+
127
+ def define_error_classes!(wrapper_module)
128
+ define_constant_unless_present(wrapper_module, :Error, Class.new(Markdown::Merge::Error))
129
+ define_constant_unless_present(wrapper_module, :ParseError, Class.new(Markdown::Merge::ParseError))
130
+ define_constant_unless_present(
131
+ wrapper_module,
132
+ :TemplateParseError,
133
+ Class.new(wrapper_module.const_get(:ParseError))
134
+ )
135
+ define_constant_unless_present(
136
+ wrapper_module,
137
+ :DestinationParseError,
138
+ Class.new(wrapper_module.const_get(:ParseError))
139
+ )
140
+ end
141
+ private_class_method :define_error_classes!
142
+
143
+ def install_singleton_value_method!(klass, method_name, value)
144
+ klass.singleton_class.class_eval do
145
+ define_method(method_name) do
146
+ WrapperSupport.send(:resolve_config_value, value, context: self)
147
+ end
148
+ end
149
+ end
150
+ private_class_method :install_singleton_value_method!
151
+
152
+ def resolve_config_value(value, context:)
153
+ return context.instance_exec(&value) if value.respond_to?(:call)
154
+
155
+ duplicate_config_value(value)
156
+ end
157
+ private_class_method :resolve_config_value
158
+
159
+ def duplicate_config_value(value)
160
+ case value
161
+ when Hash
162
+ value.each_with_object({}) do |(key, nested_value), result|
163
+ result[key] = duplicate_config_value(nested_value)
164
+ end
165
+ when Array
166
+ value.map { |nested_value| duplicate_config_value(nested_value) }
167
+ else
168
+ value
169
+ end
170
+ end
171
+ private_class_method :duplicate_config_value
172
+
173
+ def register_merge_gem!(registry_tag:, require_path:, merger_class:, test_source:, category:)
174
+ return unless defined?(Ast::Merge::RSpec::MergeGemRegistry)
175
+
176
+ Ast::Merge::RSpec::MergeGemRegistry.register(
177
+ registry_tag,
178
+ require_path: require_path,
179
+ merger_class: merger_class,
180
+ test_source: test_source,
181
+ category: category
182
+ )
183
+ end
184
+ private_class_method :register_merge_gem!
185
+
186
+ def define_constant_unless_present(wrapper_module, name, value)
187
+ return if wrapper_module.const_defined?(name, false)
188
+
189
+ wrapper_module.const_set(name, value)
190
+ end
191
+ private_class_method :define_constant_unless_present
192
+ end
193
+ end
194
+ end