canon 0.3.43 → 0.3.45

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: 88bb496bdbab5cc434e73d3f353563e110bbaf0b73a76f009886e45743206984
4
- data.tar.gz: c00284c8cc531ed825633e8e2250a1dbead21ec4db083529a6bef5d3b909bd4b
3
+ metadata.gz: a17cd55bc56c7287d027191b11cd33a6bc0999b29abce17bb8c368758379e4ad
4
+ data.tar.gz: 7b7c89255ad4134219b512265b6b26c08a1352742dfc1500a821a830fa3cd2d3
5
5
  SHA512:
6
- metadata.gz: 7e32bb5f265281f3ba69a7b9c130025d84f27578084a8a90267edaefc7e5cc6cedd8b2aa72db0a930d38def70ac07f826fc80e767cda72089d3ebedbc88219f4
7
- data.tar.gz: 82a1b117a2f121dbd436962937b1aec7de7ef691b8d15f51fbf69155d6ff9fe09af5ed5f69efdbfa4724af65dbd3858525a56ac22ebb6d603c91ad9b032ab590
6
+ metadata.gz: d4673a7e48e7fb4aa901ece0c6fc3c097e3c046af4aee63b959b218d8809a32ac9e8f4ea3db990ff969f521d9842e826e03f4815da8610fba7138070cc420f8e
7
+ data.tar.gz: f5771189aed0ef86eb98b32b9bd94261221688aece84f6f7b44861c66afcde65d80b3b0a2cb599bb96c18aaa575da4ff86d1bb385b48ff7a3ff34c858342a632
@@ -927,7 +927,7 @@ module Canon
927
927
  # @return [Integer] the last line number
928
928
  def find_end_line(start_line, line_map, content)
929
929
  newline_count = content.count("\n")
930
- [start_line + newline_count, line_map.length - 1].min
930
+ [start_line + newline_count, line_map.line_count - 1].min
931
931
  end
932
932
 
933
933
  # Find the occurrence of a value at a specific element index.
@@ -1016,7 +1016,7 @@ module Canon
1016
1016
  # so the prefix scan counts exactly the preceding siblings —
1017
1017
  # no "inside the element" correction (count_elements_before_
1018
1018
  # position subtracts one for text-node offsets).
1019
- opener = /<#{element_name}[>\s]/
1019
+ opener = opener_for(element_name)
1020
1020
  occurrences = SourceLocator.locate_all(value, text, line_map)
1021
1021
  occurrences.each do |occ|
1022
1022
  count = count_elements_before_position_open(text,
@@ -1093,7 +1093,7 @@ module Canon
1093
1093
  line_map)
1094
1094
  return nil unless line_idx
1095
1095
 
1096
- col = search_start - line_map[line_idx][:start_offset]
1096
+ col = search_start - line_map.start_at(line_idx)
1097
1097
  { char_offset: search_start, line_number: line_idx, col: col }
1098
1098
  end
1099
1099
 
@@ -1265,7 +1265,7 @@ range_start, range_end)
1265
1265
  line_map)
1266
1266
  return nil unless line_idx
1267
1267
 
1268
- col = value_pos - line_map[line_idx][:start_offset]
1268
+ col = value_pos - line_map.start_at(line_idx)
1269
1269
  return { char_offset: value_pos, line_number: line_idx,
1270
1270
  col: col }
1271
1271
  end
@@ -1280,7 +1280,7 @@ range_start, range_end)
1280
1280
  line_map)
1281
1281
  return nil unless line_idx
1282
1282
 
1283
- col = value_pos - line_map[line_idx][:start_offset]
1283
+ col = value_pos - line_map.start_at(line_idx)
1284
1284
  return { char_offset: value_pos, line_number: line_idx, col: col }
1285
1285
  end
1286
1286
 
@@ -1332,7 +1332,7 @@ range_start, range_end)
1332
1332
  line_map)
1333
1333
  return nil unless line_idx
1334
1334
 
1335
- col = value_pos - line_map[line_idx][:start_offset]
1335
+ col = value_pos - line_map.start_at(line_idx)
1336
1336
  return { char_offset: value_pos, line_number: line_idx, col: col }
1337
1337
  end
1338
1338
  end
@@ -1386,7 +1386,7 @@ range_start, range_end)
1386
1386
  line_map)
1387
1387
  return nil unless line_idx
1388
1388
 
1389
- col = anchor_pos - line_map[line_idx][:start_offset]
1389
+ col = anchor_pos - line_map.start_at(line_idx)
1390
1390
  return { char_offset: anchor_pos, line_number: line_idx,
1391
1391
  col: col }
