metanorma-utils 2.0.0 → 2.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3356c5d36c4605ce33d895949bb01b4bc59c823ef73fa6974844746578347087
4
- data.tar.gz: 13f3f9e75e204cb1c152186e9c0feae34b02542dde612626535d598022a8cd50
3
+ metadata.gz: a291c816aab2db45409cc9c255e5dc4214cac348b2161941226f6dcea35068f0
4
+ data.tar.gz: c9d66f40e1f454cb59dca034a5db883f28b7fe625d9a83a29b7d0142721234da
5
5
  SHA512:
6
- metadata.gz: f90ebfb556dadc68dde7ed04ed93f44819e69a60fe0daab2775d9cb61726b12222cfe187456770f92399a250dd6d52b079a56f6ad6a2da0a253b72ca7a7a7c03
7
- data.tar.gz: 25a38f448ca9883380623391f12f8ee6c83b1ed8f4556cab5f6eca39baea3fd79cbbdd8553cc42b265a2b64dbd2e1078a1e7c392a28f9f98a27479f0d0019b86
6
+ metadata.gz: 7a84e5fd6d93b9ed55259e1702214bf00183fa739f057533efb456e0350c54fb1b7f7c0166e11687f610adf8be5e344d5117ea187c876b29e762d17a283b3e51
7
+ data.tar.gz: 73a7acbae9dfa698a47e93e7d5dc3b0175df6c6430458160a3a8b5c6fc75a5d4f11c553200235e3d4321959b0d18b85d075e0d87a3ed8cf802f0f73e8f562a84
@@ -6,3 +6,4 @@ require_relative "utils/linestatus"
6
6
  require_relative "utils/log"
7
7
  require_relative "utils/xml"
8
8
  require_relative "utils/anchor"
