canon 0.3.48 → 0.3.50

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: fcfa53c5436e38941e6c789536bf9c268fb5b587930aeaca6020c22993def0a2
4
- data.tar.gz: a814221d4dd934e35f8f6eb1169f8694fadf2e2fd60e49a9f8f61e6355ff9853
3
+ metadata.gz: 6bbfa0c65294f7a3ad7d77b678dab6fc1b9f5c0fc67bf55faba3371e6cfc6614
4
+ data.tar.gz: 28b3106753c227ad6973a2698ede0315d3e8812feed6af1cbccdf5cf08636184
5
5
  SHA512:
6
- metadata.gz: 1ac2772108ab1d9cae92a479a98c94cbc02bff2a431a64e3dc9052ff4df5ec595a38c81849c3d9ddc1c1329b6a86bdfbb7d3aeae6b5bbacfa2d6d8d283acc0a6
7
- data.tar.gz: 7794b9a9392eaa18bb43b85ec7a2e830d3c0f172bfd827e13b7e3f6290c82cb6b3491312090f8230671c1a7442e722a7c1b3d66a44398965b89ab4664ec11a4d
6
+ metadata.gz: 9d3c4a330ec9224eff5a8c84bd677ea1fc72500b06099c188b3c7b30e6b096d3897f8308a40fc003067887dd4f45986464c8786b8bee8096614035dc8189c8ea
7
+ data.tar.gz: 84549e0eb08a383bf14934b8b22f58e3ad4aeac8aaaeb56ce80c87d1127df9d3ebc7d2f17763cf6cfd1dda65702e90d2379e0ba5d64193f250bd1f409d292aa3
@@ -41,6 +41,11 @@ module Canon
41
41
  # Track occurrences for text_content dimension to find correct element instance
42
42
  @text_occurrence1 = Hash.new(0)
43
43
  @text_occurrence2 = Hash.new(0)
44
+ # Opener-offset caches: one scan per unique element name per
45
+ # document replaces the per-call regex walk (which was also
46
+ # quadratic on repeated element names).
47
+ @opener_offsets1 = {}
48
+ @opener_offsets2 = {}
44
49
  end
45
50
 
46
51
  def enrich
@@ -1461,27 +1466,44 @@ range_start, range_end)
1461
1466
  # tag (no inside-the-element correction — see
1462
1467
  # locate_element_at_index).
1463
1468
  def count_elements_before_position_open(text, char_offset, opener)
1464
- count = 0
1465
- pos = 0
1466
- while (hit = text.index(opener, pos)) && hit < char_offset
1467
- count += 1
1468
- pos = hit + 1
1469
- end
1470
- count
1469
+ count_openers_before(opener_offsets(text, opener), char_offset)
1471
1470
  end
1472
1471
 
1473
1472
  def count_elements_before_position(text, char_offset, element_name)
1474
- # Index loop instead of copying the prefix per call — the
1475
- # copy was O(offset) on every occurrence check.
1476
- opener = /<#{element_name}[>\s]/
1477
- count = 0
1473
+ # Subtract 1 because the count includes the element we are inside
1474
+ [count_openers_before(opener_offsets(text, opener_for(element_name)),
1475
+ char_offset) - 1, 0].max
1476
+ end
1477
+
1478
+ # Count of opener offsets strictly before char_offset — binary
1479
+ # search over the cached, ascending offset array.
1480
+ def count_openers_before(offsets, char_offset)
1481
+ idx = offsets.bsearch_index { |offset| offset >= char_offset }
1482
+ idx || offsets.length
1483
+ end
1484
+
1485
+ # All opener offsets for one opener regex in one document,
1486
+ # cached per text (one linear scan per unique element name per
1487
+ # document — callers previously re-scanned per occurrence
1488
+ # check, which was also quadratic on repeated element names).
1489
+ def opener_offsets(text, opener)
1490
+ if text.equal?(@text1)
1491
+ (@opener_offsets1[opener.source] ||= scan_opener_offsets(text, opener))
1492
+ elsif text.equal?(@text2)
1493
+ (@opener_offsets2[opener.source] ||= scan_opener_offsets(text, opener))
1494
+ else
1495
+ scan_opener_offsets(text, opener)
1496
+ end
1497
+ end
1498
+
1499
+ def scan_opener_offsets(text, opener)
1500
+ offsets = []
1478
1501
  pos = 0
