markdown-merge 7.1.7 → 7.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3a8e3b7c5faff298e87bab2c7d25c23b297b0c039dc6ae2853fcfdb0ead77709
4
- data.tar.gz: 4ccf0286e5fd9ce0613f83dd85a5e42184dfd0335436813edc9b3cebeedc91db
3
+ metadata.gz: 817400fb73e8d4845d77677b66ef654ad7ac29f07352efe8b78f747d4c01f3a0
4
+ data.tar.gz: 4899c6fc0128fe1979c80c29f4b299d64563ec85592e7ae8f6f20a6b8c43ba75
5
5
  SHA512:
6
- metadata.gz: 0a77751cfca527af217ac9850c75e408673673f00fd28257877f87fb985a1f4ed7a70ffb6cd2b54394e051b8b5f7ad2b9c0fa0b2ed195cc928dc24f2e3a3f0b0
7
- data.tar.gz: 4724b5a5a7446f59fed4260e133d911752e6e76f2d3b5f4bdb9e19773dcdcc1e482f80bf41b91548ac3d6cdc1c40ea6107050a896f3ae64d488cb171522b273f
6
+ metadata.gz: db98d155c4ea0180d72bcf1467d06bad1a0aadf557d1e087629072349405520dd4dc22652401adcc8786cd9f579594eb7efd438831caad2803684239da5fc0d4
7
+ data.tar.gz: e59d2c04bd75eacb7c52679a743791725b36f5d1ece90caf385f51da51c42039a1d247a91183b95090d4bcc4dd8597b3dd0a10603ca3255768fc9232d0032390
checksums.yaml.gz.sig CHANGED
Binary file
@@ -143,6 +143,29 @@ RSpec.shared_examples 'Markdown::Merge::SourcePreservingProvider' do
143
143
  )
144
144
  end
145
145
 
146
+ it 'preserves current section customizations while inserting incoming-only sections in order' do
147
+ current = "# Title\n\ncurrent body\n\n# Last\n\ncurrent ending\n"
148
+ incoming = "# Title\n\ntemplate body\n\n# Added\n\nnew section\n\n# Last\n\ntemplate ending\n"
149
+ expected = "# Title\n\ncurrent body\n\n# Added\n\nnew section\n\n# Last\n\ncurrent ending\n"
150
+
151
+ result = provider.merge2(current_source: current, incoming_source: incoming)
152
+
153
+ expect(result).to include(ok: true, operation: :merge2, output: expected)
154
+ expect(result.fetch(:changes)).to contain_exactly(hash_including(change: :added))
155
+ expect(result.dig(:render_report, :provenance)).to contain_exactly(
156
+ hash_including(source_role: :current, copied_source: true),
157
+ hash_including(source_role: :incoming, copied_source: true),
158
+ hash_including(source_role: :current, copied_source: true)
159
+ )
160
+ expect(result.fetch(:verification)).to include(
161
+ output_reparsed: true,
162
+ semantic_match: true,
163
+ byte_provenance_verified: true,
164
+ delegated_backend_verified: true,
165
+ backend: provider_backend
166
+ )
167
+ end
168
+
146
169
  it 'localizes a same-section conflict to the parser-proven owner range' do