9
+ require_relative "utils/anchor_ranges"
@@ -0,0 +1,148 @@
1
+ module Metanorma
2
+ module Utils
3
+ # AnchorRanges provides efficient range checking for nodes based on
4
+ # anchor positions in a document. It determines whether an arbitrary node
5
+ # falls within the range # defined by two anchor points (A-B), where
6
+ # the range includes node A through all descendants of node B.
7
+ class AnchorRanges
8
+ attr_reader :anchor_map
9
+
10
+ # Initialize with a Nokogiri document
11
+ # @param doc [Nokogiri::XML::Document] The document to process
12
+ def initialize(doc)
13
+ @anchor_map = build_anchor_id_map(doc)
14
+ @anchor_to_ord = nil
15
+ @anchor_to_last_ord = nil
16
+ @id_to_ord = nil
17
+ end
18
+
19
+ # Get mapping of anchor to ord
20
+ # @return [Hash] anchor => ord
21
+ def anchor_to_ord
22
+ @anchor_to_ord ||= build_anchor_to_ord
23
+ end
24
+
25
+ # Get mapping of anchor to last descendant ord
26
+ # @return [Hash] anchor => last_ord
27
+ def anchor_to_last_ord
28
+ @anchor_to_last_ord ||= build_anchor_to_last_ord
29
+ end
30
+
31
+ # Get mapping of id to ord
32
+ # @return [Hash] id => ord
33
+ def id_to_ord
34
+ @id_to_ord ||= build_id_to_ord
35
+ end
36
+
37
+ # Check if a node (by id or anchor) is within the range A-B
38
+ # @param node_id_or_anchor [String] The id or anchor of the node to check
39
+ # @param anchor_a [String] The anchor defining the start of the range
40
+ # @param anchor_b [String] The anchor defining the end of the range
41
+ # @return [Boolean] true if the node is within the range A-B
42
+ def in_range?(node_id_or_anchor, anchor_a, anchor_b)
43
+ node_ord = find_node_ord(node_id_or_anchor)
44
+ return false if node_ord.nil?
45
+
46
+ start_ord = anchor_to_ord[anchor_a]
47
+ end_ord = anchor_to_last_ord[anchor_b]
48
+
49
+ return false if start_ord.nil? || end_ord.nil?
50
+
51
+ node_ord >= start_ord && node_ord <= end_ord
52
+ end
53
+
54
+ # Get the ordinal range for an anchor (start to last descendant)
55
+ # @param anchor [String] The anchor to get the range for
56
+ # @return [Range, nil] The range of ordinals, or nil if anchor not found
57
+ def anchor_range(anchor)
58
+ start_ord = anchor_to_ord[anchor]
59
+ end_ord = anchor_to_last_ord[anchor]
60
+ return nil if start_ord.nil? || end_ord.nil?
61
+
62
+ (start_ord..end_ord)
63
+ end
64
+
65
+ private
66
+
67
+ # Generate a map of all nodes with anchor or id attributes,
68
+ # recording their linear order and the next non-descendant anchor
69
+ # @return [Array<Hash>] Array of hashes with keys: :anchor, :id, :ord, :next_anchor
70
+ def build_anchor_id_map(doc)
71
+ nodes = doc.xpath("//*[@id or @anchor]")
72
+ nodes.each_with_index.map do |node, i|
73
+ {
74
+ anchor: node["anchor"],
75
+ id: node["id"],
76
+ ord: i,
77
+ next_anchor: find_next_non_descendant_anchor(nodes, i),
78
+ }
79
+ end
80
+ end
81
+
82
+ # Find the anchor attribute of the next node that is not a descendant
83
+ # of the node at the given index
84
+ #
85
+ # @param nodes [Nokogiri::XML::NodeSet] All nodes with anchor or id
86
+ # @param current_index [Integer] Index of the current node
87
+ # @return [String, nil] The anchor attribute of the next non-descendant node
88
+ def find_next_non_descendant_anchor(nodes, current_index)
89
+ current_node = nodes[current_index]
90
+ current_path = current_node.path
91
+
92
+ # Look through subsequent nodes
93
+ ((current_index + 1)...nodes.length).each do |i|
94
+ next_node = nodes[i]
95
+ next_path = next_node.path
96
+
97
+ # Check if next_node is a descendant of current_node
98
+ # A node is a descendant if its path starts with the current path
99
+ # followed by a path separator
100
+ unless next_path.start_with?("#{current_path}/")
101
+ return next_node["anchor"]
102
+ end
103
+ end
104
+
105
+ nil # No non-descendant node found
106
+ end
107
+
108
+ def build_anchor_to_ord
109
+ hash = {}
110
+ @anchor_map.each do |entry|
111
+ hash[entry[:anchor]] = entry[:ord] if entry[:anchor]
112
+ end
113
+ hash
114
+ end
115
+
116
+ def build_id_to_ord
117
+ hash = {}
118
+ @anchor_map.each do |entry|
119
+ hash[entry[:id]] = entry[:ord] if entry[:id]
120
+ end
121
+ hash
122
+ end
123
+
124
+ def build_anchor_to_last_ord
125
+ hash = {}
126
+ @anchor_map.each do |entry|
127
+ next unless entry[:anchor]
128
+
129
+ # The last descendant is the ord right before the next_anchor
130
+ # If there's no next_anchor, it's the last node in the map
131
+ if entry[:next_anchor]
132
+ next_ord = anchor_to_ord[entry[:next_anchor]]
133
+ hash[entry[:anchor]] = next_ord - 1 if next_ord
134
+ else
135
+ # No next anchor means this extends to the end of the document
136
+ hash[entry[:anchor]] = @anchor_map.last[:ord]
137
+ end
138
+ end
139
+ hash
140
+ end
141
+
142
+ def find_node_ord(node_id_or_anchor)
143
+ # Try as anchor first, then as id
144
+ anchor_to_ord[node_id_or_anchor] || id_to_ord[node_id_or_anchor]
145
+ end
146
+ end
147
+ end
148
+ end
data/lib/utils/log.rb CHANGED
@@ -4,7 +4,7 @@ require_relative "log_html"
4
4
  module Metanorma
