canon 0.3.44 → 0.3.46

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: e7b141fefd0f254d40b0ba7621c7400d882f5a0bcee1fc2f96a51424ce4c7c36
4
- data.tar.gz: '018e400f0949c85ca2db99aa362664e6434834b135e32efc891b9f4b1c794f46'
3
+ metadata.gz: 2dc57b150c1a0a2b9179f4b88a78c22aeb1529aa74d30857d8ca71a6159d6639
4
+ data.tar.gz: 65db8a7cadb8718b11a475858d3bfd5d4adcf0a4c738a7c940a6dfa60a6bfd3e
5
5
  SHA512:
6
- metadata.gz: 0d25eaf6dfa12649678fb9e770762fbf1ad51923d7da03b168aa36acbb11fe3242edb958069b9738bf097aa0c90188498b3408f1fd900589bc091394c578d394
7
- data.tar.gz: e519ee287d5016becf20d31ad057d825d9c6118273305ce713f13c8c0429172283b576fe6a0c477a91cfbda92328b8a4d4f3491b1e6c34e69425d24ff7f12eb1
6
+ metadata.gz: d411dc92e7978a904f303a95397f301286e9a12a3fc3732bb91919c2564375221b12cb13db64198f41942edeb5bbe2dfc8512b173cbfbdab44d27409d5a009a9
7
+ data.tar.gz: eaa339eae29ca147aa6ee9a5cecf9a328e69fc9b1f64b3acb742d11f4d451bef89efc11cde012e06e15f21b10ec09f95fbf36e0456c718ba925c015e47dba0ce
@@ -21,19 +21,22 @@ module Canon
21
21
  # @param text1 [String] The first document (preprocessed)
22
22
  # @param text2 [String] The second document (preprocessed)
23
23
  # @return [Array<DiffLine>] The assembled diff lines
24
- def self.build(diff_nodes, text1, text2)
24
+ def self.build(diff_nodes, text1, text2, lines1: nil, lines2: nil)
25
25
  return [] if diff_nodes.nil? || diff_nodes.empty?
26
26
  return [] if text1.nil? || text2.nil?
27
27
 
28
- new(diff_nodes, text1, text2).build
28
+ new(diff_nodes, text1, text2, lines1, lines2).build
29
29
  end
30
30
 
31
- def initialize(diff_nodes, text1, text2)
31
+ def initialize(diff_nodes, text1, text2, lines1 = nil, lines2 = nil)
32
32
  @diff_nodes = diff_nodes
33
33
  @text1 = text1
34
34
  @text2 = text2
35
- @lines1 = text1.split("\n")
36
- @lines2 = text2.split("\n")
35
+ # The caller may hand in the already-split lines (the
36
+ # formatter splits these documents too); re-splitting both
37
+ # documents per pipeline stage was ~2x the line strings.
38
+ @lines1 = lines1 || text1.split("\n")
39
+ @lines2 = lines2 || text2.split("\n")
37
40
  # Build reverse indices for efficient content lookup in gap handling.
38
41
  # Maps content string to array of line indices where that content appears.
39
42
  @line_to_indices1 = build_line_index(@lines1)
@@ -22,21 +22,22 @@ module Canon
22
22
  # @param text1 [String] The first document (preprocessed)
23
23
  # @param text2 [String] The second document (preprocessed)
24
24
  # @return [Array<DiffNode>] The same DiffNodes, enriched in place
25
- def self.build(diff_nodes, text1, text2)
25
+ def self.build(diff_nodes, text1, text2, lines1: nil, lines2: nil)
26
26
  return diff_nodes if diff_nodes.nil? || diff_nodes.empty?
27
27
  return diff_nodes if text1.nil? || text2.nil?
28
28
 
29
- new(diff_nodes, text1, text2).enrich
29
+ new(diff_nodes, text1, text2, lines1, lines2).enrich
30
30
  end
31
31
 
32
- def initialize(diff_nodes, text1, text2)
32
+ def initialize(diff_nodes, text1, text2, lines1 = nil, lines2 = nil)
33
33
  @diff_nodes = diff_nodes
34
34
  @text1 = text1
35
35
  @text2 = text2
36
36
  @line_map1 = SourceLocator.build_line_map(text1)
37
37
  @line_map2 = SourceLocator.build_line_map(text2)
38
- @lines1 = text1.split("\n")
39
- @lines2 = text2.split("\n")
38
+ # Shared split — see DiffLineBuilder's note.
39
+ @lines1 = lines1 || text1.split("\n")
40
+ @lines2 = lines2 || text2.split("\n")
40
41
  # Track occurrences for text_content dimension to find correct element instance
41
42
  @text_occurrence1 = Hash.new(0)
42
43
  @text_occurrence2 = Hash.new(0)
@@ -927,7 +928,7 @@ module Canon
927
928
  # @return [Integer] the last line number
928
929
  def find_end_line(start_line, line_map, content)
929
930
  newline_count = content.count("\n")
930
- [start_line + newline_count, line_map.length - 1].min
931
+ [start_line + newline_count, line_map.line_count - 1].min
931
932
  end
932
933
 
933
934
  # Find the occurrence of a value at a specific element index.
@@ -1016,7 +1017,7 @@ module Canon
1016
1017
  # so the prefix scan counts exactly the preceding siblings —
1017
1018
  # no "inside the element" correction (count_elements_before_
1018
1019
  # position subtracts one for text-node offsets).
