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,248 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Builds markdown output from merge operations.
|
|
6
|
+
#
|
|
7
|
+
# Handles markdown-specific concerns like:
|
|
8
|
+
# - Extracting source from original nodes
|
|
9
|
+
# - Reconstructing consumed link reference definitions
|
|
10
|
+
# - Preserving gap lines (blank line spacing)
|
|
11
|
+
# - Automatic structural spacing (blank lines between tables, headings, etc.)
|
|
12
|
+
# - Assembling final merged content
|
|
13
|
+
#
|
|
14
|
+
# Unlike Emitter classes used in JSON/YAML/etc, OutputBuilder focuses on
|
|
15
|
+
# source preservation and reconstruction rather than generation from scratch.
|
|
16
|
+
#
|
|
17
|
+
# @example Basic usage
|
|
18
|
+
# builder = OutputBuilder.new
|
|
19
|
+
# builder.add_node_source(node, analysis)
|
|
20
|
+
# builder.add_link_definition(link_def_node)
|
|
21
|
+
# builder.add_gap_line(count: 2)
|
|
22
|
+
# content = builder.to_s
|
|
23
|
+
class OutputBuilder
|
|
24
|
+
# Initialize a new OutputBuilder
|
|
25
|
+
#
|
|
26
|
+
# @param preserve_formatting [Boolean] Whether to preserve original formatting
|
|
27
|
+
# @param auto_spacing [Boolean] Whether to automatically insert blank lines between structural elements
|
|
28
|
+
def initialize(preserve_formatting: true, auto_spacing: true)
|
|
29
|
+
@parts = []
|
|
30
|
+
@length = 0
|
|
31
|
+
@preserve_formatting = preserve_formatting
|
|
32
|
+
@auto_spacing = auto_spacing
|
|
33
|
+
@last_node_type = nil # Track previous node type for spacing decisions
|
|
34
|
+
@last_end_line = nil # Track previous node's end line for adjacency detection
|
|
35
|
+
@last_analysis = nil # Track previous node's analysis for same-source detection
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Add a node's source content
|
|
39
|
+
#
|
|
40
|
+
# Automatically inserts structural blank lines when transitioning between
|
|
41
|
+
# certain node types (tables, headings, code blocks, etc.) if auto_spacing is enabled.
|
|
42
|
+
# Skips auto-spacing when nodes are adjacent in the same source — the original
|
|
43
|
+
# formatting is preserved by the source extraction.
|
|
44
|
+
#
|
|
45
|
+
# @param node [Object] Node to add (can be parser node, FreezeNode, LinkDefinitionNode, etc.)
|
|
46
|
+
# @param analysis [FileAnalysisBase] Analysis for accessing source
|
|
47
|
+
def add_node_source(node, analysis)
|
|
48
|
+
# Determine node type for spacing decisions
|
|
49
|
+
current_type = MarkdownStructure.node_type(node)
|
|
50
|
+
|
|
51
|
+
# Auto-spacing logic:
|
|
52
|
+
# - Skip for gap_line and freeze_block (they handle their own spacing)
|
|
53
|
+
# - Skip if last node was a gap_line (we already have spacing)
|
|
54
|
+
# - Skip if nodes are adjacent in the same source (original spacing is correct)
|
|
55
|
+
# - Otherwise, check MarkdownStructure.needs_blank_between? which handles
|
|
56
|
+
# contiguous types (like link_definitions that shouldn't have blanks between them)
|
|
57
|
+
# Skip auto-spacing when nodes are adjacent lines in the same source.
|
|
58
|
+
# The original source formatting is correct — no blank line was there.
|
|
59
|
+
if !(%i[gap_line freeze_block].include?(current_type) ||
|
|
60
|
+
@last_node_type == :gap_line) && @auto_spacing && @last_node_type && current_type && MarkdownStructure.needs_blank_between?(@last_node_type,
|
|
61
|
+
current_type) && !same_source_adjacent?(
|
|
62
|
+
node, analysis
|
|
63
|
+
) && !(@parts.empty? || blank_line_terminated?)
|
|
64
|
+
# Only add spacing if we don't already have adequate blank lines
|
|
65
|
+
# Check the last part to see if it already ends with blank line(s)
|
|
66
|
+
add_gap_line(count: 1)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
content = extract_source(node, analysis)
|
|
70
|
+
return unless content && !content.empty?
|
|
71
|
+
|
|
72
|
+
range = append_part(content)
|
|
73
|
+
# Update last node type (track all node types for proper spacing)
|
|
74
|
+
@last_node_type = current_type
|
|
75
|
+
@last_end_line = node_end_line(node)
|
|
76
|
+
@last_analysis = analysis
|
|
77
|
+
range
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Add a reconstructed link definition
|
|
81
|
+
#
|
|
82
|
+
# @param node [LinkDefinitionNode] Link definition node
|
|
83
|
+
def add_link_definition(node)
|
|
84
|
+
formatted = LinkDefinitionFormatter.format(node)
|
|
85
|
+
append_part(formatted) if formatted && !formatted.empty?
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Add gap lines (blank line preservation)
|
|
89
|
+
#
|
|
90
|
+
# @param count [Integer] Number of blank lines to add
|
|
91
|
+
def add_gap_line(count: 1)
|
|
92
|
+
append_part("\n" * count) if count > 0
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Add raw text content
|
|
96
|
+
#
|
|
97
|
+
# @param text [String] Raw text to add
|
|
98
|
+
def add_raw(text)
|
|
99
|
+
append_part(text) if text && !text.empty?
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Get final content
|
|
103
|
+
#
|
|
104
|
+
# @return [String] Assembled markdown content
|
|
105
|
+
def to_s
|
|
106
|
+
@parts.join
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Check if builder has any content
|
|
110
|
+
#
|
|
111
|
+
# @return [Boolean]
|
|
112
|
+
def empty?
|
|
113
|
+
@parts.empty?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Check whether the current output already ends with a blank-line separator.
|
|
117
|
+
#
|
|
118
|
+
# This looks across part boundaries so a trailing blank line represented as
|
|
119
|
+
# separate content + gap parts still counts as an existing separator.
|
|
120
|
+
#
|
|
121
|
+
# @return [Boolean]
|
|
122
|
+
def blank_line_terminated?
|
|
123
|
+
trailing_newlines = 0
|
|
124
|
+
|
|
125
|
+
@parts.reverse_each do |part|
|
|
126
|
+
next if part.nil? || part.empty?
|
|
127
|
+
|
|
128
|
+
idx = part.length - 1
|
|
129
|
+
while idx >= 0 && part[idx] == "\n"
|
|
130
|
+
trailing_newlines += 1
|
|
131
|
+
idx -= 1
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
break if idx >= 0
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
trailing_newlines >= 2
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Clear all content
|
|
141
|
+
def clear
|
|
142
|
+
@parts.clear
|
|
143
|
+
@length = 0
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private
|
|
147
|
+
|
|
148
|
+
def append_part(text)
|
|
149
|
+
start_offset = @length
|
|
150
|
+
@parts << text
|
|
151
|
+
@length += text.bytesize
|
|
152
|
+
[start_offset, @length]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Check if the current node is adjacent (consecutive lines) to the previous
|
|
156
|
+
# node in the same source file. When true, the original source already has
|
|
157
|
+
# the correct inter-node spacing and auto-spacing should not add blank lines.
|
|
158
|
+
#
|
|
159
|
+
# @param node [Object] Current node being added
|
|
160
|
+
# @param analysis [FileAnalysisBase] Current node's analysis
|
|
161
|
+
# @return [Boolean] true if nodes are from the same source and adjacent
|
|
162
|
+
def same_source_adjacent?(node, analysis)
|
|
163
|
+
return false unless @last_end_line && @last_analysis
|
|
164
|
+
return false unless @last_analysis.equal?(analysis)
|
|
165
|
+
|
|
166
|
+
current_start = node_start_line(node)
|
|
167
|
+
return false unless current_start
|
|
168
|
+
|
|
169
|
+
# Adjacent: the current node starts on or immediately after the previous node ended
|
|
170
|
+
current_start <= @last_end_line + 1
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Extract start line from a node
|
|
174
|
+
#
|
|
175
|
+
# @param node [Object] Node to inspect
|
|
176
|
+
# @return [Integer, nil] 1-based start line
|
|
177
|
+
def node_start_line(node)
|
|
178
|
+
if node.respond_to?(:source_position)
|
|
179
|
+
node.source_position&.dig(:start_line)
|
|
180
|
+
elsif node.respond_to?(:start_line)
|
|
181
|
+
node.start_line
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Extract end line from a node
|
|
186
|
+
#
|
|
187
|
+
# @param node [Object] Node to inspect
|
|
188
|
+
# @return [Integer, nil] 1-based end line
|
|
189
|
+
def node_end_line(node)
|
|
190
|
+
if node.respond_to?(:source_position)
|
|
191
|
+
node.source_position&.dig(:end_line)
|
|
192
|
+
elsif node.respond_to?(:end_line)
|
|
193
|
+
node.end_line
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Extract source content from a node
|
|
198
|
+
#
|
|
199
|
+
# @param node [Object] Node to extract from
|
|
200
|
+
# @param analysis [FileAnalysisBase] Analysis for source access
|
|
201
|
+
# @return [String, nil] Extracted content
|
|
202
|
+
def extract_source(node, analysis)
|
|
203
|
+
case node
|
|
204
|
+
when LinkDefinitionNode
|
|
205
|
+
# Link definitions need reconstruction with trailing newline
|
|
206
|
+
"#{LinkDefinitionFormatter.format(node)}\n"
|
|
207
|
+
when GapLineNode
|
|
208
|
+
# Gap lines are single blank lines
|
|
209
|
+
"\n"
|
|
210
|
+
when Ast::Merge::FreezeNodeBase
|
|
211
|
+
# Freeze blocks have their full text
|
|
212
|
+
node.full_text
|
|
213
|
+
else
|
|
214
|
+
# Regular nodes - extract from source
|
|
215
|
+
extract_parser_node_source(node, analysis)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Extract source from a parser-specific node
|
|
220
|
+
#
|
|
221
|
+
# @param node [Object] Parser node
|
|
222
|
+
# @param analysis [FileAnalysisBase] Analysis for source access
|
|
223
|
+
# @return [String, nil] Extracted content
|
|
224
|
+
def extract_parser_node_source(node, analysis)
|
|
225
|
+
# Try source_position method first (used by some nodes)
|
|
226
|
+
if node.respond_to?(:source_position)
|
|
227
|
+
pos = node.source_position
|
|
228
|
+
start_line = pos&.dig(:start_line)
|
|
229
|
+
end_line = pos&.dig(:end_line)
|
|
230
|
+
|
|
231
|
+
if start_line && end_line
|
|
232
|
+
return analysis.source_range(start_line, end_line)
|
|
233
|
+
elsif node.respond_to?(:to_commonmark)
|
|
234
|
+
# Fallback to commonmark rendering
|
|
235
|
+
return node.to_commonmark
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
# Try direct start_line/end_line attributes
|
|
240
|
+
return unless node.respond_to?(:start_line) && node.respond_to?(:end_line)
|
|
241
|
+
return unless node.start_line && node.end_line
|
|
242
|
+
|
|
243
|
+
# Extract source range (formatting preservation handled elsewhere)
|
|
244
|
+
analysis.source_range(node.start_line, node.end_line)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|