5
5
  module Utils
6
6
  class Log
7
- attr_writer :xml, :suppress_log
7
+ attr_accessor :suppress_log
8
8
 
9
9
  # messages: hash of message IDs to {error, severity, category}
10
10
  # severity: 0: abort; 1: serious; 2: not serious; 3: info only
@@ -12,7 +12,8 @@ module Metanorma
12
12
  @log = {}
13
13
  @c = HTMLEntities.new
14
14
  @mapid = {}
15
- @suppress_log = { severity: 4, category: [] }
15
+ @suppress_log = { severity: 4, category: [], error_ids: [],
16
+ locations: [] }
16
17
  @msg = messages.each_value do |v|
17
18
  v[:error] = v[:error]
18
19
  .encode("UTF-8", invalid: :replace, undef: :replace)
@@ -23,6 +24,12 @@ module Metanorma
23
24
  @msg.merge!(messages)
24
25
  end
25
26
 
27
+ # pass Nokogiri XML in, to record where all the anchors and ids
28
+ # are in the target document
29
+ def add_error_ranges(xml)
30
+ @anchor_ranges = AnchorRanges.new(xml)
31
+ end
32
+
26
33
  def save_to(filename, dir = nil)
27
34
  dir ||= File.dirname(filename)
28
35
  new_fn = filename.sub(/\.err\.html$/, ".html")
@@ -41,8 +48,7 @@ module Metanorma
41
48
 
42
49
  def add(id, loc, display: true, params: [])
43
50
  m = add_prep(id) or return
44
- msg = create_entry(loc, m[:error],
45
- m[:severity], params)
51
+ msg = create_entry(loc, m[:error], m[:severity], id, params)
46
52
  @log[m[:category]] << msg
47
53
  loc = loc.nil? ? "" : "(#{current_location(loc)}): "
48
54
  suppress_display?(m[:category], loc, msg, display) or
@@ -69,7 +75,8 @@ module Metanorma
69
75
  category = @msg[id][:category]
70
76
  category && /^Fetching /.match?(@msg[id][:error]) ||
71
77
  @suppress_log[:severity] <= @msg[id][:severity] ||
72
- @suppress_log[:category].include?(category)
78
+ @suppress_log[:category].include?(category) ||
79
+ @suppress_log[:error_ids].include?(id.to_s)
73
80
  end
74
81
 
75
82
  def suppress_display?(category, _loc, _msg, display)
@@ -77,11 +84,11 @@ module Metanorma
77
84
  !display
78
85
  end
79
86
 
80
- def create_entry(loc, msg, severity, params)
81
- interpolated = interpolate_msg(msg, params)
82
- item = { location: current_location(loc), severity: severity,
83
- error: interpolated, context: context(loc),
84
- line: line(loc, msg) }
87
+ def create_entry(loc, msg, severity, error_id, params)
88
+ loc_str, anchor, node_id = current_location(loc)
89
+ item = { error_id: error_id, location: loc_str, severity: severity,
90
+ error: interpolate_msg(msg, params), context: context(loc),
91
+ line: line(loc, msg), anchor: anchor, id: node_id }
85
92
  if item[:error].include?(" :: ")
86
93
  a = item[:error].split(" :: ", 2)
87
94
  item[:context] = a[1]
@@ -102,35 +109,47 @@ module Metanorma
102
109
  end
103
110
 
104
111
  def current_location(node)
