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
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ module Cleanse
6
+ # Repairs templating corruption where an ordered-list marker was prefixed
7
+ # onto an existing unordered-list marker, producing lines like:
8
+ #
9
+ # 1. - item
10
+ # 2. * item
11
+ #
12
+ # The repair keeps the original inner bullet marker and removes the
13
+ # synthetic ordered marker.
14
+ class ListMarkerDuplication
15
+ DUPLICATED_MARKER = /\A(?<indent>\s*)(?<number>\d+)\.\s+(?<bullet>[-*+])(?<tail>(?:\s+.*)?\s*)\z/
16
+
17
+ attr_reader :source, :issues
18
+
19
+ def initialize(source)
20
+ @source = source.to_s
21
+ @issues = []
22
+ analyze
23
+ end
24
+
25
+ def malformed?
26
+ issues.any?
27
+ end
28
+
29
+ def issue_count
30
+ issues.length
31
+ end
32
+
33
+ def fix
34
+ return source unless malformed?
35
+
36
+ source.each_line.with_index(1).map do |line, line_number|
37
+ duplicated_marker_line?(line) ? repaired_line(line, line_number) : line
38
+ end.join
39
+ end
40
+
41
+ private
42
+
43
+ def analyze
44
+ source.each_line.with_index(1) do |line, line_number|
45
+ next unless duplicated_marker_line?(line)
46
+
47
+ issues << {
48
+ type: :duplicated_list_marker,
49
+ line: line_number,
50
+ description: 'Ordered-list marker duplicated an existing unordered-list marker'
51
+ }
52
+ end
53
+ end
54
+
55
+ def duplicated_marker_line?(line)
56
+ line.match?(DUPLICATED_MARKER)
57
+ end
58
+
59
+ def repaired_line(line, _line_number)
60
+ match = line.match(DUPLICATED_MARKER)
61
+ "#{match[:indent]}#{match[:bullet]}#{match[:tail]}"
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ module Cleanse
6
+ # Composes targeted repair passes for known templating corruption
7
+ # signatures seen in historical kettle-jem runs.
8
+ class TemplatingCorruption
9
+ PASS_TYPES = [
10
+ ListMarkerDuplication,
11
+ CondensedLinkRefs,
12
+ CodeFenceSpacing,
13
+ BlockSpacing
14
+ ].freeze
15
+
16
+ attr_reader :source, :passes, :issues
17
+
18
+ def initialize(source)
19
+ @source = source.to_s
20
+ @passes = []
21
+ @issues = []
22
+ analyze
23
+ end
24
+
25
+ def malformed?
26
+ issues.any?
27
+ end
28
+
29
+ def issue_count
30
+ issues.length
31
+ end
32
+
33
+ def fix
34
+ current = source
35
+
36
+ PASS_TYPES.each do |pass_type|
37
+ pass = pass_type.new(current)
38
+ next unless pass_issues?(pass)
39
+
40
+ current = pass_output(pass, current)
41
+ end
42
+
43
+ current
44
+ end
45
+
46
+ private
47
+
48
+ def analyze
49
+ current = source
50
+
51
+ PASS_TYPES.each do |pass_type|
52
+ pass = pass_type.new(current)
53
+ @passes << pass
54
+ append_issues(pass)
55
+ current = pass_output(pass, current) if pass_issues?(pass)
56
+ end
57
+ end
58
+
59
+ def append_issues(pass)
60
+ if pass.respond_to?(:issues)
61
+ issues.concat(Array(pass.issues))
62
+ elsif pass_issues?(pass)
63
+ issues << {
64
+ type: pass.class.name.split('::').last.gsub(/([a-z])([A-Z])/, '\1_\2').downcase.to_sym,
65
+ description: "#{pass.class.name} detected malformed content"
66
+ }
67
+ end
68
+ end
69
+
70
+ def pass_issues?(pass)
71
+ pass.respond_to?(:malformed?) ? pass.malformed? : pass.respond_to?(:condensed?) && pass.condensed?
72
+ end
73
+
74
+ def pass_output(pass, fallback)
75
+ if pass.respond_to?(:fix)
76
+ pass.fix
77
+ elsif pass.respond_to?(:expand)
78
+ pass.expand
79
+ else
80
+ fallback
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ # Namespace for document cleansing/repair utilities.
6
+ #
7
+ # The Cleanse module contains parsers and fixers for repairing malformed
8
+ # Markdown documents, particularly those affected by previous bugs in
9
+ # ast-merge or other merge tools.
10
+ #
11
+ # @example Fix condensed link reference definitions
12
+ # content = File.read("README.md")
13
+ # parser = Markdown::Merge::Cleanse::CondensedLinkRefs.new(content)
14
+ # if parser.condensed?
15
+ # File.write("README.md", parser.expand)
16
+ # end
17
+ #
18
+ # @example Fix code fence spacing issues
19
+ # content = File.read("README.md")
20
+ # parser = Markdown::Merge::Cleanse::CodeFenceSpacing.new(content)
21
+ # if parser.malformed?
22
+ # File.write("README.md", parser.fix)
23
+ # end
24
+ #
25
+ # @example Fix block element spacing issues
26
+ # content = File.read("README.md")
27
+ # parser = Markdown::Merge::Cleanse::BlockSpacing.new(content)
28
+ # if parser.malformed?
29
+ # File.write("README.md", parser.fix)
30
+ # end
31
+ #
32
+ # @see Cleanse::CondensedLinkRefs For fixing condensed link reference definitions
33
+ # @see Cleanse::CodeFenceSpacing For fixing code fence spacing issues
34
+ # @see Cleanse::BlockSpacing For fixing missing blank lines between block elements
35
+ # @api public
36
+ module Cleanse
37
+ autoload :BlockSpacing, 'markdown/merge/cleanse/block_spacing'
38
+ autoload :CodeFenceSpacing, 'markdown/merge/cleanse/code_fence_spacing'
39
+ autoload :CondensedLinkRefs, 'markdown/merge/cleanse/condensed_link_refs'
40
+ autoload :ListMarkerDuplication, 'markdown/merge/cleanse/list_marker_duplication'
41
+ autoload :TemplatingCorruption, 'markdown/merge/cleanse/templating_corruption'
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Markdown
4
+ module Merge
5
+ # Fuzzy matches fenced code blocks by fence info and surrounding markdown context
6
+ # so inner code-block merging can run even when block content itself differs.
7
+ class CodeBlockMatchRefiner < Ast::Merge::MatchRefinerBase
8
+ include Ast::Merge::JaccardSimilarity
9
+
10
+ DEFAULT_THRESHOLD = 0.65
11
+
12
+ def initialize(threshold: DEFAULT_THRESHOLD, **options)
13
+ super(threshold: threshold, node_types: [:code_block], **options)
14
+ end
15
+
16
+ def call(template_nodes, dest_nodes, context = {})
17
+ template_blocks = template_nodes.select { |node| node_type(node).to_s == 'code_block' }
18
+ dest_blocks = dest_nodes.select { |node| node_type(node).to_s == 'code_block' }
19
+ return [] if template_blocks.empty? || dest_blocks.empty?
20
+
21
+ greedy_match(template_blocks, dest_blocks) 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
+ return 0.0 unless normalized_fence_info(template_node) == normalized_fence_info(dest_node)
30
+
31
+ template_analysis = context[:template_analysis]
32
+ dest_analysis = context[:dest_analysis]
33
+ context_score = surrounding_context_similarity(template_node, template_analysis, dest_node, dest_analysis)
34
+ position_score = relative_position_similarity(template_node, template_analysis, dest_node, dest_analysis)
35
+
36
+ 0.75 + (context_score * 0.15) + (position_score * 0.10)
37
+ end
38
+
39
+ def normalized_fence_info(node)
40
+ raw = Ast::Merge::NodeTyping.unwrap(node)
41
+ raw.respond_to?(:fence_info) ? raw.fence_info.to_s.strip.downcase : ''
42
+ end
43
+
44
+ def surrounding_context_similarity(template_node, template_analysis, dest_node, dest_analysis)
45
+ template_context = [preceding_context_text(template_node, template_analysis),
46
+ following_context_text(template_node, template_analysis)].join(' ')
47
+ dest_context = [preceding_context_text(dest_node, dest_analysis),
48
+ following_context_text(dest_node, dest_analysis)].join(' ')
49
+ return 0.0 if template_context.empty? || dest_context.empty?
50
+
51
+ jaccard(extract_tokens(template_context), extract_tokens(dest_context))
52
+ end
53
+
54
+ def relative_position_similarity(template_node, template_analysis, dest_node, dest_analysis)
55
+ template_index = statement_index(template_analysis, template_node)
56
+ dest_index = statement_index(dest_analysis, dest_node)
57
+ template_count = statement_count(template_analysis)
58
+ dest_count = statement_count(dest_analysis)
59
+ return 0.0 unless template_index && dest_index && template_count.positive? && dest_count.positive?
60
+
61
+ template_ratio = template_index.to_f / template_count
62
+ dest_ratio = dest_index.to_f / dest_count
63
+ 1.0 - (template_ratio - dest_ratio).abs
64
+ end
65
+
66
+ def preceding_context_text(node, analysis)
67
+ return '' unless analysis
68
+
69
+ index = statement_index(analysis, node)
70
+ return '' unless index
71
+
72
+ (index - 1).downto(0) do |current_index|
73
+ candidate = analysis.statements[current_index]
74
+ signature = analysis.signature_at(current_index)
75
+ next unless signature.is_a?(Array) && %i[heading paragraph list].include?(signature.first)
76
+
77
+ return candidate.text.to_s
78
+ end
79
+
80
+ ''
81
+ end
82
+
83
+ def following_context_text(node, analysis)
84
+ return '' unless analysis
85
+
86
+ index = statement_index(analysis, node)
87
+ return '' unless index
88
+
89
+ ((index + 1)...analysis.statements.length).each do |current_index|
90
+ candidate = analysis.statements[current_index]
91
+ signature = analysis.signature_at(current_index)
92
+ next unless signature.is_a?(Array) && %i[heading paragraph list].include?(signature.first)
93
+
94
+ return candidate.text.to_s
95
+ end
96
+
97
+ ''
98
+ end
99
+
100
+ def statement_index(analysis, node)
101
+ return unless analysis
102
+
103
+ analysis.statements.index(node)
104
+ end
105
+
106
+ def statement_count(analysis)
107
+ Array(analysis&.statements).length
108
+ end
109
+ end
110
+ end
111
+ end