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
|
@@ -24,7 +24,7 @@ module Markdown
|
|
|
24
24
|
#
|
|
25
25
|
# @see FileAnalysisBase
|
|
26
26
|
# @see SmartMergerBase
|
|
27
|
-
class FileAligner
|
|
27
|
+
class FileAligner < ::Ast::Merge::FileAlignerBase
|
|
28
28
|
# @return [FileAnalysisBase] Template file analysis
|
|
29
29
|
attr_reader :template_analysis
|
|
30
30
|
|
|
@@ -39,157 +39,457 @@ module Markdown
|
|
|
39
39
|
# @param template_analysis [FileAnalysisBase] Analysis of the template file
|
|
40
40
|
# @param dest_analysis [FileAnalysisBase] Analysis of the destination file
|
|
41
41
|
# @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
|
|
42
|
-
def initialize(template_analysis, dest_analysis, match_refiner: nil)
|
|
43
|
-
|
|
44
|
-
@dest_analysis = dest_analysis
|
|
45
|
-
@match_refiner = match_refiner
|
|
42
|
+
def initialize(template_analysis, dest_analysis, match_refiner: nil, **options)
|
|
43
|
+
super(template_analysis, dest_analysis, match_refiner: match_refiner, **options)
|
|
46
44
|
end
|
|
47
45
|
|
|
48
|
-
|
|
49
|
-
#
|
|
50
|
-
# @return [Array<Hash>] Alignment entries with type, indices, and nodes
|
|
51
|
-
def align
|
|
52
|
-
template_statements = @template_analysis.statements
|
|
53
|
-
dest_statements = @dest_analysis.statements
|
|
54
|
-
|
|
55
|
-
# Build signature maps
|
|
56
|
-
template_by_sig = build_signature_map(template_statements, @template_analysis)
|
|
57
|
-
dest_by_sig = build_signature_map(dest_statements, @dest_analysis)
|
|
58
|
-
|
|
59
|
-
# Track which indices have been matched
|
|
60
|
-
matched_template = Set.new
|
|
61
|
-
matched_dest = Set.new
|
|
62
|
-
alignment = []
|
|
63
|
-
|
|
64
|
-
# First pass: find matches by signature
|
|
65
|
-
template_by_sig.each do |sig, template_indices|
|
|
66
|
-
next unless dest_by_sig.key?(sig)
|
|
67
|
-
|
|
68
|
-
dest_indices = dest_by_sig[sig]
|
|
69
|
-
|
|
70
|
-
# Match indices pairwise (first template with first dest, etc.)
|
|
71
|
-
template_indices.zip(dest_indices).each do |t_idx, d_idx|
|
|
72
|
-
next unless t_idx && d_idx
|
|
73
|
-
|
|
74
|
-
alignment << {
|
|
75
|
-
type: :match,
|
|
76
|
-
template_index: t_idx,
|
|
77
|
-
dest_index: d_idx,
|
|
78
|
-
signature: sig,
|
|
79
|
-
template_node: template_statements[t_idx],
|
|
80
|
-
dest_node: dest_statements[d_idx],
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
matched_template << t_idx
|
|
84
|
-
matched_dest << d_idx
|
|
85
|
-
end
|
|
86
|
-
end
|
|
46
|
+
private
|
|
87
47
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
dest_analysis: @dest_analysis,
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
refiner_matches.each do |match|
|
|
100
|
-
t_idx = template_statements.index(match.template_node)
|
|
101
|
-
d_idx = dest_statements.index(match.dest_node)
|
|
102
|
-
|
|
103
|
-
next unless t_idx && d_idx
|
|
104
|
-
next if matched_template.include?(t_idx) || matched_dest.include?(d_idx)
|
|
105
|
-
|
|
106
|
-
alignment << {
|
|
107
|
-
type: :match,
|
|
108
|
-
template_index: t_idx,
|
|
109
|
-
dest_index: d_idx,
|
|
110
|
-
signature: [:refined_match, match.score],
|
|
111
|
-
template_node: match.template_node,
|
|
112
|
-
dest_node: match.dest_node,
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
matched_template << t_idx
|
|
116
|
-
matched_dest << d_idx
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
end
|
|
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
|
|
120
56
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
|
132
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
|
|
133
98
|
end
|
|
99
|
+
end
|
|
134
100
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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)
|
|
147
134
|
end
|
|
135
|
+
end
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 }
|
|
158
150
|
else
|
|
159
|
-
|
|
160
|
-
[2, 0] # Unknown types sort last
|
|
161
|
-
# :nocov:
|
|
151
|
+
paths << stack.map { |entry| entry[:signature] }.freeze
|
|
162
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]
|
|
163
247
|
end
|
|
248
|
+
end
|
|
164
249
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
})
|
|
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
|
|
171
255
|
|
|
172
|
-
|
|
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
|
|
173
269
|
end
|
|
174
270
|
|
|
175
|
-
|
|
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
|
|
176
277
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
190
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
|
|
191
485
|
|
|
192
|
-
|
|
486
|
+
text
|
|
487
|
+
.strip
|
|
488
|
+
.sub(/\A(?:[-*+]|\d+\.)\s+/, '')
|
|
489
|
+
.gsub(/[^\p{L}\p{N}]+/u, ' ')
|
|
490
|
+
.gsub(/\s+/, ' ')
|
|
491
|
+
.downcase
|
|
492
|
+
.strip
|
|
193
493
|
end
|
|
194
494
|
end
|
|
195
495
|
end
|