105
- if node.nil? then ""
106
- elsif node.respond_to?(:id) && !node.id.nil? then "ID #{node.id}"
107
- elsif node.respond_to?(:id) && node.id.nil? && node.respond_to?(:parent)
108
- while !node.nil? && node.id.nil?
109
- node = node.parent
110
- end
111
- node.nil? ? "" : "ID #{node.id}"
112
- elsif node.respond_to?(:to_xml) && node.respond_to?(:parent)
113
- while !node.nil? && node["id"].nil? && node.respond_to?(:parent)
114
- node = node.parent
115
- end
116
- node.respond_to?(:parent) ? "ID #{node['anchor'] || node['id']}" : ""
117
- elsif node.is_a? String then node
118
- elsif node.respond_to?(:lineno) && !node.lineno.nil? &&
119
- !node.lineno.empty?
120
- "Asciidoctor Line #{'%06d' % node.lineno}"
121
- elsif node.respond_to?(:line) && !node.line.nil?
122
- "XML Line #{'%06d' % node.line}"
123
- elsif node.respond_to?(:parent)
124
- while !node.nil? &&
125
- (!node.respond_to?(:level) || node.level.positive?) &&
126
- (!node.respond_to?(:context) || node.context != :section)
127
- node = node.parent
128
- return "Section: #{node.title}" if node.respond_to?(:context) &&
129
- node&.context == :section
130
- end
131
- "??"
132
- else "??"
112
+ anchor = nil
113
+ id = nil
114
+ ret = if node.nil? then ""
115
+ elsif node.respond_to?(:id) && !node.id.nil? then "ID #{node.id}"
116
+ elsif node.respond_to?(:id) && node.id.nil? &&
117
+ node.respond_to?(:parent)
118
+ while !node.nil? && node.id.nil?
119
+ node = node.parent
120
+ end
121
+ node.nil? ? "" : "ID #{node.id}"
122
+ elsif node.respond_to?(:to_xml) && node.respond_to?(:parent)
123
+ loc, anchor, id = xml_current_location(node)
124
+ loc
125
+ elsif node.is_a? String then node
126
+ elsif node.respond_to?(:lineno) && !node.lineno.nil? &&
127
+ !node.lineno.empty?
128
+ "Asciidoctor Line #{'%06d' % node.lineno}"
129
+ elsif node.respond_to?(:line) && !node.line.nil?
130
+ "XML Line #{'%06d' % node.line}"
131
+ elsif node.respond_to?(:parent)
132
+ while !node.nil? &&
133
+ (!node.respond_to?(:level) || node.level.positive?) &&
134
+ (!node.respond_to?(:context) || node.context != :section)
135
+ node = node.parent
136
+ node.respond_to?(:context) && node&.context == :section and
137
+ return "Section: #{node.title}"
138
+ end
139
+ "??"
140
+ else "??"
141
+ end
142
+ [ret, anchor, id]
143
+ end
144
+
145
+ def xml_current_location(node)
146
+ while !node.nil? && node["id"].nil? && node.respond_to?(:parent)
147
+ node = node.parent
133
148
  end
149
+ anchor = node["anchor"]
150
+ id = node["id"]
151
+ loc = node.respond_to?(:parent) ? "ID #{anchor || id}" : ""
152
+ [loc, anchor, id]
134
153
  end
135
154
 
136
155
  def line(node, msg)
@@ -160,6 +179,40 @@ module Metanorma
160
179
  end
161
180
  ret.to_xml
162
181
  end
182
+
183
+ def filter_locations?
184
+ @suppress_log[:locations] && !@suppress_log[:locations].empty? or return
185
+ @anchor_ranges or return
186
+ true
187
+ end
188
+
189
+ def filter_locations
190
+ filter_locations? or return
191
+ @log.transform_values! do |entries|
192
+ entries.reject do |entry|
193
+ # Use anchor if present, otherwise use id
194
+ entry_in_suppress_range?(entry, entry[:anchor] || entry[:id])
195
+ end
196
+ end
197
+ end
198
+
199
+ def entry_in_suppress_range_prep(entry)
200
+ entry[:to] ||= entry[:from]
201
+ entry[:error_ids] ||= []
202
+ entry
203
+ end
204
+
205
+ def entry_in_suppress_range?(entry, id)
206
+ # Use anchor if present, otherwise use id
207
+ id.nil? and return false
208
+ @suppress_log[:locations].each do |loc|
209
+ entry_in_suppress_range_prep(loc)
210
+ @anchor_ranges.in_range?(id, loc[:from], loc[:to]) or next
211
+ loc[:error_ids].empty? || loc[:error_ids]
212
+ .include?(entry[:error_id].to_s) and return true
213
+ end
214
+ false
215
+ end
163
216
  end
