markdown-merge 1.0.0
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 +7 -0
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +251 -0
- data/CITATION.cff +20 -0
- data/CODE_OF_CONDUCT.md +134 -0
- data/CONTRIBUTING.md +227 -0
- data/FUNDING.md +74 -0
- data/LICENSE.txt +21 -0
- data/README.md +1087 -0
- data/REEK +0 -0
- data/RUBOCOP.md +71 -0
- data/SECURITY.md +21 -0
- data/lib/markdown/merge/cleanse/block_spacing.rb +253 -0
- data/lib/markdown/merge/cleanse/code_fence_spacing.rb +294 -0
- data/lib/markdown/merge/cleanse/condensed_link_refs.rb +405 -0
- data/lib/markdown/merge/cleanse.rb +42 -0
- data/lib/markdown/merge/code_block_merger.rb +300 -0
- data/lib/markdown/merge/conflict_resolver.rb +128 -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 +196 -0
- data/lib/markdown/merge/file_analysis.rb +353 -0
- data/lib/markdown/merge/file_analysis_base.rb +629 -0
- data/lib/markdown/merge/freeze_node.rb +93 -0
- data/lib/markdown/merge/gap_line_node.rb +136 -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/markdown_structure.rb +123 -0
- data/lib/markdown/merge/merge_result.rb +166 -0
- data/lib/markdown/merge/node_type_normalizer.rb +126 -0
- data/lib/markdown/merge/output_builder.rb +166 -0
- data/lib/markdown/merge/partial_template_merger.rb +334 -0
- data/lib/markdown/merge/smart_merger.rb +221 -0
- data/lib/markdown/merge/smart_merger_base.rb +621 -0
- data/lib/markdown/merge/table_match_algorithm.rb +504 -0
- data/lib/markdown/merge/table_match_refiner.rb +136 -0
- data/lib/markdown/merge/version.rb +12 -0
- data/lib/markdown/merge/whitespace_normalizer.rb +251 -0
- data/lib/markdown/merge.rb +149 -0
- data/lib/markdown-merge.rb +4 -0
- data/sig/markdown/merge.rbs +341 -0
- data.tar.gz.sig +0 -0
- metadata +365 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Orchestrates the smart merge process for Markdown files using tree_haver backends.
|
|
6
|
+
#
|
|
7
|
+
# Extends SmartMergerBase with backend-agnostic parsing via tree_haver.
|
|
8
|
+
# Supports both Commonmarker and Markly backends.
|
|
9
|
+
#
|
|
10
|
+
# Uses FileAnalysis, FileAligner, ConflictResolver, and MergeResult to
|
|
11
|
+
# merge two Markdown files intelligently. Freeze blocks marked with
|
|
12
|
+
# HTML comments are preserved exactly as-is.
|
|
13
|
+
#
|
|
14
|
+
# SmartMerger provides flexible configuration for different merge scenarios:
|
|
15
|
+
# - Preserve destination customizations (default)
|
|
16
|
+
# - Apply template updates
|
|
17
|
+
# - Add new sections from template
|
|
18
|
+
# - Inner-merge fenced code blocks using language-specific mergers (optional)
|
|
19
|
+
#
|
|
20
|
+
# @example Basic merge (destination customizations preserved)
|
|
21
|
+
# merger = SmartMerger.new(template_content, dest_content)
|
|
22
|
+
# result = merger.merge
|
|
23
|
+
# if result.success?
|
|
24
|
+
# File.write("output.md", result.content)
|
|
25
|
+
# end
|
|
26
|
+
#
|
|
27
|
+
# @example With specific backend
|
|
28
|
+
# merger = SmartMerger.new(
|
|
29
|
+
# template_content,
|
|
30
|
+
# dest_content,
|
|
31
|
+
# backend: :markly
|
|
32
|
+
# )
|
|
33
|
+
# result = merger.merge
|
|
34
|
+
#
|
|
35
|
+
# @example Template updates win
|
|
36
|
+
# merger = SmartMerger.new(
|
|
37
|
+
# template_content,
|
|
38
|
+
# dest_content,
|
|
39
|
+
# preference: :template,
|
|
40
|
+
# add_template_only_nodes: true
|
|
41
|
+
# )
|
|
42
|
+
# result = merger.merge
|
|
43
|
+
#
|
|
44
|
+
# @example Custom signature matching
|
|
45
|
+
# sig_gen = ->(node) {
|
|
46
|
+
# canonical_type = Ast::Merge::NodeTyping.merge_type_for(node) || node.type
|
|
47
|
+
# if canonical_type == :heading
|
|
48
|
+
# [:heading, node.header_level] # Match by level only, not content
|
|
49
|
+
# else
|
|
50
|
+
# node # Fall through to default
|
|
51
|
+
# end
|
|
52
|
+
# }
|
|
53
|
+
# merger = SmartMerger.new(
|
|
54
|
+
# template_content,
|
|
55
|
+
# dest_content,
|
|
56
|
+
# signature_generator: sig_gen
|
|
57
|
+
# )
|
|
58
|
+
#
|
|
59
|
+
# @see FileAnalysis
|
|
60
|
+
# @see SmartMergerBase
|
|
61
|
+
class SmartMerger < SmartMergerBase
|
|
62
|
+
# @return [Symbol] The backend being used (:commonmarker, :markly)
|
|
63
|
+
attr_reader :backend
|
|
64
|
+
|
|
65
|
+
# Creates a new SmartMerger for intelligent Markdown file merging.
|
|
66
|
+
#
|
|
67
|
+
# @param template_content [String] Template Markdown source code
|
|
68
|
+
# @param dest_content [String] Destination Markdown source code
|
|
69
|
+
#
|
|
70
|
+
# @param backend [Symbol] Backend to use for parsing:
|
|
71
|
+
# - `:commonmarker` - Use Commonmarker (comrak Rust parser)
|
|
72
|
+
# - `:markly` - Use Markly (cmark-gfm C library)
|
|
73
|
+
# - `:auto` (default) - Auto-detect available backend
|
|
74
|
+
#
|
|
75
|
+
# @param signature_generator [Proc, nil] Optional proc to generate custom node signatures.
|
|
76
|
+
# The proc receives a node (wrapped with canonical merge_type) and should return one of:
|
|
77
|
+
# - An array representing the node's signature
|
|
78
|
+
# - `nil` to indicate the node should have no signature
|
|
79
|
+
# - The original node to fall through to default signature computation
|
|
80
|
+
#
|
|
81
|
+
# @param preference [Symbol] Controls which version to use when nodes
|
|
82
|
+
# have matching signatures but different content:
|
|
83
|
+
# - `:destination` (default) - Use destination version (preserves customizations)
|
|
84
|
+
# - `:template` - Use template version (applies updates)
|
|
85
|
+
#
|
|
86
|
+
# @param add_template_only_nodes [Boolean] Controls whether to add nodes that only
|
|
87
|
+
# exist in template:
|
|
88
|
+
# - `false` (default) - Skip template-only nodes
|
|
89
|
+
# - `true` - Add template-only nodes to result
|
|
90
|
+
#
|
|
91
|
+
# @param inner_merge_code_blocks [Boolean, CodeBlockMerger] Controls inner-merge for
|
|
92
|
+
# fenced code blocks:
|
|
93
|
+
# - `true` - Enable inner-merge using default CodeBlockMerger
|
|
94
|
+
# - `false` (default) - Disable inner-merge (use standard conflict resolution)
|
|
95
|
+
# - `CodeBlockMerger` instance - Use custom CodeBlockMerger
|
|
96
|
+
#
|
|
97
|
+
# @param freeze_token [String] Token to use for freeze block markers.
|
|
98
|
+
# Default: "markdown-merge"
|
|
99
|
+
# Looks for: <!-- markdown-merge:freeze --> / <!-- markdown-merge:unfreeze -->
|
|
100
|
+
#
|
|
101
|
+
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching of
|
|
102
|
+
# unmatched nodes. Default: nil (fuzzy matching disabled).
|
|
103
|
+
# Set to TableMatchRefiner.new to enable fuzzy table matching.
|
|
104
|
+
#
|
|
105
|
+
# @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
|
|
106
|
+
# for per-node-type merge preferences. Maps node type names to callables.
|
|
107
|
+
#
|
|
108
|
+
# @param parser_options [Hash] Backend-specific parser options.
|
|
109
|
+
# For commonmarker: { options: {} }
|
|
110
|
+
# For markly: { flags: Markly::DEFAULT, extensions: [:table] }
|
|
111
|
+
#
|
|
112
|
+
# @raise [TemplateParseError] If template has syntax errors
|
|
113
|
+
# @raise [DestinationParseError] If destination has syntax errors
|
|
114
|
+
def initialize(
|
|
115
|
+
template_content,
|
|
116
|
+
dest_content,
|
|
117
|
+
backend: :auto,
|
|
118
|
+
signature_generator: nil,
|
|
119
|
+
preference: :destination,
|
|
120
|
+
add_template_only_nodes: false,
|
|
121
|
+
inner_merge_code_blocks: false,
|
|
122
|
+
freeze_token: FileAnalysis::DEFAULT_FREEZE_TOKEN,
|
|
123
|
+
match_refiner: nil,
|
|
124
|
+
node_typing: nil,
|
|
125
|
+
**parser_options
|
|
126
|
+
)
|
|
127
|
+
@requested_backend = backend
|
|
128
|
+
@parser_options = parser_options
|
|
129
|
+
|
|
130
|
+
super(
|
|
131
|
+
template_content,
|
|
132
|
+
dest_content,
|
|
133
|
+
signature_generator: signature_generator,
|
|
134
|
+
preference: preference,
|
|
135
|
+
add_template_only_nodes: add_template_only_nodes,
|
|
136
|
+
inner_merge_code_blocks: inner_merge_code_blocks,
|
|
137
|
+
freeze_token: freeze_token,
|
|
138
|
+
match_refiner: match_refiner,
|
|
139
|
+
node_typing: node_typing,
|
|
140
|
+
# Pass through for FileAnalysis
|
|
141
|
+
backend: backend,
|
|
142
|
+
**parser_options,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Capture the resolved backend from template analysis
|
|
146
|
+
@backend = @template_analysis.backend
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Create a FileAnalysis instance for parsing.
|
|
150
|
+
#
|
|
151
|
+
# @param content [String] Markdown content to analyze
|
|
152
|
+
# @param options [Hash] Analysis options
|
|
153
|
+
# @return [FileAnalysis] File analysis instance
|
|
154
|
+
def create_file_analysis(content, **opts)
|
|
155
|
+
FileAnalysis.new(
|
|
156
|
+
content,
|
|
157
|
+
backend: opts[:backend] || @requested_backend,
|
|
158
|
+
freeze_token: opts[:freeze_token],
|
|
159
|
+
signature_generator: opts[:signature_generator],
|
|
160
|
+
**@parser_options,
|
|
161
|
+
)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Returns the TemplateParseError class to use.
|
|
165
|
+
#
|
|
166
|
+
# @return [Class] Markdown::Merge::TemplateParseError
|
|
167
|
+
def template_parse_error_class
|
|
168
|
+
TemplateParseError
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Returns the DestinationParseError class to use.
|
|
172
|
+
#
|
|
173
|
+
# @return [Class] Markdown::Merge::DestinationParseError
|
|
174
|
+
def destination_parse_error_class
|
|
175
|
+
DestinationParseError
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Convert a node to its source text.
|
|
179
|
+
#
|
|
180
|
+
# Handles wrapped nodes from NodeTypeNormalizer, gap line nodes,
|
|
181
|
+
# and link definition nodes created during gap detection.
|
|
182
|
+
#
|
|
183
|
+
# @param node [Object] Node to convert (may be wrapped)
|
|
184
|
+
# @param analysis [FileAnalysis] Analysis for source lookup
|
|
185
|
+
# @return [String] Source text
|
|
186
|
+
def node_to_source(node, analysis)
|
|
187
|
+
# Check for any FreezeNode type (base class or subclass)
|
|
188
|
+
if node.is_a?(Ast::Merge::FreezeNodeBase)
|
|
189
|
+
return node.full_text
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Handle gap line nodes (created for blank lines and link definitions)
|
|
193
|
+
if node.is_a?(LinkDefinitionNode) || node.is_a?(GapLineNode)
|
|
194
|
+
return node.content
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Unwrap if needed to access source_position
|
|
198
|
+
raw_node = Ast::Merge::NodeTyping.unwrap(node)
|
|
199
|
+
|
|
200
|
+
pos = raw_node.source_position
|
|
201
|
+
start_line = pos&.dig(:start_line)
|
|
202
|
+
end_line = pos&.dig(:end_line)
|
|
203
|
+
|
|
204
|
+
# Fall back to to_commonmark if no position info
|
|
205
|
+
return raw_node.to_commonmark unless start_line && end_line
|
|
206
|
+
|
|
207
|
+
# Get source from line range
|
|
208
|
+
source = analysis.source_range(start_line, end_line)
|
|
209
|
+
|
|
210
|
+
# Handle Markly's buggy position reporting for :html nodes
|
|
211
|
+
# where end_line < start_line results in empty source_range.
|
|
212
|
+
# Fall back to to_commonmark in that case.
|
|
213
|
+
if source.empty? && raw_node.respond_to?(:to_commonmark)
|
|
214
|
+
raw_node.to_commonmark.chomp
|
|
215
|
+
else
|
|
216
|
+
source
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|