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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +99 -482
- data/lib/markdown/merge/backend_support.rb +200 -0
- data/lib/markdown/merge/cleanse/block_spacing.rb +18 -23
- data/lib/markdown/merge/cleanse/code_fence_spacing.rb +16 -16
- data/lib/markdown/merge/cleanse/condensed_link_refs.rb +36 -30
- 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 +5 -3
- data/lib/markdown/merge/code_block_match_refiner.rb +111 -0
- data/lib/markdown/merge/code_block_merger.rb +489 -47
- data/lib/markdown/merge/comment_tracker.rb +42 -0
- data/lib/markdown/merge/conflict_resolver.rb +77 -6
- data/lib/markdown/merge/debug_logger.rb +2 -2
- data/lib/markdown/merge/document_problems.rb +3 -3
- data/lib/markdown/merge/file_aligner.rb +433 -133
- data/lib/markdown/merge/file_analysis.rb +387 -51
- data/lib/markdown/merge/file_analysis_base.rb +188 -51
- data/lib/markdown/merge/gap_line_node.rb +14 -8
- data/lib/markdown/merge/link_definition_node.rb +5 -5
- data/lib/markdown/merge/link_parser.rb +60 -60
- data/lib/markdown/merge/link_reference_rehydrator.rb +14 -14
- 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 +3 -3
- data/lib/markdown/merge/merge_result.rb +321 -4
- data/lib/markdown/merge/node_type_normalizer.rb +6 -6
- data/lib/markdown/merge/output_builder.rb +101 -19
- data/lib/markdown/merge/partial_template_merger.rb +248 -27
- data/lib/markdown/merge/preservation_support.rb +291 -0
- data/lib/markdown/merge/smart_merger.rb +62 -14
- data/lib/markdown/merge/smart_merger_base.rb +929 -60
- data/lib/markdown/merge/table_match_algorithm.rb +22 -27
- data/lib/markdown/merge/table_match_refiner.rb +6 -10
- data/lib/markdown/merge/version.rb +5 -4
- data/lib/markdown/merge/whitespace_normalizer.rb +25 -33
- data/lib/markdown/merge/wrapper_support.rb +194 -0
- data/lib/markdown/merge.rb +669 -122
- data/lib/markdown-merge.rb +9 -4
- data/sig/markdown/merge.rbs +3 -336
- data.tar.gz.sig +0 -0
- metadata +104 -93
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -308
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
4
|
-
require "set"
|
|
3
|
+
require 'digest'
|
|
5
4
|
|
|
6
5
|
module Markdown
|
|
7
6
|
module Merge
|
|
@@ -40,7 +39,7 @@ module Markdown
|
|
|
40
39
|
|
|
41
40
|
# Default freeze token for identifying freeze blocks
|
|
42
41
|
# @return [String]
|
|
43
|
-
DEFAULT_FREEZE_TOKEN =
|
|
42
|
+
DEFAULT_FREEZE_TOKEN = 'markdown-merge'
|
|
44
43
|
|
|
45
44
|
# @return [Object] The root document node
|
|
46
45
|
attr_reader :document
|
|
@@ -48,7 +47,10 @@ module Markdown
|
|
|
48
47
|
# @return [Array] Parse errors if any
|
|
49
48
|
attr_reader :errors
|
|
50
49
|
|
|
51
|
-
#
|
|
50
|
+
# @return [CommentTracker] Comment tracker for this file
|
|
51
|
+
attr_reader :comment_tracker
|
|
52
|
+
|
|
53
|
+
# NOTE: :source is inherited from Ast::Merge::FileAnalyzable
|
|
52
54
|
|
|
53
55
|
# Initialize file analysis
|
|
54
56
|
#
|
|
@@ -61,7 +63,8 @@ module Markdown
|
|
|
61
63
|
# But remove the final empty string if source ends with newline
|
|
62
64
|
# (that empty string represents the "line after the last newline" which doesn't exist)
|
|
63
65
|
@lines = source.split("\n", -1)
|
|
64
|
-
@lines.pop if @lines.last ==
|
|
66
|
+
@lines.pop if @lines.last == '' && source.end_with?("\n")
|
|
67
|
+
@comment_tracker = CommentTracker.new(@lines)
|
|
65
68
|
|
|
66
69
|
@freeze_token = freeze_token
|
|
67
70
|
@signature_generator = signature_generator
|
|
@@ -69,19 +72,19 @@ module Markdown
|
|
|
69
72
|
@errors = []
|
|
70
73
|
|
|
71
74
|
# Parse the Markdown source - subclasses implement this
|
|
72
|
-
@document = DebugLogger.time(
|
|
75
|
+
@document = DebugLogger.time('FileAnalysisBase#parse') do
|
|
73
76
|
parse_document(source)
|
|
74
77
|
end
|
|
75
78
|
|
|
76
79
|
# Extract and integrate all nodes including freeze blocks
|
|
77
80
|
@statements = extract_and_integrate_all_nodes
|
|
78
81
|
|
|
79
|
-
DebugLogger.debug(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
DebugLogger.debug('FileAnalysisBase initialized', {
|
|
83
|
+
signature_generator: signature_generator ? 'custom' : 'default',
|
|
84
|
+
document_children: count_children(@document),
|
|
85
|
+
statements_count: @statements.size,
|
|
86
|
+
freeze_blocks: freeze_blocks.size
|
|
87
|
+
})
|
|
85
88
|
end
|
|
86
89
|
|
|
87
90
|
# Parse the source document.
|
|
@@ -110,6 +113,106 @@ module Markdown
|
|
|
110
113
|
@errors.empty? && !@document.nil?
|
|
111
114
|
end
|
|
112
115
|
|
|
116
|
+
# Get shared comment capability information for this analysis.
|
|
117
|
+
#
|
|
118
|
+
# @return [Object]
|
|
119
|
+
def comment_capability
|
|
120
|
+
@comment_capability ||= comment_tracker.augment(owners: []).capability
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Describe how Markdown merges currently own and emit comments.
|
|
124
|
+
#
|
|
125
|
+
# Standalone HTML comments are source-augmented and emitted through the
|
|
126
|
+
# shared synthetic comment layer rather than parser-native comment AST.
|
|
127
|
+
#
|
|
128
|
+
# @return [Ast::Merge::Comment::SupportStyle]
|
|
129
|
+
def comment_support_style
|
|
130
|
+
@comment_support_style ||= shared_comment_support_style(
|
|
131
|
+
source: :markdown_source,
|
|
132
|
+
style: :html_comment,
|
|
133
|
+
read_strategy: :source_augmented_portable_write
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Get all tracked comments converted to shared comment nodes.
|
|
138
|
+
#
|
|
139
|
+
# @return [Array]
|
|
140
|
+
def comment_nodes
|
|
141
|
+
comment_tracker.comment_nodes
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Get a shared comment node at a specific line.
|
|
145
|
+
#
|
|
146
|
+
# @param line_num [Integer] 1-based line number
|
|
147
|
+
# @return [Object, nil]
|
|
148
|
+
def comment_node_at(line_num)
|
|
149
|
+
comment_tracker.comment_node_at(line_num)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Get comments in a line range converted to a shared comment region.
|
|
153
|
+
#
|
|
154
|
+
# @param range [Range] Range of 1-based line numbers
|
|
155
|
+
# @param kind [Symbol] Region kind
|
|
156
|
+
# @param full_line_only [Boolean] Whether to keep only full-line comments
|
|
157
|
+
# @return [Object]
|
|
158
|
+
def comment_region_for_range(range, kind:, full_line_only: false)
|
|
159
|
+
comment_tracker.comment_region_for_range(
|
|
160
|
+
range,
|
|
161
|
+
kind: kind,
|
|
162
|
+
full_line_only: full_line_only
|
|
163
|
+
)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Build a passive shared comment attachment for an owner.
|
|
167
|
+
#
|
|
168
|
+
# @param owner [Object] Structural owner for the attachment
|
|
169
|
+
# @param options [Hash] Additional metadata / lookup overrides
|
|
170
|
+
# @return [Object]
|
|
171
|
+
def comment_attachment_for(owner, **options)
|
|
172
|
+
augmented_attachment = comment_augmenter(**options).attachment_for(owner)
|
|
173
|
+
|
|
174
|
+
shared_comment_attachment_for(
|
|
175
|
+
owner,
|
|
176
|
+
tracker_attachment: augmented_attachment || comment_tracker.comment_attachment_for(owner, **options),
|
|
177
|
+
**options
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# @return [Symbol]
|
|
182
|
+
def comment_attachment_strategy
|
|
183
|
+
:normalize_tracked_layout_merge
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def ruleset_logical_owners
|
|
187
|
+
{
|
|
188
|
+
link_definition: :preserve_if_referenced
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ruleset_surfaces
|
|
193
|
+
[
|
|
194
|
+
{ name: :fenced_code_block, selector: :language_tag }
|
|
195
|
+
]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def ruleset_delegation_policies
|
|
199
|
+
[
|
|
200
|
+
{ surface_name: :fenced_code_block, strategy: :by_language }
|
|
201
|
+
]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Build a passive shared comment augmenter for this analysis.
|
|
205
|
+
#
|
|
206
|
+
# @param owners [Array, nil] Owners used for attachment inference
|
|
207
|
+
# @param options [Hash] Additional augmenter options
|
|
208
|
+
# @return [Object]
|
|
209
|
+
def comment_augmenter(owners: nil, **options)
|
|
210
|
+
comment_tracker.augment(
|
|
211
|
+
owners: owners || comment_augmenter_default_owners,
|
|
212
|
+
**options
|
|
213
|
+
)
|
|
214
|
+
end
|
|
215
|
+
|
|
113
216
|
# Get all statements (block nodes outside freeze blocks + FreezeNode instances)
|
|
114
217
|
# @return [Array<Object, FreezeNode>]
|
|
115
218
|
attr_reader :statements
|
|
@@ -159,8 +262,18 @@ module Markdown
|
|
|
159
262
|
type = node.type
|
|
160
263
|
case type
|
|
161
264
|
when :heading, :header
|
|
162
|
-
|
|
163
|
-
|
|
265
|
+
level = node.header_level
|
|
266
|
+
# H1 is the document title — treat as a singleton.
|
|
267
|
+
# A well-formed markdown document has exactly one H1. Matching by text
|
|
268
|
+
# would cause a generic template title ("AGENTS.md - Development Guide")
|
|
269
|
+
# and a project-qualified destination title ("AGENTS.md - myGem Development Guide")
|
|
270
|
+
# to be treated as different nodes, keeping both in the merged output.
|
|
271
|
+
# Using level-only for H1 makes them the same structural slot so the
|
|
272
|
+
# preferred version wins cleanly without duplication.
|
|
273
|
+
return [:heading, 1] if level == 1
|
|
274
|
+
|
|
275
|
+
# H2+ match by level and normalized text content
|
|
276
|
+
[:heading, level, extract_text_content(node)]
|
|
164
277
|
when :paragraph
|
|
165
278
|
# Content-based: Match paragraphs by content hash (first 32 chars of digest)
|
|
166
279
|
text = extract_text_content(node)
|
|
@@ -171,9 +284,20 @@ module Markdown
|
|
|
171
284
|
fence_info = node.respond_to?(:fence_info) ? node.fence_info : nil
|
|
172
285
|
[:code_block, fence_info, Digest::SHA256.hexdigest(content)[0, 16]]
|
|
173
286
|
when :list
|
|
174
|
-
#
|
|
287
|
+
# Content-fingerprint: Match lists by type and a hash of the first few
|
|
288
|
+
# items' significant tokens. This lets two lists with similar (but not
|
|
289
|
+
# identical) content match by signature so item-level inner-merge can run,
|
|
290
|
+
# rather than the template list being appended as a template-only node.
|
|
175
291
|
list_type = node.respond_to?(:list_type) ? node.list_type : nil
|
|
176
|
-
|
|
292
|
+
items_text = []
|
|
293
|
+
child = node.first_child
|
|
294
|
+
while child
|
|
295
|
+
items_text << extract_text_content(child).downcase.gsub(/\W+/, ' ').strip
|
|
296
|
+
child = next_sibling(child)
|
|
297
|
+
break if items_text.size >= 5
|
|
298
|
+
end
|
|
299
|
+
fingerprint = Digest::SHA256.hexdigest(items_text.sort.join('|'))[0, 16]
|
|
300
|
+
[:list, list_type, fingerprint]
|
|
177
301
|
when :block_quote, :blockquote
|
|
178
302
|
# Content-based: Match block quotes by content hash
|
|
179
303
|
text = extract_text_content(node)
|
|
@@ -238,10 +362,10 @@ module Markdown
|
|
|
238
362
|
# @param end_line [Integer] End line (1-indexed)
|
|
239
363
|
# @return [String] Source text
|
|
240
364
|
def source_range(start_line, end_line)
|
|
241
|
-
return
|
|
365
|
+
return '' if start_line < 1 || end_line < start_line
|
|
242
366
|
|
|
243
367
|
extracted_lines = @lines[(start_line - 1)..(end_line - 1)]
|
|
244
|
-
return
|
|
368
|
+
return '' if extracted_lines.empty?
|
|
245
369
|
|
|
246
370
|
# Add newlines between and after lines, but not after the last line of the file
|
|
247
371
|
# unless it originally had one
|
|
@@ -267,7 +391,7 @@ module Markdown
|
|
|
267
391
|
def extract_table_header_content(node)
|
|
268
392
|
# First row of a table is typically the header
|
|
269
393
|
first_row = node.first_child
|
|
270
|
-
return
|
|
394
|
+
return '' unless first_row
|
|
271
395
|
|
|
272
396
|
extract_text_content(first_row)
|
|
273
397
|
end
|
|
@@ -287,6 +411,22 @@ module Markdown
|
|
|
287
411
|
|
|
288
412
|
private
|
|
289
413
|
|
|
414
|
+
def comment_augmenter_default_owners
|
|
415
|
+
@comment_augmenter_default_owners ||= @statements.select do |statement|
|
|
416
|
+
statement.respond_to?(:source_position) && statement.source_position &&
|
|
417
|
+
(!statement.respond_to?(:merge_type) || statement.merge_type != :gap_line) &&
|
|
418
|
+
!standalone_comment_statement?(statement)
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def standalone_comment_statement?(statement)
|
|
423
|
+
pos = statement.respond_to?(:source_position) ? statement.source_position : nil
|
|
424
|
+
return false unless pos
|
|
425
|
+
return false unless pos[:start_line] && pos[:end_line] && pos[:start_line] == pos[:end_line]
|
|
426
|
+
|
|
427
|
+
comment_tracker.comment_node_at(pos[:start_line])
|
|
428
|
+
end
|
|
429
|
+
|
|
290
430
|
# Extract all nodes and integrate freeze blocks
|
|
291
431
|
# @return [Array<Object>] Integrated list of nodes and freeze blocks
|
|
292
432
|
def extract_and_integrate_all_nodes
|
|
@@ -367,15 +507,11 @@ module Markdown
|
|
|
367
507
|
# @return [Array<Object>] Gap nodes
|
|
368
508
|
def create_gap_nodes(line_numbers)
|
|
369
509
|
line_numbers.map do |line_num|
|
|
370
|
-
content = @lines[line_num - 1] ||
|
|
510
|
+
content = @lines[line_num - 1] || ''
|
|
371
511
|
|
|
372
512
|
# Try to parse as link definition first
|
|
373
513
|
link_node = LinkDefinitionNode.parse(content, line_number: line_num)
|
|
374
|
-
|
|
375
|
-
link_node
|
|
376
|
-
else
|
|
377
|
-
GapLineNode.new(content, line_number: line_num)
|
|
378
|
-
end
|
|
514
|
+
link_node || GapLineNode.new(content, line_number: line_num)
|
|
379
515
|
end
|
|
380
516
|
end
|
|
381
517
|
|
|
@@ -397,10 +533,17 @@ module Markdown
|
|
|
397
533
|
# Set preceding_node for gap lines based on their position in the sorted list
|
|
398
534
|
# This allows gap lines to have context-aware signatures
|
|
399
535
|
sorted_nodes.each_with_index do |node, idx|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
536
|
+
next unless node.is_a?(GapLineNode) && idx > 0
|
|
537
|
+
|
|
538
|
+
# Find the previous non-gap-line node (structural node)
|
|
539
|
+
preceding = sorted_nodes[0...idx].reverse.find { |n| !n.is_a?(GapLineNode) }
|
|
540
|
+
node.preceding_node = preceding
|
|
541
|
+
next unless preceding
|
|
542
|
+
|
|
543
|
+
node.preceding_signature = begin
|
|
544
|
+
compute_node_signature(preceding)
|
|
545
|
+
rescue StandardError
|
|
546
|
+
nil
|
|
404
547
|
end
|
|
405
548
|
end
|
|
406
549
|
|
|
@@ -432,7 +575,7 @@ module Markdown
|
|
|
432
575
|
|
|
433
576
|
# Check HTML nodes for freeze markers
|
|
434
577
|
# Handle both raw Markly (:html) and TreeHaver normalized ("html_block", :html_block) types
|
|
435
|
-
if
|
|
578
|
+
if [:html, :html_block, 'html_block', 'html'].include?(node_type)
|
|
436
579
|
# Try multiple content extraction methods:
|
|
437
580
|
# 1. string_content (raw Markly/Commonmarker)
|
|
438
581
|
# 2. to_commonmark on wrapper
|
|
@@ -448,21 +591,15 @@ module Markdown
|
|
|
448
591
|
end
|
|
449
592
|
end
|
|
450
593
|
|
|
451
|
-
if content.nil? || content.empty?
|
|
452
|
-
if child.respond_to?(:to_commonmark)
|
|
453
|
-
content = child.to_commonmark.to_s
|
|
454
|
-
end
|
|
455
|
-
end
|
|
594
|
+
content = child.to_commonmark.to_s if (content.nil? || content.empty?) && child.respond_to?(:to_commonmark)
|
|
456
595
|
|
|
457
596
|
# TreeHaver Commonmarker wrapper stores content in inner_node
|
|
458
597
|
if (content.nil? || content.empty?) && child.respond_to?(:inner_node)
|
|
459
598
|
inner = child.inner_node
|
|
460
|
-
if inner.respond_to?(:to_commonmark)
|
|
461
|
-
content = inner.to_commonmark.to_s
|
|
462
|
-
end
|
|
599
|
+
content = inner.to_commonmark.to_s if inner.respond_to?(:to_commonmark)
|
|
463
600
|
end
|
|
464
601
|
|
|
465
|
-
content ||=
|
|
602
|
+
content ||= ''
|
|
466
603
|
match = content.match(pattern)
|
|
467
604
|
|
|
468
605
|
if match
|
|
@@ -475,7 +612,7 @@ module Markdown
|
|
|
475
612
|
type: marker_type.to_sym,
|
|
476
613
|
text: content.strip,
|
|
477
614
|
reason: reason,
|
|
478
|
-
node: child
|
|
615
|
+
node: child # Keep reference to the actual node
|
|
479
616
|
}
|
|
480
617
|
end
|
|
481
618
|
end
|
|
@@ -483,7 +620,7 @@ module Markdown
|
|
|
483
620
|
child = next_sibling(child)
|
|
484
621
|
end
|
|
485
622
|
|
|
486
|
-
DebugLogger.debug(
|
|
623
|
+
DebugLogger.debug('Found freeze markers', { count: markers.size })
|
|
487
624
|
markers
|
|
488
625
|
end
|
|
489
626
|
|
|
@@ -503,14 +640,14 @@ module Markdown
|
|
|
503
640
|
start_marker = stack.pop
|
|
504
641
|
blocks << create_freeze_block(start_marker, marker)
|
|
505
642
|
else
|
|
506
|
-
DebugLogger.debug(
|
|
643
|
+
DebugLogger.debug('Unmatched unfreeze marker', { line: marker[:line] })
|
|
507
644
|
end
|
|
508
645
|
end
|
|
509
646
|
end
|
|
510
647
|
|
|
511
648
|
# Warn about unclosed freeze blocks
|
|
512
649
|
stack.each do |unclosed|
|
|
513
|
-
DebugLogger.debug(
|
|
650
|
+
DebugLogger.debug('Unclosed freeze marker', { line: unclosed[:line] })
|
|
514
651
|
end
|
|
515
652
|
|
|
516
653
|
blocks.sort_by(&:start_line)
|
|
@@ -532,10 +669,10 @@ module Markdown
|
|
|
532
669
|
content_end = end_line - 1
|
|
533
670
|
|
|
534
671
|
content = if content_start <= content_end
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
672
|
+
source_range(content_start, content_end)
|
|
673
|
+
else
|
|
674
|
+
''
|
|
675
|
+
end
|
|
539
676
|
|
|
540
677
|
# Parse the content to get nodes (for nested analysis)
|
|
541
678
|
parsed_nodes = parse_freeze_block_content(content)
|
|
@@ -547,7 +684,7 @@ module Markdown
|
|
|
547
684
|
start_marker: start_marker[:text],
|
|
548
685
|
end_marker: end_marker[:text],
|
|
549
686
|
nodes: parsed_nodes,
|
|
550
|
-
reason: start_marker[:reason]
|
|
687
|
+
reason: start_marker[:reason]
|
|
551
688
|
)
|
|
552
689
|
end
|
|
553
690
|
|
|
@@ -579,10 +716,10 @@ module Markdown
|
|
|
579
716
|
end
|
|
580
717
|
nodes
|
|
581
718
|
rescue StandardError => e
|
|
582
|
-
# :
|
|
583
|
-
DebugLogger.debug(
|
|
719
|
+
# simplecov:disable defensive - parser rarely fails on valid markdown subset
|
|
720
|
+
DebugLogger.debug('Failed to parse freeze block content', { error: e.message })
|
|
584
721
|
[]
|
|
585
|
-
# :
|
|
722
|
+
# simplecov:enable
|
|
586
723
|
end
|
|
587
724
|
end
|
|
588
725
|
|
|
@@ -30,6 +30,9 @@ module Markdown
|
|
|
30
30
|
# This is set after integration to avoid circular dependencies during creation
|
|
31
31
|
attr_accessor :preceding_node
|
|
32
32
|
|
|
33
|
+
# @return [Array, nil] Signature of the preceding structural node when available
|
|
34
|
+
attr_accessor :preceding_signature
|
|
35
|
+
|
|
33
36
|
# Initialize a new GapLineNode
|
|
34
37
|
#
|
|
35
38
|
# @param content [String] The line content (without trailing newline)
|
|
@@ -37,13 +40,14 @@ module Markdown
|
|
|
37
40
|
def initialize(content, line_number:)
|
|
38
41
|
@content = content.chomp
|
|
39
42
|
@line_number = line_number
|
|
40
|
-
@preceding_node = nil
|
|
43
|
+
@preceding_node = nil # Set later during integration
|
|
44
|
+
@preceding_signature = nil
|
|
41
45
|
|
|
42
46
|
location = Ast::Merge::AstNode::Location.new(
|
|
43
47
|
start_line: line_number,
|
|
44
48
|
end_line: line_number,
|
|
45
49
|
start_column: 0,
|
|
46
|
-
end_column: @content.length
|
|
50
|
+
end_column: @content.length
|
|
47
51
|
)
|
|
48
52
|
|
|
49
53
|
super(slice: @content, location: location)
|
|
@@ -57,7 +61,7 @@ module Markdown
|
|
|
57
61
|
|
|
58
62
|
# Alias for compatibility with wrapped nodes that have merge_type
|
|
59
63
|
# @return [Symbol] :gap_line
|
|
60
|
-
|
|
64
|
+
alias merge_type type
|
|
61
65
|
|
|
62
66
|
# Generate a signature for matching gap lines.
|
|
63
67
|
# Gap lines are matched by their position relative to the preceding structural node.
|
|
@@ -77,11 +81,13 @@ module Markdown
|
|
|
77
81
|
# Offset from preceding node's end (e.g., heading ends on line 1, gap is line 2, offset = 1)
|
|
78
82
|
offset = @line_number - preceding_end_line
|
|
79
83
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
84
|
+
context_signature = @preceding_signature || if @preceding_node.respond_to?(:type)
|
|
85
|
+
@preceding_node.type
|
|
86
|
+
else
|
|
87
|
+
:unknown
|
|
88
|
+
end
|
|
83
89
|
|
|
84
|
-
[:gap_line_after,
|
|
90
|
+
[:gap_line_after, context_signature, offset, @content]
|
|
85
91
|
else
|
|
86
92
|
# Fallback if we can't get position
|
|
87
93
|
[:gap_line, @line_number, @content]
|
|
@@ -99,7 +105,7 @@ module Markdown
|
|
|
99
105
|
start_line: @line_number,
|
|
100
106
|
end_line: @line_number,
|
|
101
107
|
start_column: 0,
|
|
102
|
-
end_column: @content.length
|
|
108
|
+
end_column: @content.length
|
|
103
109
|
}
|
|
104
110
|
end
|
|
105
111
|
|
|
@@ -61,7 +61,7 @@ module Markdown
|
|
|
61
61
|
start_line: line_number,
|
|
62
62
|
end_line: line_number,
|
|
63
63
|
start_column: 0,
|
|
64
|
-
end_column: content.length
|
|
64
|
+
end_column: content.length
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
super(slice: content, location: location)
|
|
@@ -71,7 +71,7 @@ module Markdown
|
|
|
71
71
|
# Shared parser instance for parsing link definitions
|
|
72
72
|
# @return [LinkParser]
|
|
73
73
|
def parser
|
|
74
|
-
@parser ||= LinkParser.new
|
|
74
|
+
@parser ||= LinkParser.new
|
|
75
75
|
end
|
|
76
76
|
|
|
77
77
|
# Parse a line and create a LinkDefinitionNode if it's a link definition.
|
|
@@ -88,7 +88,7 @@ module Markdown
|
|
|
88
88
|
line_number: line_number,
|
|
89
89
|
label: result[:label],
|
|
90
90
|
url: result[:url],
|
|
91
|
-
title: result[:title]
|
|
91
|
+
title: result[:title]
|
|
92
92
|
)
|
|
93
93
|
end
|
|
94
94
|
|
|
@@ -109,7 +109,7 @@ module Markdown
|
|
|
109
109
|
|
|
110
110
|
# Alias for compatibility with wrapped nodes that have merge_type
|
|
111
111
|
# @return [Symbol] :link_definition
|
|
112
|
-
|
|
112
|
+
alias merge_type type
|
|
113
113
|
|
|
114
114
|
# Generate a signature for matching link definitions.
|
|
115
115
|
# Link definitions are matched by their label (case-insensitive in Markdown).
|
|
@@ -126,7 +126,7 @@ module Markdown
|
|
|
126
126
|
start_line: @location.start_line,
|
|
127
127
|
end_line: @location.end_line,
|
|
128
128
|
start_column: @location.start_column,
|
|
129
|
-
end_column: @location.end_column
|
|
129
|
+
end_column: @location.end_column
|
|
130
130
|
}
|
|
131
131
|
end
|
|
132
132
|
|