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,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require
|
|
3
|
+
require 'parslet'
|
|
4
4
|
|
|
5
5
|
module Markdown
|
|
6
6
|
module Merge
|
|
@@ -35,33 +35,33 @@ module Markdown
|
|
|
35
35
|
rule(:spaces?) { space.repeat }
|
|
36
36
|
|
|
37
37
|
# Bracket content: handles nested brackets recursively
|
|
38
|
-
rule(:bracket_content)
|
|
38
|
+
rule(:bracket_content) do
|
|
39
39
|
(
|
|
40
|
-
str(
|
|
41
|
-
str(
|
|
40
|
+
str('[') >> bracket_content.maybe >> str(']') |
|
|
41
|
+
str(']').absent? >> any
|
|
42
42
|
).repeat
|
|
43
|
-
|
|
43
|
+
end
|
|
44
44
|
|
|
45
|
-
rule(:label) { str(
|
|
45
|
+
rule(:label) { str('[') >> bracket_content.as(:label) >> str(']') }
|
|
46
46
|
|
|
47
47
|
rule(:url_char) { match('[^\s>]') }
|
|
48
48
|
rule(:bare_url) { url_char.repeat(1) }
|
|
49
|
-
rule(:angled_url_char) { match(
|
|
50
|
-
rule(:angled_url) { str(
|
|
49
|
+
rule(:angled_url_char) { match('[^>]') }
|
|
50
|
+
rule(:angled_url) { str('<') >> angled_url_char.repeat(1) >> str('>') }
|
|
51
51
|
rule(:url) { (angled_url | bare_url).as(:url) }
|
|
52
52
|
|
|
53
53
|
rule(:title_content_double) { (str('"').absent? >> any).repeat }
|
|
54
54
|
rule(:title_content_single) { (str("'").absent? >> any).repeat }
|
|
55
|
-
rule(:title_content_paren) { (str(
|
|
55
|
+
rule(:title_content_paren) { (str(')').absent? >> any).repeat }
|
|
56
56
|
|
|
57
57
|
rule(:title_double) { str('"') >> title_content_double.as(:title) >> str('"') }
|
|
58
58
|
rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
|
|
59
|
-
rule(:title_paren) { str(
|
|
59
|
+
rule(:title_paren) { str('(') >> title_content_paren.as(:title) >> str(')') }
|
|
60
60
|
rule(:title) { title_double | title_single | title_paren }
|
|
61
61
|
|
|
62
|
-
rule(:definition)
|
|
63
|
-
spaces? >> label >> str(
|
|
64
|
-
|
|
62
|
+
rule(:definition) do
|
|
63
|
+
spaces? >> label >> str(':') >> spaces? >> url >> (spaces >> title).maybe >> spaces?
|
|
64
|
+
end
|
|
65
65
|
|
|
66
66
|
root(:definition)
|
|
67
67
|
end
|
|
@@ -72,22 +72,22 @@ module Markdown
|
|
|
72
72
|
rule(:spaces) { space.repeat(1) }
|
|
73
73
|
|
|
74
74
|
# Bracket content with recursive nesting
|
|
75
|
-
rule(:bracket_content)
|
|
75
|
+
rule(:bracket_content) do
|
|
76
76
|
(
|
|
77
|
-
str(
|
|
78
|
-
str(
|
|
77
|
+
str('[') >> bracket_content.maybe >> str(']') |
|
|
78
|
+
str(']').absent? >> any
|
|
79
79
|
).repeat
|
|
80
|
-
|
|
80
|
+
end
|
|
81
81
|
|
|
82
|
-
rule(:link_text) { str(
|
|
82
|
+
rule(:link_text) { str('[') >> bracket_content.as(:text) >> str(']') }
|
|
83
83
|
|
|
84
84
|
# URL content - handles balanced parens inside URLs
|
|
85
|
-
rule(:paren_content)
|
|
85
|
+
rule(:paren_content) do
|
|
86
86
|
(
|
|
87
|
-
str(
|
|
87
|
+
str('(') >> paren_content.maybe >> str(')') |
|
|
88
88
|
match('[^()\s"\']')
|
|
89
89
|
).repeat
|
|
90
|
-
|
|
90
|
+
end
|
|
91
91
|
|
|
92
92
|
rule(:url) { paren_content.as(:url) }
|
|
93
93
|
|
|
@@ -97,7 +97,7 @@ module Markdown
|
|
|
97
97
|
rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
|
|
98
98
|
rule(:title) { title_double | title_single }
|
|
99
99
|
|
|
100
|
-
rule(:url_part) { str(
|
|
100
|
+
rule(:url_part) { str('(') >> url >> (spaces >> title).maybe >> str(')') }
|
|
101
101
|
|
|
102
102
|
rule(:inline_link) { link_text >> url_part }
|
|
103
103
|
|
|
@@ -109,21 +109,21 @@ module Markdown
|
|
|
109
109
|
rule(:space) { match('[ \t]') }
|
|
110
110
|
rule(:spaces) { space.repeat(1) }
|
|
111
111
|
|
|
112
|
-
rule(:bracket_content)
|
|
112
|
+
rule(:bracket_content) do
|
|
113
113
|
(
|
|
114
|
-
str(
|
|
115
|
-
str(
|
|
114
|
+
str('[') >> bracket_content.maybe >> str(']') |
|
|
115
|
+
str(']').absent? >> any
|
|
116
116
|
).repeat
|
|
117
|
-
|
|
117
|
+
end
|
|
118
118
|
|
|
119
|
-
rule(:alt_text) { str(
|
|
119
|
+
rule(:alt_text) { str('![') >> bracket_content.as(:alt) >> str(']') }
|
|
120
120
|
|
|
121
|
-
rule(:paren_content)
|
|
121
|
+
rule(:paren_content) do
|
|
122
122
|
(
|
|
123
|
-
str(
|
|
123
|
+
str('(') >> paren_content.maybe >> str(')') |
|
|
124
124
|
match('[^()\s"\']')
|
|
125
125
|
).repeat
|
|
126
|
-
|
|
126
|
+
end
|
|
127
127
|
|
|
128
128
|
rule(:url) { paren_content.as(:url) }
|
|
129
129
|
|
|
@@ -133,7 +133,7 @@ module Markdown
|
|
|
133
133
|
rule(:title_single) { str("'") >> title_content_single.as(:title) >> str("'") }
|
|
134
134
|
rule(:title) { title_double | title_single }
|
|
135
135
|
|
|
136
|
-
rule(:url_part) { str(
|
|
136
|
+
rule(:url_part) { str('(') >> url >> (spaces >> title).maybe >> str(')') }
|
|
137
137
|
|
|
138
138
|
rule(:inline_image) { alt_text >> url_part }
|
|
139
139
|
|
|
@@ -170,11 +170,11 @@ module Markdown
|
|
|
170
170
|
|
|
171
171
|
url = result[:url].to_s
|
|
172
172
|
# Strip angle brackets if present
|
|
173
|
-
url = url[1..-2] if url.start_with?(
|
|
173
|
+
url = url[1..-2] if url.start_with?('<') && url.end_with?('>')
|
|
174
174
|
|
|
175
175
|
definition = {
|
|
176
176
|
label: result[:label].to_s,
|
|
177
|
-
url: url
|
|
177
|
+
url: url
|
|
178
178
|
}
|
|
179
179
|
definition[:title] = result[:title].to_s if result[:title]
|
|
180
180
|
definition
|
|
@@ -239,7 +239,7 @@ module Markdown
|
|
|
239
239
|
def build_link_tree(links, images)
|
|
240
240
|
# Combine all items
|
|
241
241
|
all_items = links.map { |l| l.merge(type: :link) } +
|
|
242
|
-
|
|
242
|
+
images.map { |i| i.merge(type: :image) }
|
|
243
243
|
|
|
244
244
|
# Sort by start position
|
|
245
245
|
sorted = all_items.sort_by { |item| item[:start_pos] }
|
|
@@ -297,15 +297,15 @@ module Markdown
|
|
|
297
297
|
def find_constructs(content, type)
|
|
298
298
|
results = []
|
|
299
299
|
pos = 0
|
|
300
|
-
grammar =
|
|
301
|
-
start_marker =
|
|
300
|
+
grammar = type == :image ? @image_grammar : @link_grammar
|
|
301
|
+
start_marker = type == :image ? '![' : '['
|
|
302
302
|
|
|
303
303
|
while pos < content.length
|
|
304
304
|
idx = content.index(start_marker, pos)
|
|
305
305
|
break unless idx
|
|
306
306
|
|
|
307
307
|
# For links, skip if preceded by ! (that's an image)
|
|
308
|
-
if type == :link && idx > 0 && content[idx - 1] ==
|
|
308
|
+
if type == :link && idx > 0 && content[idx - 1] == '!'
|
|
309
309
|
pos = idx + 1
|
|
310
310
|
next
|
|
311
311
|
end
|
|
@@ -327,12 +327,12 @@ module Markdown
|
|
|
327
327
|
remaining = content[start_idx..]
|
|
328
328
|
|
|
329
329
|
# Find the closing ) by tracking balanced brackets/parens
|
|
330
|
-
bracket_end = find_bracket_end(remaining,
|
|
330
|
+
bracket_end = find_bracket_end(remaining, type == :image ? 1 : 0)
|
|
331
331
|
return unless bracket_end
|
|
332
332
|
|
|
333
333
|
# Check for ( after ]
|
|
334
334
|
return if bracket_end + 1 >= remaining.length
|
|
335
|
-
return unless remaining[bracket_end + 1] ==
|
|
335
|
+
return unless remaining[bracket_end + 1] == '('
|
|
336
336
|
|
|
337
337
|
paren_end = find_paren_end(remaining, bracket_end + 1)
|
|
338
338
|
return unless paren_end
|
|
@@ -344,22 +344,22 @@ module Markdown
|
|
|
344
344
|
result = grammar.parse(substring)
|
|
345
345
|
|
|
346
346
|
parsed = if type == :image
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
347
|
+
{
|
|
348
|
+
alt: result[:alt].to_s,
|
|
349
|
+
url: result[:url].to_s,
|
|
350
|
+
start_pos: start_idx,
|
|
351
|
+
end_pos: start_idx + substring.length,
|
|
352
|
+
original: substring
|
|
353
|
+
}
|
|
354
|
+
else
|
|
355
|
+
{
|
|
356
|
+
text: result[:text].to_s,
|
|
357
|
+
url: result[:url].to_s,
|
|
358
|
+
start_pos: start_idx,
|
|
359
|
+
end_pos: start_idx + substring.length,
|
|
360
|
+
original: substring
|
|
361
|
+
}
|
|
362
|
+
end
|
|
363
363
|
|
|
364
364
|
parsed[:title] = result[:title].to_s if result[:title]
|
|
365
365
|
parsed
|
|
@@ -374,9 +374,9 @@ module Markdown
|
|
|
374
374
|
|
|
375
375
|
while pos < text.length
|
|
376
376
|
case text[pos]
|
|
377
|
-
when
|
|
377
|
+
when '['
|
|
378
378
|
depth += 1
|
|
379
|
-
when
|
|
379
|
+
when ']'
|
|
380
380
|
depth -= 1
|
|
381
381
|
return pos if depth == 0
|
|
382
382
|
end
|
|
@@ -395,7 +395,7 @@ module Markdown
|
|
|
395
395
|
while pos < text.length
|
|
396
396
|
char = text[pos]
|
|
397
397
|
|
|
398
|
-
if !in_quotes &&
|
|
398
|
+
if !in_quotes && ['"', "'"].include?(char)
|
|
399
399
|
in_quotes = true
|
|
400
400
|
quote_char = char
|
|
401
401
|
elsif in_quotes && char == quote_char
|
|
@@ -403,9 +403,9 @@ module Markdown
|
|
|
403
403
|
quote_char = nil
|
|
404
404
|
elsif !in_quotes
|
|
405
405
|
case char
|
|
406
|
-
when
|
|
406
|
+
when '('
|
|
407
407
|
depth += 1
|
|
408
|
-
when
|
|
408
|
+
when ')'
|
|
409
409
|
depth -= 1
|
|
410
410
|
return pos if depth == 0
|
|
411
411
|
end
|
|
@@ -105,8 +105,8 @@ module Markdown
|
|
|
105
105
|
result = content.dup
|
|
106
106
|
replacements.sort_by { |r| -r[:start_pos] }.each do |replacement|
|
|
107
107
|
result = result[0...replacement[:start_pos]] +
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
replacement[:replacement] +
|
|
109
|
+
result[replacement[:end_pos]..]
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
result
|
|
@@ -158,10 +158,10 @@ module Markdown
|
|
|
158
158
|
else
|
|
159
159
|
# Leaf node - process directly
|
|
160
160
|
replacement = if item[:type] == :image
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
process_image(item)
|
|
162
|
+
else
|
|
163
|
+
process_link(item)
|
|
164
|
+
end
|
|
165
165
|
replacements << replacement if replacement
|
|
166
166
|
end
|
|
167
167
|
end
|
|
@@ -190,14 +190,14 @@ module Markdown
|
|
|
190
190
|
severity: :info,
|
|
191
191
|
text: item[:text],
|
|
192
192
|
url: item[:url],
|
|
193
|
-
title: item[:title]
|
|
193
|
+
title: item[:title]
|
|
194
194
|
)
|
|
195
195
|
return
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
# Build the new link text by applying child replacements to the original text
|
|
199
199
|
# Extract the original "text" part of the link (between [ and ])
|
|
200
|
-
original_text = item[:text] ||
|
|
200
|
+
original_text = item[:text] || ''
|
|
201
201
|
|
|
202
202
|
# Apply child replacements to build the new text content
|
|
203
203
|
# Children positions are relative to the document, so we need to adjust
|
|
@@ -223,7 +223,7 @@ module Markdown
|
|
|
223
223
|
{
|
|
224
224
|
start_pos: item[:start_pos],
|
|
225
225
|
end_pos: item[:end_pos],
|
|
226
|
-
replacement: "[#{new_text}][#{label}]"
|
|
226
|
+
replacement: "[#{new_text}][#{label}]"
|
|
227
227
|
}
|
|
228
228
|
end
|
|
229
229
|
|
|
@@ -257,7 +257,7 @@ module Markdown
|
|
|
257
257
|
severity: :warning,
|
|
258
258
|
url: url,
|
|
259
259
|
labels: labels,
|
|
260
|
-
selected_label: @url_to_label[url]
|
|
260
|
+
selected_label: @url_to_label[url]
|
|
261
261
|
)
|
|
262
262
|
end
|
|
263
263
|
end
|
|
@@ -273,7 +273,7 @@ module Markdown
|
|
|
273
273
|
severity: :info,
|
|
274
274
|
text: link_text,
|
|
275
275
|
url: url,
|
|
276
|
-
title: title
|
|
276
|
+
title: title
|
|
277
277
|
)
|
|
278
278
|
return
|
|
279
279
|
end
|
|
@@ -285,7 +285,7 @@ module Markdown
|
|
|
285
285
|
{
|
|
286
286
|
start_pos: link[:start_pos],
|
|
287
287
|
end_pos: link[:end_pos],
|
|
288
|
-
replacement: "[#{link_text}][#{label}]"
|
|
288
|
+
replacement: "[#{link_text}][#{label}]"
|
|
289
289
|
}
|
|
290
290
|
end
|
|
291
291
|
|
|
@@ -300,7 +300,7 @@ module Markdown
|
|
|
300
300
|
severity: :info,
|
|
301
301
|
alt: alt_text,
|
|
302
302
|
url: url,
|
|
303
|
-
title: title
|
|
303
|
+
title: title
|
|
304
304
|
)
|
|
305
305
|
return
|
|
306
306
|
end
|
|
@@ -312,7 +312,7 @@ module Markdown
|
|
|
312
312
|
{
|
|
313
313
|
start_pos: image[:start_pos],
|
|
314
314
|
end_pos: image[:end_pos],
|
|
315
|
-
replacement: "![#{alt_text}][#{label}]"
|
|
315
|
+
replacement: "![#{alt_text}][#{label}]"
|
|
316
316
|
}
|
|
317
317
|
end
|
|
318
318
|
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Fuzzy matches markdown list nodes by item-token overlap so inner list merging
|
|
6
|
+
# can repair previously-corrupted lists that no longer share an exact signature.
|
|
7
|
+
class ListMatchRefiner < Ast::Merge::MatchRefinerBase
|
|
8
|
+
include Ast::Merge::JaccardSimilarity
|
|
9
|
+
|
|
10
|
+
DEFAULT_THRESHOLD = 0.45
|
|
11
|
+
|
|
12
|
+
def initialize(threshold: DEFAULT_THRESHOLD, **options)
|
|
13
|
+
super(threshold: threshold, node_types: [:list], **options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(template_nodes, dest_nodes, context = {})
|
|
17
|
+
template_lists = template_nodes.select { |node| node_type(node).to_s == 'list' }
|
|
18
|
+
dest_lists = dest_nodes.select { |node| node_type(node).to_s == 'list' }
|
|
19
|
+
return [] if template_lists.empty? || dest_lists.empty?
|
|
20
|
+
|
|
21
|
+
greedy_match(template_lists, dest_lists) 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
|
+
template_items = list_item_anchors(template_node)
|
|
30
|
+
dest_items = list_item_anchors(dest_node)
|
|
31
|
+
return 0.0 if template_items.empty? || dest_items.empty?
|
|
32
|
+
|
|
33
|
+
containment = template_item_containment(template_items, dest_items)
|
|
34
|
+
token_overlap = jaccard(list_tokens(template_node), list_tokens(dest_node))
|
|
35
|
+
first_item_score = template_items.first == dest_items.first ? 1.0 : 0.0
|
|
36
|
+
context_score = context_similarity(
|
|
37
|
+
template_node,
|
|
38
|
+
context[:template_analysis],
|
|
39
|
+
dest_node,
|
|
40
|
+
context[:dest_analysis]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
(containment * 0.35) + (token_overlap * 0.35) + (context_score * 0.2) + (first_item_score * 0.1)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def list_item_anchors(list_node)
|
|
47
|
+
raw = Ast::Merge::NodeTyping.unwrap(list_node)
|
|
48
|
+
|
|
49
|
+
raw.each_with_object([]) do |child, anchors|
|
|
50
|
+
next unless child.respond_to?(:type) && %w[list_item item].include?(child.type.to_s)
|
|
51
|
+
|
|
52
|
+
anchors << normalize_anchor(child.text.to_s)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def list_tokens(list_node)
|
|
57
|
+
extract_tokens(list_node.text.to_s)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def template_item_containment(template_items, dest_items)
|
|
61
|
+
template_set = template_items.to_set
|
|
62
|
+
dest_set = dest_items.to_set
|
|
63
|
+
return 0.0 if template_set.empty?
|
|
64
|
+
|
|
65
|
+
(template_set & dest_set).size.to_f / template_set.size
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def context_similarity(template_node, template_analysis, dest_node, dest_analysis)
|
|
69
|
+
template_context = preceding_context_text(template_node, template_analysis)
|
|
70
|
+
dest_context = preceding_context_text(dest_node, dest_analysis)
|
|
71
|
+
return 0.0 if template_context.empty? || dest_context.empty?
|
|
72
|
+
|
|
73
|
+
jaccard(extract_tokens(template_context), extract_tokens(dest_context))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def preceding_context_text(node, analysis)
|
|
77
|
+
return '' unless analysis
|
|
78
|
+
|
|
79
|
+
index = analysis.statements.index(node)
|
|
80
|
+
return '' unless index
|
|
81
|
+
|
|
82
|
+
(index - 1).downto(0) do |current_index|
|
|
83
|
+
candidate = analysis.statements[current_index]
|
|
84
|
+
signature = analysis.signature_at(current_index)
|
|
85
|
+
next unless signature.is_a?(Array) && %i[heading paragraph code_block].include?(signature.first)
|
|
86
|
+
|
|
87
|
+
return candidate.text.to_s
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
''
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def normalize_anchor(text)
|
|
94
|
+
text.to_s.strip.gsub(/\s+/, ' ').downcase
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|