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,496 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Markdown
|
|
4
|
+
module Merge
|
|
5
|
+
# Aligns Markdown block elements between template and destination files.
|
|
6
|
+
#
|
|
7
|
+
# Uses structural signatures to match headings, paragraphs, lists, code blocks,
|
|
8
|
+
# and other block elements. The alignment is then used by SmartMerger to
|
|
9
|
+
# determine how to combine the files.
|
|
10
|
+
#
|
|
11
|
+
# @example Basic usage
|
|
12
|
+
# aligner = FileAligner.new(template_analysis, dest_analysis)
|
|
13
|
+
# alignment = aligner.align
|
|
14
|
+
# alignment.each do |entry|
|
|
15
|
+
# case entry[:type]
|
|
16
|
+
# when :match
|
|
17
|
+
# # Both files have this element
|
|
18
|
+
# when :template_only
|
|
19
|
+
# # Only in template
|
|
20
|
+
# when :dest_only
|
|
21
|
+
# # Only in destination
|
|
22
|
+
# end
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
# @see FileAnalysisBase
|
|
26
|
+
# @see SmartMergerBase
|
|
27
|
+
class FileAligner < ::Ast::Merge::FileAlignerBase
|
|
28
|
+
# @return [FileAnalysisBase] Template file analysis
|
|
29
|
+
attr_reader :template_analysis
|
|
30
|
+
|
|
31
|
+
# @return [FileAnalysisBase] Destination file analysis
|
|
32
|
+
attr_reader :dest_analysis
|
|
33
|
+
|
|
34
|
+
# @return [#call, nil] Optional match refiner for fuzzy matching
|
|
35
|
+
attr_reader :match_refiner
|
|
36
|
+
|
|
37
|
+
# Initialize a file aligner
|
|
38
|
+
#
|
|
39
|
+
# @param template_analysis [FileAnalysisBase] Analysis of the template file
|
|
40
|
+
# @param dest_analysis [FileAnalysisBase] Analysis of the destination file
|
|
41
|
+
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
|
|
42
|
+
def initialize(template_analysis, dest_analysis, match_refiner: nil, **options)
|
|
43
|
+
super(template_analysis, dest_analysis, match_refiner: match_refiner, **options)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def signature_for(analysis, index)
|
|
49
|
+
signature = analysis.signature_at(index)
|
|
50
|
+
return signature if signature.nil?
|
|
51
|
+
|
|
52
|
+
signature = contextual_list_signature(analysis, index, signature) if list_signature?(signature)
|
|
53
|
+
|
|
54
|
+
contextualize_signature_with_heading_owner_path(analysis, index, signature)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def apply_match_refiner!(alignment, template_statements:, dest_statements:, matched_template:, matched_dest:)
|
|
58
|
+
return unless match_refiner
|
|
59
|
+
|
|
60
|
+
unmatched_template = template_statements.each_with_index.reject do |_, i|
|
|
61
|
+
matched_template.include?(i)
|
|
62
|
+
end.map(&:first)
|
|
63
|
+
unmatched_dest = dest_statements.each_with_index.reject { |_, i| matched_dest.include?(i) }.map(&:first)
|
|
64
|
+
return if unmatched_template.empty? || unmatched_dest.empty?
|
|
65
|
+
|
|
66
|
+
refined_matches = match_refiner.call(
|
|
67
|
+
unmatched_template,
|
|
68
|
+
unmatched_dest,
|
|
69
|
+
{
|
|
70
|
+
template_analysis: template_analysis,
|
|
71
|
+
dest_analysis: dest_analysis
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
Array(refined_matches).each do |match|
|
|
76
|
+
template_statement = refined_match_template_statement(match)
|
|
77
|
+
dest_statement = refined_match_dest_statement(match)
|
|
78
|
+
template_index = template_statements.index(template_statement)
|
|
79
|
+
dest_index = dest_statements.index(dest_statement)
|
|
80
|
+
|
|
81
|
+
next unless template_index && dest_index
|
|
82
|
+
next if matched_template.include?(template_index) || matched_dest.include?(dest_index)
|
|
83
|
+
next unless refined_match_heading_owner_compatible?(template_index, dest_index)
|
|
84
|
+
|
|
85
|
+
entry = build_match_entry(
|
|
86
|
+
signature: refined_match_signature(match),
|
|
87
|
+
template_index: template_index,
|
|
88
|
+
dest_index: dest_index,
|
|
89
|
+
template_statement: template_statement,
|
|
90
|
+
dest_statement: dest_statement
|
|
91
|
+
)
|
|
92
|
+
repair_dest_index = adjacent_section_hijack_repair_sort_dest_index(template_index, dest_index)
|
|
93
|
+
entry[:repair_dest_index] = repair_dest_index if repair_dest_index
|
|
94
|
+
alignment << entry
|
|
95
|
+
|
|
96
|
+
matched_template << template_index
|
|
97
|
+
matched_dest << dest_index
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def contextualize_signature_with_heading_owner_path(analysis, index, signature)
|
|
102
|
+
return signature unless section_owned_signature?(signature)
|
|
103
|
+
return signature if synthetic_section_label_paragraph?(analysis, index)
|
|
104
|
+
return signature if gap_line_after_synthetic_section_label?(analysis, index)
|
|
105
|
+
|
|
106
|
+
owner_path = heading_owner_path_for(analysis, index)
|
|
107
|
+
return signature if owner_path.empty?
|
|
108
|
+
|
|
109
|
+
[signature.first, owner_path, *signature.drop(1)]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def section_owned_signature?(signature)
|
|
113
|
+
signature.is_a?(Array) && %i[
|
|
114
|
+
paragraph
|
|
115
|
+
code_block
|
|
116
|
+
blockquote
|
|
117
|
+
block_quote
|
|
118
|
+
table
|
|
119
|
+
custom_block
|
|
120
|
+
hrule
|
|
121
|
+
thematic_break
|
|
122
|
+
gap_line_after
|
|
123
|
+
].include?(signature.first)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def heading_owner_path_for(analysis, index)
|
|
127
|
+
heading_owner_paths_for(analysis).fetch(index, EMPTY_HEADING_OWNER_PATH)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def heading_owner_paths_for(analysis)
|
|
131
|
+
@heading_owner_paths ||= {}
|
|
132
|
+
@heading_owner_paths.fetch(analysis.object_id) do
|
|
133
|
+
@heading_owner_paths[analysis.object_id] = build_heading_owner_paths(analysis)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
EMPTY_HEADING_OWNER_PATH = [].freeze
|
|
138
|
+
|
|
139
|
+
def build_heading_owner_paths(analysis)
|
|
140
|
+
statements = statements_for(analysis)
|
|
141
|
+
stack = []
|
|
142
|
+
|
|
143
|
+
statements.each_with_index.each_with_object([]) do |(_statement, index), paths|
|
|
144
|
+
signature = analysis.signature_at(index)
|
|
145
|
+
if heading_signature?(signature)
|
|
146
|
+
level = signature[1]
|
|
147
|
+
stack.pop while stack.any? && stack.last[:level] >= level
|
|
148
|
+
paths << stack.map { |entry| entry[:signature] }.freeze
|
|
149
|
+
stack << { level: level, signature: signature }
|
|
150
|
+
else
|
|
151
|
+
paths << stack.map { |entry| entry[:signature] }.freeze
|
|
152
|
+
end
|
|
153
|
+
end.freeze
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def refined_match_heading_owner_compatible?(template_index, dest_index)
|
|
157
|
+
template_owner_path = heading_owner_path_for(template_analysis, template_index)
|
|
158
|
+
dest_owner_path = heading_owner_path_for(dest_analysis, dest_index)
|
|
159
|
+
return true if template_owner_path == dest_owner_path
|
|
160
|
+
|
|
161
|
+
refined_list_repair_compatible?(template_index, dest_index) ||
|
|
162
|
+
adjacent_section_hijack_repair_compatible?(template_index, dest_index)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def refined_list_repair_compatible?(template_index, dest_index)
|
|
166
|
+
template_signature = template_analysis.signature_at(template_index)
|
|
167
|
+
dest_signature = dest_analysis.signature_at(dest_index)
|
|
168
|
+
return false unless list_signature?(template_signature) && list_signature?(dest_signature)
|
|
169
|
+
return true if synthetic_label_scoped_list?(template_analysis, template_index)
|
|
170
|
+
|
|
171
|
+
template_count = template_signature[2].to_i
|
|
172
|
+
dest_count = dest_signature[2].to_i
|
|
173
|
+
[template_count, dest_count].max >= [template_count, dest_count].min + 2
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def adjacent_section_hijack_repair_compatible?(template_index, dest_index)
|
|
177
|
+
template_signature = template_analysis.signature_at(template_index)
|
|
178
|
+
dest_signature = dest_analysis.signature_at(dest_index)
|
|
179
|
+
return false unless repairable_hijacked_section_signature?(template_signature, dest_signature)
|
|
180
|
+
return false unless exact_statement_text_match?(template_analysis, template_index, dest_analysis, dest_index)
|
|
181
|
+
|
|
182
|
+
template_owner = section_owner_for(template_analysis, template_index)
|
|
183
|
+
dest_owner = section_owner_for(dest_analysis, dest_index)
|
|
184
|
+
return false unless template_owner && dest_owner
|
|
185
|
+
return false if template_owner[:signature] == dest_owner[:signature]
|
|
186
|
+
return false unless section_empty_for_heading_signature?(dest_analysis, template_owner[:signature])
|
|
187
|
+
|
|
188
|
+
neighboring_section_signatures?(template_analysis, template_owner[:signature], dest_owner[:signature]) &&
|
|
189
|
+
neighboring_section_signatures?(dest_analysis, template_owner[:signature], dest_owner[:signature])
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def template_only_entry_context(template_index:, matched_entries_by_template_position:, **)
|
|
193
|
+
next_match = next_match_for_template_only_entry(
|
|
194
|
+
template_index,
|
|
195
|
+
matched_entries_by_template_position
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
{
|
|
199
|
+
anchor_dest_index: anchor_dest_index_for_entry(next_match),
|
|
200
|
+
anchor_position: next_match ? :before : :append
|
|
201
|
+
}
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def next_match_for_template_only_entry(template_index, matched_entries_by_template_position)
|
|
205
|
+
same_owner_match = next_same_owner_match_for_template_only_entry(
|
|
206
|
+
template_index,
|
|
207
|
+
matched_entries_by_template_position
|
|
208
|
+
)
|
|
209
|
+
return same_owner_match if same_owner_match
|
|
210
|
+
|
|
211
|
+
section_boundary_match = next_section_boundary_match_for_template_only_entry(
|
|
212
|
+
template_index,
|
|
213
|
+
matched_entries_by_template_position
|
|
214
|
+
)
|
|
215
|
+
return section_boundary_match if section_boundary_match
|
|
216
|
+
|
|
217
|
+
_previous_match, next_match = surrounding_matched_entries(matched_entries_by_template_position, template_index)
|
|
218
|
+
next_match
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def next_same_owner_match_for_template_only_entry(template_index, matched_entries_by_template_position)
|
|
222
|
+
owner = section_owner_for(template_analysis, template_index)
|
|
223
|
+
return unless owner
|
|
224
|
+
|
|
225
|
+
matched_entries_by_template_position.find do |entry|
|
|
226
|
+
candidate_index = entry[:template_index]
|
|
227
|
+
next false unless candidate_index && candidate_index > template_index
|
|
228
|
+
|
|
229
|
+
candidate_signature = template_analysis.signature_at(candidate_index)
|
|
230
|
+
next false if candidate_signature.is_a?(Array) && candidate_signature.first == :gap_line_after
|
|
231
|
+
|
|
232
|
+
candidate_owner = section_owner_for(template_analysis, candidate_index)
|
|
233
|
+
candidate_owner && candidate_owner[:signature] == owner[:signature]
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def next_section_boundary_match_for_template_only_entry(template_index, matched_entries_by_template_position)
|
|
238
|
+
owner = section_owner_for(template_analysis, template_index)
|
|
239
|
+
return unless owner
|
|
240
|
+
|
|
241
|
+
matched_entries_by_template_position.find do |entry|
|
|
242
|
+
candidate_index = entry[:template_index]
|
|
243
|
+
next false unless candidate_index && candidate_index > owner[:heading_index]
|
|
244
|
+
|
|
245
|
+
candidate_signature = template_analysis.signature_at(candidate_index)
|
|
246
|
+
heading_signature?(candidate_signature) && candidate_signature[1] <= owner[:level]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def section_owner_for(analysis, index)
|
|
251
|
+
statements = statements_for(analysis)
|
|
252
|
+
statement = statements[index]
|
|
253
|
+
signature = analysis.signature_at(index)
|
|
254
|
+
return unless statement && signature
|
|
255
|
+
|
|
256
|
+
if heading_signature?(signature)
|
|
257
|
+
{ heading_index: index, level: signature[1], owner_path: heading_owner_path_for(analysis, index),
|
|
258
|
+
signature: signature }
|
|
259
|
+
else
|
|
260
|
+
owner_path = heading_owner_path_for(analysis, index)
|
|
261
|
+
return if owner_path.empty?
|
|
262
|
+
|
|
263
|
+
owner_signature = owner_path.last
|
|
264
|
+
owner_index = nearest_owner_heading_index_for(analysis, index, owner_signature)
|
|
265
|
+
return unless owner_index
|
|
266
|
+
|
|
267
|
+
{ heading_index: owner_index, level: owner_signature[1], owner_path: owner_path, signature: owner_signature }
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def nearest_owner_heading_index_for(analysis, index, owner_signature)
|
|
272
|
+
index.downto(0) do |current_index|
|
|
273
|
+
signature = analysis.signature_at(current_index)
|
|
274
|
+
next unless heading_signature?(signature)
|
|
275
|
+
return current_index if signature == owner_signature
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
nil
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def log_alignment(alignment)
|
|
282
|
+
DebugLogger.debug('Alignment complete', {
|
|
283
|
+
total: alignment.size,
|
|
284
|
+
matches: alignment.count { |e| e[:type] == :match },
|
|
285
|
+
template_only: alignment.count { |e| e[:type] == :template_only },
|
|
286
|
+
dest_only: alignment.count { |e| e[:type] == :dest_only }
|
|
287
|
+
})
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def match_sort_key(entry)
|
|
291
|
+
[0, anchor_dest_index_for_entry(entry), 0, entry[:template_index] || 0]
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def template_only_sort_key(entry, _dest_size)
|
|
295
|
+
anchor_dest_index = entry[:anchor_dest_index]
|
|
296
|
+
|
|
297
|
+
case entry[:anchor_position]
|
|
298
|
+
when :before
|
|
299
|
+
[0, anchor_dest_index, -1, entry[:template_index]]
|
|
300
|
+
else
|
|
301
|
+
[1, entry[:template_index], 0, 0]
|
|
302
|
+
end
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def list_signature?(signature)
|
|
306
|
+
signature.is_a?(Array) && signature.first == :list
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def contextual_list_signature(analysis, index, signature)
|
|
310
|
+
statement = statements_for(analysis)[index]
|
|
311
|
+
list_type = signature[1]
|
|
312
|
+
preceding_context_index = nearest_list_context_index(analysis, index)
|
|
313
|
+
preceding_context = preceding_context_index ? analysis.signature_at(preceding_context_index) : nil
|
|
314
|
+
owner_path = if preceding_context_index && synthetic_section_label_paragraph?(analysis, preceding_context_index)
|
|
315
|
+
EMPTY_HEADING_OWNER_PATH
|
|
316
|
+
else
|
|
317
|
+
heading_owner_path_for(analysis, index)
|
|
318
|
+
end
|
|
319
|
+
first_anchor = first_list_item_anchor(statement, analysis)
|
|
320
|
+
[:list, owner_path, list_type, preceding_context, first_anchor]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def heading_signature?(signature)
|
|
324
|
+
signature.is_a?(Array) && signature.first == :heading
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def nearest_list_context_index(analysis, index)
|
|
328
|
+
(index - 1).downto(0) do |current_index|
|
|
329
|
+
candidate = analysis.signature_at(current_index)
|
|
330
|
+
next unless contextual_predecessor_signature?(candidate)
|
|
331
|
+
|
|
332
|
+
return current_index
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
nil
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
def contextual_predecessor_signature?(signature)
|
|
339
|
+
signature.is_a?(Array) && %i[heading paragraph code_block].include?(signature.first)
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def synthetic_section_label_paragraph?(analysis, index)
|
|
343
|
+
signature = analysis.signature_at(index)
|
|
344
|
+
return false unless signature.is_a?(Array) && signature.first == :paragraph
|
|
345
|
+
|
|
346
|
+
statement = statements_for(analysis)[index]
|
|
347
|
+
return false unless statement&.respond_to?(:text)
|
|
348
|
+
|
|
349
|
+
text = statement.text.to_s.strip
|
|
350
|
+
return false if text.empty? || text.length > 120
|
|
351
|
+
return false if text.include?("\n")
|
|
352
|
+
return false if text.match?(/[.!?:]\z/)
|
|
353
|
+
|
|
354
|
+
text.split(/\s+/).length <= 12
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def synthetic_label_scoped_list?(analysis, index)
|
|
358
|
+
return false unless list_signature?(analysis.signature_at(index))
|
|
359
|
+
|
|
360
|
+
context_index = nearest_list_context_index(analysis, index)
|
|
361
|
+
context_index && synthetic_section_label_paragraph?(analysis, context_index)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def gap_line_after_synthetic_section_label?(analysis, index)
|
|
365
|
+
signature = analysis.signature_at(index)
|
|
366
|
+
return false unless signature.is_a?(Array) && signature.first == :gap_line_after
|
|
367
|
+
return false unless index.positive?
|
|
368
|
+
|
|
369
|
+
synthetic_section_label_paragraph?(analysis, index - 1)
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def repairable_hijacked_section_signature?(template_signature, dest_signature)
|
|
373
|
+
return false unless template_signature == dest_signature
|
|
374
|
+
|
|
375
|
+
paragraph_signature?(template_signature) || list_signature?(template_signature)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def paragraph_signature?(signature)
|
|
379
|
+
signature.is_a?(Array) && signature.first == :paragraph
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def exact_statement_text_match?(template_analysis, template_index, dest_analysis, dest_index)
|
|
383
|
+
template_statement = statements_for(template_analysis)[template_index]
|
|
384
|
+
dest_statement = statements_for(dest_analysis)[dest_index]
|
|
385
|
+
return false unless template_statement&.respond_to?(:text) && dest_statement&.respond_to?(:text)
|
|
386
|
+
|
|
387
|
+
template_statement.text.to_s == dest_statement.text.to_s
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def adjacent_section_hijack_repair_sort_dest_index(template_index, dest_index)
|
|
391
|
+
return unless adjacent_section_hijack_repair_compatible?(template_index, dest_index)
|
|
392
|
+
|
|
393
|
+
template_owner = section_owner_for(template_analysis, template_index)
|
|
394
|
+
return unless template_owner
|
|
395
|
+
|
|
396
|
+
heading_index = heading_index_for_signature(dest_analysis, template_owner[:signature])
|
|
397
|
+
return unless heading_index
|
|
398
|
+
|
|
399
|
+
boundary_index = next_section_boundary_heading_index(dest_analysis, heading_index)
|
|
400
|
+
return heading_index + 0.5 unless boundary_index
|
|
401
|
+
|
|
402
|
+
boundary_index - 0.5
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def anchor_dest_index_for_entry(entry)
|
|
406
|
+
entry&.fetch(:repair_dest_index, entry[:dest_index])
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def section_empty_for_heading_signature?(analysis, heading_signature)
|
|
410
|
+
heading_index = heading_index_for_signature(analysis, heading_signature)
|
|
411
|
+
return false unless heading_index
|
|
412
|
+
|
|
413
|
+
next_heading_index = next_section_boundary_heading_index(analysis, heading_index)
|
|
414
|
+
last_index = next_heading_index ? next_heading_index - 1 : statements_for(analysis).length - 1
|
|
415
|
+
((heading_index + 1)..last_index).none? do |index|
|
|
416
|
+
signature = analysis.signature_at(index)
|
|
417
|
+
signature && signature.first != :gap_line_after && !heading_signature?(signature)
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
def neighboring_section_signatures?(analysis, first_signature, second_signature)
|
|
422
|
+
first_index = heading_index_for_signature(analysis, first_signature)
|
|
423
|
+
second_index = heading_index_for_signature(analysis, second_signature)
|
|
424
|
+
return false unless first_index && second_index
|
|
425
|
+
|
|
426
|
+
neighboring_heading_signatures(analysis, first_index).include?(second_signature)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def neighboring_heading_signatures(analysis, heading_index)
|
|
430
|
+
signature = analysis.signature_at(heading_index)
|
|
431
|
+
return [] unless heading_signature?(signature)
|
|
432
|
+
|
|
433
|
+
level = signature[1]
|
|
434
|
+
parent_path = heading_owner_path_for(analysis, heading_index)
|
|
435
|
+
headings = statements_for(analysis).each_index.filter_map do |index|
|
|
436
|
+
candidate = analysis.signature_at(index)
|
|
437
|
+
next unless heading_signature?(candidate)
|
|
438
|
+
next unless candidate[1] == level
|
|
439
|
+
next unless heading_owner_path_for(analysis, index) == parent_path
|
|
440
|
+
|
|
441
|
+
candidate
|
|
442
|
+
end
|
|
443
|
+
current_position = headings.index(signature)
|
|
444
|
+
return [] unless current_position
|
|
445
|
+
|
|
446
|
+
[headings[current_position - 1], headings[current_position + 1]].compact
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def heading_index_for_signature(analysis, heading_signature)
|
|
450
|
+
statements_for(analysis).each_index.find do |index|
|
|
451
|
+
analysis.signature_at(index) == heading_signature
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def next_section_boundary_heading_index(analysis, heading_index)
|
|
456
|
+
signature = analysis.signature_at(heading_index)
|
|
457
|
+
return unless heading_signature?(signature)
|
|
458
|
+
|
|
459
|
+
level = signature[1]
|
|
460
|
+
((heading_index + 1)...statements_for(analysis).length).find do |index|
|
|
461
|
+
candidate = analysis.signature_at(index)
|
|
462
|
+
heading_signature?(candidate) && candidate[1] <= level
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def first_list_item_anchor(statement, analysis)
|
|
467
|
+
raw = Ast::Merge::NodeTyping.unwrap(statement)
|
|
468
|
+
first_item = nil
|
|
469
|
+
|
|
470
|
+
raw.each do |child|
|
|
471
|
+
next unless child.respond_to?(:type) && %w[list_item item].include?(child.type.to_s)
|
|
472
|
+
|
|
473
|
+
first_item = child
|
|
474
|
+
break
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
return '' unless first_item
|
|
478
|
+
|
|
479
|
+
text = if analysis && first_item.respond_to?(:source_position) && first_item.source_position
|
|
480
|
+
pos = first_item.source_position
|
|
481
|
+
analysis.source_range(pos[:start_line], pos[:end_line]).to_s
|
|
482
|
+
else
|
|
483
|
+
first_item.respond_to?(:text) ? first_item.text.to_s : ''
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
text
|
|
487
|
+
.strip
|
|
488
|
+
.sub(/\A(?:[-*+]|\d+\.)\s+/, '')
|
|
489
|
+
.gsub(/[^\p{L}\p{N}]+/u, ' ')
|
|
490
|
+
.gsub(/\s+/, ' ')
|
|
491
|
+
.downcase
|
|
492
|
+
.strip
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
end
|