1392
1392
  else
@@ -1395,7 +1395,7 @@ range_start, range_end)
1395
1395
  line_map)
1396
1396
  return nil unless line_idx
1397
1397
 
1398
- col = tag_end_pos - line_map[line_idx][:start_offset]
1398
+ col = tag_end_pos - line_map.start_at(line_idx)
1399
1399
  return { char_offset: tag_end_pos, line_number: line_idx,
1400
1400
  col: col }
1401
1401
  end
@@ -1447,6 +1447,15 @@ range_start, range_end)
1447
1447
  # @param char_offset [Integer] character offset to check before
1448
1448
  # @param element_name [String] name of element to count
1449
1449
  # @return [Integer] element index (0-based) of the element containing the position
1450
+ # Opener regexes memoized per element name — compiling
1451
+ # /<name[>\s]/ per call was an allocation per occurrence check.
1452
+ OPENER_CACHE = {}.compare_by_identity
1453
+ private_constant :OPENER_CACHE
1454
+
1455
+ def opener_for(element_name)
1456
+ OPENER_CACHE[element_name] ||= /<#{element_name}[>\s]/
1457
+ end
1458
+
1450
1459
  # Occurrence count with offsets AT the element's own opening
1451
1460
  # tag (no inside-the-element correction — see
1452
1461
  # locate_element_at_index).
@@ -15,23 +15,51 @@ module Canon
15
15
  # SourceLocator.locate("line2", "line1\nline2\nline3", line_map)
16
16
  # # => { char_offset: 6, line_number: 1, col: 0 }
17
17
  class SourceLocator
18
+ # Line offset map: two flat Integer arrays (starts, ends) — a
19
+ # hash-per-line was one allocation per document line on every
20
+ # enrichment, the presentation stage's quiet constant.
21
+ class LineMap
22
+ attr_reader :starts, :ends
23
+
24
+ def initialize(starts, ends)
25
+ @starts = starts
26
+ @ends = ends
27
+ end
28
+
29
+ def empty?
30
+ @starts.empty?
31
+ end
32
+
33
+ def line_count
34
+ @starts.length
35
+ end
36
+
37
+ def start_at(index)
38
+ @starts[index]
39
+ end
40
+
41
+ def end_at(index)
42
+ @ends[index]
43
+ end
44
+ end
45
+
18
46
  # Build a line offset map from source text.
19
- # Each entry records the start and end character offset of a line.
20
47
  #
21
48
  # @param text [String] the full source text
22
- # @return [Array<Hash>] array of { start_offset:, end_offset: } hashes,
23
- # one per line (0-indexed)
49
+ # @return [LineMap] flat offset arrays, one entry per line
50
+ # (0-indexed)
24
51
  def self.build_line_map(text)
25
- return [] if text.nil? || text.empty?
52
+ return LineMap.new([], []) if text.nil? || text.empty?
26
53
 
27
- map = []
54
+ starts = []
28
55
  offset = 0
29
56
  text.each_line do |line|
30
- line_end = offset + line.length
31
- map << { start_offset: offset, end_offset: line_end }
32
- offset = line_end
57
+ starts << offset
58
+ offset += line.length
33
59
  end
34
- map
60
+ ends = starts[1..] || []
61
+ ends << text.length
62
+ LineMap.new(starts, ends)
35
63
  end
36
64
 
37
65
  # Locate a substring within source text and return its position.
@@ -55,7 +83,7 @@ module Canon
55
83
  line_idx = find_line_for_offset(char_offset, line_map)
56
84
  return nil if line_idx.nil?
57
85
 
58
- col = char_offset - line_map[line_idx][:start_offset]
86
+ col = char_offset - line_map.start_at(line_idx)
59
87
 
60
88
  { char_offset: char_offset, line_number: line_idx, col: col }
61
89
  end
@@ -77,7 +105,7 @@ module Canon
77
105
  line_idx = find_line_for_offset(pos, line_map)
78
106
  break if line_idx.nil?
79
107
 
80
- col = pos - line_map[line_idx][:start_offset]
108
+ col = pos - line_map.start_at(line_idx)
81
109
  results << { char_offset: pos, line_number: line_idx, col: col }
82
110
  offset = pos + 1
83
111
  end
@@ -92,8 +120,8 @@ module Canon
92
120
  # @param line_map [Array<Hash>] the line offset map
93
121
  # @return [Integer, nil] the 0-based line index, or nil
94
122
  def find_line_for_offset(char_offset, line_map)
95
- line_map.bsearch_index do |entry|
96
- entry[:end_offset] > char_offset
123
+ line_map.ends.bsearch_index do |end_offset|
124
+ end_offset > char_offset
97
125
  end
98
126
  end
99
127
  end
@@ -87,25 +87,91 @@ module Canon
87
87
  end
88
88
  return if unlocated.empty?
89
89
 
90
- lines = []
90
+ # Place each unlocated node's context after the report
91
+ # context containing its nearest preceding LOCATED node
92
+ # (@differences is comparator-walk order = document order),
93
+ # so the change reads in position instead of trailing.
91
94
  unlocated.each do |dn|
92
- fmt = dn.formatting?
93
- dn.serialized_before&.split("\n")&.each do |line|
95
+ context = Canon::Diff::DiffContext.new(
96
+ lines: unlocated_lines(dn),
97
+ )
98
+ anchor_index = context_index_after(report, preceding_located(dn))
99
+ report.contexts.insert(anchor_index, context)
100
+ end
101
+ end
102
+
103
+ # Mini-diff of one unlocated node's serialized sides: shared
104
+ # lines render as :unchanged context instead of remove-all +
105
+ # add-all. Blank line numbers throughout — no located position.
106
+ def unlocated_lines(diff_node)
107
+ before = diff_node.serialized_before&.split("\n") || []
108
+ after = diff_node.serialized_after&.split("\n") || []
109
+ fmt = diff_node.formatting?
110
+ lines = []
111
+ require "diff/lcs" unless defined?(::Diff::LCS)
112
+
113
+ ::Diff::LCS.sdiff(before, after).each do |change|
114
+ case change.action
115
+ when "="
94
116
  lines << Canon::Diff::DiffLine.new(
95
- line_number: nil, new_position: nil, content: line,
96
- type: :removed, diff_node: dn, formatting: fmt
117
+ line_number: nil, new_position: nil,
118
+ content: change.old_element, type: :unchanged,
119
+ diff_node: diff_node
97
120
  )
98
- end
99
- next unless dn.serialized_after
100
-
101
- dn.serialized_after.split("\n").each do |line|
121
+ when "-"
122
+ lines << Canon::Diff::DiffLine.new(
123
+ line_number: nil, new_position: nil,
124
+ content: change.old_element, type: :removed,
125
+ diff_node: diff_node, formatting: fmt
126
+ )
127
+ when "+"
102
128
  lines << Canon::Diff::DiffLine.new(
103
- line_number: nil, new_position: nil, content: line,
104
- type: :added, diff_node: dn, formatting: fmt
129
+ line_number: nil, new_position: nil,
130
+ content: change.new_element, type: :added,
131
+ diff_node: diff_node, formatting: fmt
105
132
  )
133
+ when "!"
134
+ lines << Canon::Diff::DiffLine.new(
135
+ line_number: nil, new_position: nil,
136
+ content: change.old_element, type: :removed,
137
+ diff_node: diff_node, formatting: fmt
138
+ )
139
+ lines << Canon::Diff::DiffLine.new(
140
+ line_number: nil, new_position: nil,
141
+ content: change.new_element, type: :added,
142
+ diff_node: diff_node, formatting: fmt
143
+ )
144
+ end
145
+ end
146
+ lines
147
+ end
148
+
149
+ # The nearest preceding node (in @differences walk order)
150
+ # that the enricher DID locate.
151
+ def preceding_located(diff_node)
152
+ index = @differences.index(diff_node)
153
+ return nil unless index
154
+
155
+ (index - 1).downto(0) do |i|
156
+ dn = @differences[i]
157
+ return dn if dn.char_ranges && !dn.char_ranges.empty?
158
+ end
159
+ nil
160
+ end
161
+
162
+ # Index into report.contexts for inserting an unlocated
163
+ # context: after the context holding the anchor node's lines,
164
+ # or at the front when the anchor is nil (nothing precedes).
165
+ def context_index_after(report, anchor_node)
166
+ return 0 if anchor_node.nil?
167
+
168
+ report.contexts.each_with_index do |context, idx|
169
+ has_anchor = context.lines.any? do |dl|
170
+ dl.diff_node&.equal?(anchor_node)
106
171
  end
172
+ return idx + 1 if has_anchor
107
173
  end
108
- report.contexts << Canon::Diff::DiffContext.new(lines: lines)
174
+ report.contexts.length
109
175
  end
110
176
 
111
177
  # Format a DiffReport for display
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.43"
4
+ VERSION = "0.3.45"
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.43
4
+ version: 0.3.45
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-15 00:00:00.000000000 Z
11
+ date: 2026-09-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: diff-lcs