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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +673 -0
- data/lib/markdown/merge/backend_support.rb +200 -0
- data/lib/markdown/merge/cleanse/block_spacing.rb +248 -0
- data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
- data/lib/markdown/merge/cleanse/condensed_link_refs.rb +411 -0
- data/lib/markdown/merge/cleanse/list_marker_duplication.rb +66 -0
- data/lib/markdown/merge/cleanse/templating_corruption.rb +86 -0
- data/lib/markdown/merge/cleanse.rb +44 -0
- data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
- data/lib/markdown/merge/code_block_merger.rb +742 -0
- data/lib/markdown/merge/comment_tracker.rb +42 -0
- data/lib/markdown/merge/conflict_resolver.rb +199 -0
- data/lib/markdown/merge/debug_logger.rb +26 -0
- data/lib/markdown/merge/document_problems.rb +190 -0
- data/lib/markdown/merge/file_aligner.rb +496 -0
- data/lib/markdown/merge/file_analysis.rb +689 -0
- data/lib/markdown/merge/file_analysis_base.rb +766 -0
- data/lib/markdown/merge/freeze_node.rb +93 -0
- data/lib/markdown/merge/gap_line_node.rb +142 -0
- data/lib/markdown/merge/link_definition_formatter.rb +49 -0
- data/lib/markdown/merge/link_definition_node.rb +157 -0
- data/lib/markdown/merge/link_parser.rb +421 -0
- data/lib/markdown/merge/link_reference_rehydrator.rb +320 -0
- data/lib/markdown/merge/list_match_refiner.rb +98 -0
- data/lib/markdown/merge/list_merger.rb +322 -0
- data/lib/markdown/merge/markdown_structure.rb +123 -0
- data/lib/markdown/merge/merge_result.rb +483 -0
- data/lib/markdown/merge/node_type_normalizer.rb +126 -0
- data/lib/markdown/merge/output_builder.rb +248 -0
- data/lib/markdown/merge/partial_template_merger.rb +555 -0
- data/lib/markdown/merge/preservation_support.rb +291 -0
- data/lib/markdown/merge/rspec/shared_examples/source_preserving_provider.rb +338 -0
- data/lib/markdown/merge/smart_merger.rb +269 -0
- data/lib/markdown/merge/smart_merger_base.rb +1490 -0
- data/lib/markdown/merge/source_preserving_provider.rb +814 -0
- data/lib/markdown/merge/table_match_algorithm.rb +499 -0
- data/lib/markdown/merge/table_match_refiner.rb +132 -0
- data/lib/markdown/merge/version.rb +5 -3
- data/lib/markdown/merge/whitespace_normalizer.rb +243 -0
- data/lib/markdown/merge/wrapper_support.rb +194 -0
- data/lib/markdown/merge.rb +271 -87
- data/lib/markdown-merge.rb +7 -1
- data/sig/markdown/merge.rbs +62 -0
- data.tar.gz.sig +0 -0
- metadata +289 -15
- 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
|