164
217
  end
165
218
  end
@@ -35,12 +35,14 @@ module Metanorma
35
35
  m[e[:severity]] += 1
36
36
  end.compact
37
37
  s.keys.sort.map do |k|
38
- "Severity #{k}: <b>#{s[k]}</b> errors"
38
+ error = s[k] == 1 ? "error" : "errors"
39
+ "Severity #{k}: <b>#{s[k]}</b> #{error}"
39
40
  end.join("; ")
40
41
  end
41
42
 
42
43
  def write(file = nil)
43
44
  (!file && @filename) or save_to(file || "metanorma", nil)
45
+ filter_locations
44
46
  File.open(@filename, "w:UTF-8") do |f|
45
47
  f.puts log_hdr(@filename)
46
48
  @log.each_key { |key| write_key(f, key) }
@@ -51,8 +53,8 @@ module Metanorma
51
53
  def write_key(file, key)
52
54
  file.puts <<~HTML
53
55
  <h2 id="#{to_ncname(key)}">#{key}</h2>\n<table border="1">
54
- <thead><th width="5%">Line</th><th width="20%">ID</th>
55
- <th width="30%">Message</th><th width="40%">Context</th><th width="5%">Severity</th></thead>
56
+ <thead><th width="5%">Line</th><th width="20%">ID</th><th width="10%">Error</th>
57
+ <th width="20%">Message</th><th width="40%">Context</th><th width="5%">Severity</th></thead>
56
58
  <tbody>
57
59
  HTML
58
60
  @log[key].sort_by { |a| [a[:line], a[:location], a[:error]] }
@@ -107,7 +109,7 @@ module Metanorma
107
109
  entry[:context] &&= @c.encode(break_up_long_str(entry[:context], 40, 2))
108
110
  file.print <<~HTML
109
111
  <tr class="severity#{entry[:severity]}">
110
- <td>#{entry[:line]}</td><th><code>#{entry[:location]}</code></th>
112
+ <td>#{entry[:line]}</td><th><code>#{entry[:location]}</code></th><td>#{entry[:error_id]}</td>
111
113
  <td>#{entry[:error]}</td><td><pre>#{entry[:context]}</pre></td><td>#{entry[:severity]}</td></tr>
112
114
  HTML
113
115
  end
@@ -115,7 +117,7 @@ module Metanorma
115
117
  def display_messages
116
118
  grouped = group_messages_by_category
117
119
  grouped.map { |cat, keys| format_category_section(cat, keys) }
118
- .join("\n")
120
+ .join("\n\n")
119
121
  end
120
122
 
121
123
  def group_messages_by_category
@@ -131,7 +133,7 @@ module Metanorma
131
133
 
132
134
  def format_error_line(key)
133
135
  padded_key = key.to_s.ljust(12)
134
- "\t#{padded_key}: #{@msg[key][:error]}"
136
+ "\t#{padded_key}: #{@msg[key][:error].gsub("\n", ' ')}"
135
137
  end
136
138
 
137
139
  def sort_messages_by_category_and_key
data/lib/utils/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  module Metanorma
2
2
  module Utils
3
- VERSION = "2.0.0".freeze
3
+ VERSION = "2.0.1".freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metanorma-utils
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-11-03 00:00:00.000000000 Z
11
+ date: 2025-11-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -308,6 +308,7 @@ files:
308
308
  - lib/metanorma-utils.rb
309
309
  - lib/sterile/sterile.rb
310
310
  - lib/utils/anchor.rb
311
+ - lib/utils/anchor_ranges.rb
311
312
  - lib/utils/cjk.rb
312
313
  - lib/utils/hash_transform_keys.rb
313
314
  - lib/utils/image.rb