147
170
  result = provider.merge3(
148
171
  base_source: base,
@@ -174,16 +197,27 @@ RSpec.shared_examples 'Markdown::Merge::SourcePreservingProvider' do
174
197
  expect(result.dig(:render_report, :strategy)).to eq(:full_file_conflict)
175
198
  end
176
199
 
177
- expect(provider.analyze(source: "# Alpha\n\n## Nested\n\ntext\n")).to include(
178
- ok: false,
179
- diagnostics: include(hash_including(category: :unsafe_source_range))
180
- )
200
+ expect(provider.analyze(source: "# Alpha\n\n## Nested\n\ntext\n")).to include(ok: true)
181
201
  expect(provider.analyze(source: "headingless prose\n")).to include(
182
202
  ok: false,
183
203
  diagnostics: include(hash_including(category: :unsafe_source_range))
184
204
  )
185
205
  end
186
206
 
207
+ it 'merges independent nested leaf heading edits while preserving ancestor framing' do
208
+ result = provider.merge3(
209
+ base_source: "# Guide\n\n## Left\nOne.\n\n## Right\nOne.\n",
210
+ ours_source: "# Guide\n\n## Left\nTwo.\n\n## Right\nOne.\n",
211
+ theirs_source: "# Guide\n\n## Left\nOne.\n\n## Right\nTwo.\n"
212
+ )
213
+
214
+ expect(result).to include(
215
+ ok: true,
216
+ output: "# Guide\n\n## Left\nTwo.\n\n## Right\nTwo.\n"
217
+ )
218
+ expect(result.dig(:verification, :semantic_match)).to be(true)
219
+ end
220
+
187
221
  it 'fails closed for setext headings and non-whitespace prose outside sections' do
188
222
  expect(provider.analyze(source: "Alpha\n=====\n\ntext\n")).to include(
189
223
  ok: false,
@@ -198,14 +198,40 @@ module Markdown
198
198
  end
199
199
 
200
200
  def merge2(request)
201
- merged = merge3(
202
- request.merge(
203
- base_source: request.fetch(:current_source),
204
- ours_source: request.fetch(:current_source),
205
- theirs_source: request.fetch(:incoming_source)
206
- )
201
+ current = parse_document(:merge2, request, :current)
202
+ return current if provider_failure?(current)
203
+
204
+ incoming = parse_document(:merge2, request, :incoming)
205
+ return incoming if provider_failure?(incoming)
206
+
207
+ choices = current.owners.to_h { |owner| [owner.id, :current] }
208
+ incoming.owners.each { |owner| choices[owner.id] ||= :incoming }
209
+ decision = Decision.new(changes: [], conflicts: [], choices: choices.freeze)
210
+ documents = { current: current, incoming: incoming }.freeze
211
+ ordered = merged_order(documents, decision)
212
+ return unsafe_merge2_order_failure(request) unless ordered
213
+
214
+ fragments = ordered.map do |id|
215
+ role = decision.choices.fetch(id)
216
+ [documents.fetch(role).by_id.fetch(id), role]
217
+ end
218
+ output = composite_prefix(documents, :current) + fragments.map { |owner, _role| owner.source_text }.join
219
+ expected = fragments.map { |owner, _role| [owner.signature, owner.fingerprint] }
220
+ verification = verify_composite(output, expected)
221
+ unless verification[:semantic_match]
222
+ return render_failure(request, verification, :exact_markdown_composite, operation: :merge2)
223
+ end
224
+
225
+ output_document = parse_source(output, :output)
226
+ changes = diff_documents(current, output_document)
227
+ result(
228
+ :merge2,
229
+ request,
230
+ output: output,
231
+ changes: changes,
232
+ render_report: composite_render_report(fragments, output),
233
+ verification: verification
207
234
  )
208
- merged.merge(operation: :merge2, verification: merged.fetch(:verification).except(:base_participated))
209
235
  end
210
236
 
211
237
  def merge3(request)
@@ -250,13 +276,17 @@ module Markdown
250
276
  headings = parsed.fetch(:headings)
251
277
  return { unsafe: :headingless_document, source_role: role } if headings.empty?
252
278
  return { unsafe: :setext_heading, source_role: role } unless headings.all? { |heading| heading.style == :atx }
253
- return { unsafe: :nested_heading_hierarchy, source_role: role } unless headings.map(&:level).uniq.one?
279
+
280
+ headings = independently_ownable_headings(headings)
281
+ return { unsafe: :nested_heading_hierarchy, source_role: role } if headings.empty?
254
282
 
255
283
  starts = headings.map(&:start_line).map { |line| line_start_byte(source, line) }
256
284
  if starts.any?(&:nil?) || starts != starts.sort.uniq
257
285
  return { unsafe: :invalid_heading_start, source_role: role }
258
286
  end
259
- return { unsafe: :source_before_first_section, source_role: role } unless starts.first.zero?
287
+ unless starts.first.zero? || heading_only_prefix?(source, parsed.fetch(:headings), starts.first)
288
+ return { unsafe: :source_before_first_section, source_role: role }
289
+ end
260
290
 
261
291
  owners = headings.each_with_index.map do |heading, index|
262
292
  signature = [heading.level, heading.text].freeze
@@ -288,6 +318,27 @@ module Markdown
288
318
  { parse_error: e.message, source_role: role }
289
319
  end
290
320
 
321
+ # A nested heading is ownable when it has no descendant headings. Ancestor
322
+ # headings remain unmanaged framing, so edits to them still fail closed.
323
+ def independently_ownable_headings(headings)
324
+ headings.each_with_index.reject do |heading, index|
325
+ headings[(index + 1)..].any? do |candidate|
326
+ candidate.level > heading.level &&
327
+ headings[(index + 1)..].take_while { |item| item.level > heading.level }.include?(candidate)
328
+ end
329
+ end.map(&:first)
330
+ end
331
+
332
+ def heading_only_prefix?(source, headings, first_owner_start)
333
+ prefix = source.byteslice(0...first_owner_start).to_s
334
+ heading_lines = headings.select { |heading| line_start_byte(source, heading.start_line) < first_owner_start }
335
+ heading_lines.any? && prefix.lines.all? do |line|
336
+ line.strip.empty? || heading_lines.any? do |heading|
337
+ line.strip.match?(/\A\#{1,6}\s+#{Regexp.escape(heading.text.to_s)}\s*\z/)
338
+ end
339
+ end
340
+ end
341
+
291
342
  def parse_exact_source(source, role)
292
343
  parsed = parse_ast(source, role)
293
344
  return parsed if provider_failure?(parsed)
@@ -517,7 +568,7 @@ module Markdown
517
568
  owner = role && documents.fetch(role).by_id[id]
518
569
  owner && [owner, role]
519
570
  end
520
- output = fragments.map { |owner, _role| owner.source_text }.join
571
+ output = composite_prefix(documents) + fragments.map { |owner, _role| owner.source_text }.join
521
572
  expected = fragments.map { |owner, _role| [owner.signature, owner.fingerprint] }
522
573
  verification = verify_composite(output, expected)
523
574
  return render_failure(request, verification, :exact_markdown_composite, decision.changes) unless
@@ -615,6 +666,14 @@ module Markdown
615
666
  }
616
667
  end
617
668
 
669
+ def composite_prefix(documents, role = :ours)
670
+ document = documents.fetch(role)
671
+ first_owner = document.owners.first
672
+ return '' unless first_owner
673
+
674
+ document.source.byteslice(0...first_owner.start_byte).to_s
675
+ end
676
+
618
677
  def render_conflicts(request, documents, decision)
619
678
  localized = localized_conflict_output(request, documents, decision)
620
679
  strategy = localized ? :section_localized_conflict : :full_file_conflict
@@ -714,9 +773,20 @@ module Markdown
714
773
  )
715
774
  end
716
775
 
717
- def render_failure(request, verification, strategy, changes = [])
776
+ def unsafe_merge2_order_failure(request)
718
777
  failure(
719
- :merge3,
778
+ :merge2,
779
+ request,
780
+ category: :unsafe_source_range,
781
+ message: 'Markdown section order cannot be proven across current and incoming sources.',
782
+ conflicts: [{ category: :incompatible_section_order, path: '<document>' }],
783
+ render_report: { strategy: :no_output, provenance: :exact_source_bytes }
784
+ )
785
+ end
786
+
787
+ def render_failure(request, verification, strategy, changes = [], operation: :merge3)
788
+ failure(
789
+ operation,
720
790
  request,
721
791
  category: :render_failure,
722
792
  message: 'Markdown output failed backend reparse and ordered semantic verification.',
@@ -5,7 +5,7 @@ module Markdown
5
5
  # Version namespace for this gem.
6
6
  module Version
7
7
  # Current gem version.
8
- VERSION = '7.1.7'
8
+ VERSION = '7.1.8'
9
9
  end
10
10
  # Current gem version exposed at the traditional constant location.
11
11
  VERSION = Version::VERSION # Traditional Constant Location
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.7
4
+ version: 7.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -43,28 +43,28 @@ dependencies:
43
43
  requirements:
44
44
  - - '='
45
45
  - !ruby/object:Gem::Version
46
- version: 7.1.7
46
+ version: 7.1.8
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - '='
52
52
  - !ruby/object:Gem::Version
53
- version: 7.1.7
53
+ version: 7.1.8
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: tree_haver
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - '='
59
59
  - !ruby/object:Gem::Version
60
- version: 7.1.7
60
+ version: 7.1.8
61
61
  type: :runtime
62
62
  prerelease: false
63
63
  version_requirements: !ruby/object:Gem::Requirement
64
64
  requirements:
65
65
  - - '='
66
66
  - !ruby/object:Gem::Version
67
- version: 7.1.7
67
+ version: 7.1.8
68
68
  - !ruby/object:Gem::Dependency
69
69
  name: version_gem
70
70
  requirement: !ruby/object:Gem::Requirement
@@ -94,7 +94,7 @@ dependencies:
94
94
  version: '3.0'
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
- version: 3.0.17
97
+ version: 3.0.31
98
98
  type: :development
99
99
  prerelease: false
100
100
  version_requirements: !ruby/object:Gem::Requirement
@@ -104,7 +104,7 @@ dependencies:
104
104
  version: '3.0'
105
105
  - - ">="
106
106
  - !ruby/object:Gem::Version
107
- version: 3.0.17
107
+ version: 3.0.31
108
108
  - !ruby/object:Gem::Dependency
109
109
  name: bundler-audit
110
110
  requirement: !ruby/object:Gem::Requirement
@@ -182,7 +182,7 @@ dependencies:
182
182
  version: '3.2'
183
183
  - - ">="
184
184
  - !ruby/object:Gem::Version
185
- version: 3.2.2
185
+ version: 3.2.3
186
186
  type: :development
187
187
  prerelease: false
188
188
  version_requirements: !ruby/object:Gem::Requirement
@@ -192,7 +192,7 @@ dependencies:
192
192
  version: '3.2'
193
193
  - - ">="
194
194
  - !ruby/object:Gem::Version
195
- version: 3.2.2
195
+ version: 3.2.3
196
196
  - !ruby/object:Gem::Dependency
197
197
  name: kettle-test
198
198
  requirement: !ruby/object:Gem::Requirement
@@ -222,7 +222,7 @@ dependencies:
222
222
  version: '3.2'
223
223
  - - ">="
224
224
  - !ruby/object:Gem::Version
225
- version: 3.2.6
225
+ version: 3.2.7
226
226
  type: :development
227
227
  prerelease: false
228
228
  version_requirements: !ruby/object:Gem::Requirement
@@ -232,7 +232,7 @@ dependencies:
232
232
  version: '3.2'
233
233
  - - ">="
234
234
  - !ruby/object:Gem::Version
235
- version: 3.2.6
235
+ version: 3.2.7
236
236
  - !ruby/object:Gem::Dependency
237
237
  name: ruby-progressbar
238
238
  requirement: !ruby/object:Gem::Requirement
@@ -345,10 +345,10 @@ licenses:
345
345
  - PolyForm-Small-Business-1.0.0
346
346
  metadata:
347
347
  homepage_uri: https://structuredmerge.org
348
- source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.7
349
- changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.7/CHANGELOG.md
348
+ source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.8
349
+ changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.8/CHANGELOG.md
350
350
  bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
351
- documentation_uri: https://www.rubydoc.info/gems/markdown-merge/7.1.7
351
+ documentation_uri: https://www.rubydoc.info/gems/markdown-merge/7.1.8
352
352
  funding_uri: https://github.com/sponsors/pboling
353
353
  wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
354
354
  news_uri: https://www.railsbling.com/tags/markdown-merge
metadata.gz.sig CHANGED
Binary file