1019
- opener = /<#{element_name}[>\s]/
1020
+ opener = opener_for(element_name)
1020
1021
  occurrences = SourceLocator.locate_all(value, text, line_map)
1021
1022
  occurrences.each do |occ|
1022
1023
  count = count_elements_before_position_open(text,
@@ -1093,7 +1094,7 @@ module Canon
1093
1094
  line_map)
1094
1095
  return nil unless line_idx
1095
1096
 
1096
- col = search_start - line_map[line_idx][:start_offset]
1097
+ col = search_start - line_map.start_at(line_idx)
1097
1098
  { char_offset: search_start, line_number: line_idx, col: col }
1098
1099
  end
1099
1100
 
@@ -1265,7 +1266,7 @@ range_start, range_end)
1265
1266
  line_map)
1266
1267
  return nil unless line_idx
1267
1268
 
1268
- col = value_pos - line_map[line_idx][:start_offset]
1269
+ col = value_pos - line_map.start_at(line_idx)
1269
1270
  return { char_offset: value_pos, line_number: line_idx,
1270
1271
  col: col }
1271
1272
  end
@@ -1280,7 +1281,7 @@ range_start, range_end)
1280
1281
  line_map)
1281
1282
  return nil unless line_idx
1282
1283
 
1283
- col = value_pos - line_map[line_idx][:start_offset]
1284
+ col = value_pos - line_map.start_at(line_idx)
1284
1285
  return { char_offset: value_pos, line_number: line_idx, col: col }
1285
1286
  end
1286
1287
 
@@ -1332,7 +1333,7 @@ range_start, range_end)
1332
1333
  line_map)
1333
1334
  return nil unless line_idx
1334
1335
 
1335
- col = value_pos - line_map[line_idx][:start_offset]
1336
+ col = value_pos - line_map.start_at(line_idx)
1336
1337
  return { char_offset: value_pos, line_number: line_idx, col: col }
1337
1338
  end
1338
1339
  end
@@ -1386,7 +1387,7 @@ range_start, range_end)
1386
1387
  line_map)
1387
1388
  return nil unless line_idx
1388
1389
 
1389
- col = anchor_pos - line_map[line_idx][:start_offset]
1390
+ col = anchor_pos - line_map.start_at(line_idx)
1390
1391
  return { char_offset: anchor_pos, line_number: line_idx,
1391
1392
  col: col }
1392
1393
  else
@@ -1395,7 +1396,7 @@ range_start, range_end)
1395
1396
  line_map)
1396
1397
  return nil unless line_idx
1397
1398
 
1398
- col = tag_end_pos - line_map[line_idx][:start_offset]
1399
+ col = tag_end_pos - line_map.start_at(line_idx)
1399
1400
  return { char_offset: tag_end_pos, line_number: line_idx,
1400
1401
  col: col }
1401
1402
  end
@@ -1447,6 +1448,15 @@ range_start, range_end)
1447
1448
  # @param char_offset [Integer] character offset to check before
1448
1449
  # @param element_name [String] name of element to count
1449
1450
  # @return [Integer] element index (0-based) of the element containing the position
1451
+ # Opener regexes memoized per element name — compiling
1452
+ # /<name[>\s]/ per call was an allocation per occurrence check.
1453
+ OPENER_CACHE = {}.compare_by_identity
1454
+ private_constant :OPENER_CACHE
1455
+
1456
+ def opener_for(element_name)
1457
+ OPENER_CACHE[element_name] ||= /<#{element_name}[>\s]/
1458
+ end
1459
+
1450
1460
  # Occurrence count with offsets AT the element's own opening
1451
1461
  # tag (no inside-the-element correction — see
1452
1462
  # 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
@@ -44,12 +44,22 @@ module Canon
44
44
  # Compute line number width BEFORE formatting
45
45
  compute_line_num_width(doc1, doc2)
46
46
 
47
+ # The pipeline splits both documents once here; every stage
48
+ # downstream reuses the arrays (three redundant full-doc
49
+ # splits per render before).
50
+ @pipeline_lines1 = doc1.split("\n")
51
+ @pipeline_lines2 = doc2.split("\n")
52
+
47
53
  # Phase 1: Enrich DiffNodes with character positions
48
- Canon::Diff::DiffNodeEnricher.build(@differences, doc1, doc2)
54
+ Canon::Diff::DiffNodeEnricher.build(@differences, doc1, doc2,
55
+ lines1: @pipeline_lines1,
56
+ lines2: @pipeline_lines2)
49
57
 
50
58
  # Phase 2: Assemble DiffLines from enriched DiffNodes
51
59
  diff_lines = Canon::Diff::DiffLineBuilder.build(@differences, doc1,
52
- doc2)
60
+ doc2,
61
+ lines1: @pipeline_lines1,
62
+ lines2: @pipeline_lines2)
53
63
 
54
64
  # Layers 3-5: Build report through pipeline
55
65
  report = Canon::Diff::DiffReportBuilder.build(
@@ -178,8 +188,8 @@ module Canon
178
188
  def format_report(report, doc1, doc2)
179
189
  return "" if report.contexts.empty?
180
190
 
181
- lines1 = doc1.split("\n")
182
- lines2 = doc2.split("\n")
191
+ lines1 = @pipeline_lines1 || doc1.split("\n")
192
+ lines2 = @pipeline_lines2 || doc2.split("\n")
183
193
 
184
194
  output = []
185
195
 
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.44"
4
+ VERSION = "0.3.46"
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.44
4
+ version: 0.3.46
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