1479
- while (hit = text.index(opener, pos)) && hit < char_offset
1480
- count += 1
1502
+ while (hit = text.index(opener, pos))
1503
+ offsets << hit
1481
1504
  pos = hit + 1
1482
1505
  end
1483
- # Subtract 1 because the count includes the element we are inside
1484
- [count - 1, 0].max
1506
+ offsets
1485
1507
  end
1486
1508
  end
1487
1509
  end
@@ -118,6 +118,13 @@ module Canon
118
118
  after = diff_node.serialized_after&.split("\n") || []
119
119
  fmt = diff_node.formatting?
120
120
  lines = []
121
+ header = parent_chain(diff_node.path)
122
+ if header
123
+ lines << Canon::Diff::DiffLine.new(
124
+ line_number: nil, new_position: nil,
125
+ content: header, type: :location_header
126
+ )
127
+ end
121
128
  require "diff/lcs" unless defined?(::Diff::LCS)
122
129
 
123
130
  ::Diff::LCS.sdiff(before, after).each do |change|
@@ -156,6 +163,27 @@ module Canon
156
163
  lines
157
164
  end
158
165
 
166
+ # Display chain of a DiffNode's ancestor elements, e.g.
167
+ # "bibitem > formattedref" — gives the blank-numbered
168
+ # unlocated lines a readable home (#86).
169
+ def parent_chain(path)
170
+ return nil if path.nil?
171
+
172
+ segments = path.split("/").reject(&:empty?)
173
+ return nil if segments.length < 2
174
+
175
+ chain = segments[0...-1].filter_map do |seg|
176
+ seg[/\A(?:\{[^}]+\})?([a-zA-Z0-9_:-]+)/, 1]
177
+ end
178
+ # Comparator paths repeat the element name before the node
179
+ # segment ("formattedref/formattedref/text()") — collapse
180
+ # consecutive repeats for display.
181
+ collapsed = chain.each_with_object([]) do |name, acc|
182
+ acc << name unless acc.last == name
183
+ end
184
+ collapsed.join(" > ")
185
+ end
186
+
159
187
  # The nearest preceding node (in @differences walk order)
160
188
  # that the enricher DID locate.
161
189
  def preceding_located(diff_node)
@@ -225,6 +253,9 @@ module Canon
225
253
  new_num = (diff_line.new_position || diff_line.line_number)&.+(1)
226
254
  output << format_unified_line(old_num, new_num, " ",
227
255
  diff_line.content)
256
+ when :location_header
257
+ output << format_location_header(diff_line.content)
258
+
228
259
  when :removed
229
260
  line_num = diff_line.line_number&.+(1)
230
261
  formatting = diff_line.formatting?
@@ -992,6 +1023,18 @@ module Canon
992
1023
  end
993
1024
  end
994
1025
 
1026
+ # Dimmed section header for unlocated-node contexts (#86):
1027
+ # "⋯ in bibitem > formattedref"
1028
+ def format_location_header(chain)
1029
+ blank = " " * @line_num_width
1030
+ text = " ⋯ in #{chain}"
1031
+ if @use_color
1032
+ "#{blank}|#{blank} | #{colorize(text, :light_black)}"
1033
+ else
1034
+ "#{blank}|#{blank} | #{text}"
1035
+ end
1036
+ end
1037
+
995
1038
  def format_unified_line(old_num, new_num, marker, content, color = nil,
996
1039
  informative: false, formatting: false)
997
1040
  old_str = old_num ? "%#{@line_num_width}d" % old_num : " " * @line_num_width
data/lib/canon/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Canon
4
- VERSION = "0.3.48"
4
+ VERSION = "0.3.50"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: canon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.48
4
+ version: 0.3.50
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-09-16 00:00:00.000000000 Z
11
+ date: 2026-09-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs