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 +4 -4
- data/lib/metanorma-utils.rb +1 -0
- data/lib/utils/anchor_ranges.rb +148 -0
- data/lib/utils/log.rb +91 -38
- data/lib/utils/log_html.rb +8 -6
- data/lib/utils/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a291c816aab2db45409cc9c255e5dc4214cac348b2161941226f6dcea35068f0
|
|
4
|
+
data.tar.gz: c9d66f40e1f454cb59dca034a5db883f28b7fe625d9a83a29b7d0142721234da
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7a84e5fd6d93b9ed55259e1702214bf00183fa739f057533efb456e0350c54fb1b7f7c0166e11687f610adf8be5e344d5117ea187c876b29e762d17a283b3e51
|
|
7
|
+
data.tar.gz: 73a7acbae9dfa698a47e93e7d5dc3b0175df6c6430458160a3a8b5c6fc75a5d4f11c553200235e3d4321959b0d18b85d075e0d87a3ed8cf802f0f73e8f562a84
|
data/lib/metanorma-utils.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
82
|
-
item = { location:
|
|
83
|
-
error:
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
data/lib/utils/log_html.rb
CHANGED
|
@@ -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
|
-
|
|
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="
|
|
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
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.
|
|
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-
|
|
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
|