metanorma-utils 1.11.8 → 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 +124 -151
- data/lib/utils/log_html.rb +165 -0
- data/lib/utils/version.rb +1 -1
- data/metanorma-utils.gemspec +1 -1
- metadata +8 -6
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
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
require "htmlentities"
|
|
2
|
+
require_relative "log_html"
|
|
2
3
|
|
|
3
4
|
module Metanorma
|
|
4
5
|
module Utils
|
|
5
6
|
class Log
|
|
6
|
-
|
|
7
|
+
attr_accessor :suppress_log
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
# messages: hash of message IDs to {error, severity, category}
|
|
10
|
+
# severity: 0: abort; 1: serious; 2: not serious; 3: info only
|
|
11
|
+
def initialize(messages = {})
|
|
9
12
|
@log = {}
|
|
10
13
|
@c = HTMLEntities.new
|
|
11
14
|
@mapid = {}
|
|
12
|
-
@suppress_log = { severity: 4, category: []
|
|
15
|
+
@suppress_log = { severity: 4, category: [], error_ids: [],
|
|
16
|
+
locations: [] }
|
|
17
|
+
@msg = messages.each_value do |v|
|
|
18
|
+
v[:error] = v[:error]
|
|
19
|
+
.encode("UTF-8", invalid: :replace, undef: :replace)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def add_msg(messages)
|
|
24
|
+
@msg.merge!(messages)
|
|
13
25
|
end
|
|
14
26
|
|
|
15
|
-
|
|
16
|
-
|
|
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)
|
|
17
31
|
end
|
|
18
32
|
|
|
19
33
|
def save_to(filename, dir = nil)
|
|
@@ -24,21 +38,27 @@ module Metanorma
|
|
|
24
38
|
@htmlfilename = "#{b}.html"
|
|
25
39
|
end
|
|
26
40
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
@
|
|
30
|
-
@
|
|
31
|
-
|
|
32
|
-
@
|
|
41
|
+
def add_prep(id)
|
|
42
|
+
id = id.to_sym
|
|
43
|
+
@msg[id] or raise "Logging: Error #{id} is not defined!"
|
|
44
|
+
@novalid || suppress_log?(id) and return nil
|
|
45
|
+
@log[@msg[id][:category]] ||= []
|
|
46
|
+
@msg[id]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def add(id, loc, display: true, params: [])
|
|
50
|
+
m = add_prep(id) or return
|
|
51
|
+
msg = create_entry(loc, m[:error], m[:severity], id, params)
|
|
52
|
+
@log[m[:category]] << msg
|
|
33
53
|
loc = loc.nil? ? "" : "(#{current_location(loc)}): "
|
|
34
|
-
suppress_display?(category, loc, msg, display) or
|
|
35
|
-
warn "#{category}: #{loc}#{msg}"
|
|
54
|
+
suppress_display?(m[:category], loc, msg, display) or
|
|
55
|
+
warn "#{m[:category]}: #{loc}#{msg[:error]}"
|
|
36
56
|
end
|
|
37
57
|
|
|
38
58
|
def abort_messages
|
|
39
59
|
@log.values.each_with_object([]) do |v, m|
|
|
40
60
|
v.each do |e|
|
|
41
|
-
e[:severity].zero? and m << e[:
|
|
61
|
+
e[:severity].zero? and m << e[:error]
|
|
42
62
|
end
|
|
43
63
|
end
|
|
44
64
|
end
|
|
@@ -51,10 +71,12 @@ module Metanorma
|
|
|
51
71
|
end
|
|
52
72
|
end
|
|
53
73
|
|
|
54
|
-
def suppress_log?(
|
|
55
|
-
category
|
|
56
|
-
|
|
57
|
-
@suppress_log[:
|
|
74
|
+
def suppress_log?(id)
|
|
75
|
+
category = @msg[id][:category]
|
|
76
|
+
category && /^Fetching /.match?(@msg[id][:error]) ||
|
|
77
|
+
@suppress_log[:severity] <= @msg[id][:severity] ||
|
|
78
|
+
@suppress_log[:category].include?(category) ||
|
|
79
|
+
@suppress_log[:error_ids].include?(id.to_s)
|
|
58
80
|
end
|
|
59
81
|
|
|
60
82
|
def suppress_display?(category, _loc, _msg, display)
|
|
@@ -62,48 +84,72 @@ module Metanorma
|
|
|
62
84
|
!display
|
|
63
85
|
end
|
|
64
86
|
|
|
65
|
-
def create_entry(loc, msg, severity)
|
|
66
|
-
|
|
67
|
-
item = { location:
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 }
|
|
92
|
+
if item[:error].include?(" :: ")
|
|
93
|
+
a = item[:error].split(" :: ", 2)
|
|
71
94
|
item[:context] = a[1]
|
|
72
|
-
item[:
|
|
95
|
+
item[:error] = a[0]
|
|
73
96
|
end
|
|
74
97
|
item
|
|
75
98
|
end
|
|
76
99
|
|
|
100
|
+
def interpolate_msg(msg, params)
|
|
101
|
+
# Count %s placeholders in the message
|
|
102
|
+
placeholder_count = msg.scan(/%s/).length
|
|
103
|
+
interpolation_params = if params.empty?
|
|
104
|
+
::Array.new(placeholder_count, "")
|
|
105
|
+
else
|
|
106
|
+
params
|
|
107
|
+
end
|
|
108
|
+
placeholder_count.zero? ? msg : (msg % interpolation_params)
|
|
109
|
+
end
|
|
110
|
+
|
|
77
111
|
def current_location(node)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
106
148
|
end
|
|
149
|
+
anchor = node["anchor"]
|
|
150
|
+
id = node["id"]
|
|
151
|
+
loc = node.respond_to?(:parent) ? "ID #{anchor || id}" : ""
|
|
152
|
+
[loc, anchor, id]
|
|
107
153
|
end
|
|
108
154
|
|
|
109
155
|
def line(node, msg)
|
|
@@ -134,111 +180,38 @@ module Metanorma
|
|
|
134
180
|
ret.to_xml
|
|
135
181
|
end
|
|
136
182
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
<style> pre { white-space: pre-wrap; }
|
|
142
|
-
thead th { font-weight: bold; background-color: aqua; }
|
|
143
|
-
.severity0 { font-weight: bold; background-color: lightpink }
|
|
144
|
-
.severity1 { font-weight: bold; }
|
|
145
|
-
.severity2 { }
|
|
146
|
-
.severity3 { font-style: italic; color: grey; }
|
|
147
|
-
</style>
|
|
148
|
-
</head><body><h1>#{file} errors</h1>
|
|
149
|
-
<ul>#{log_index}</ul>
|
|
150
|
-
HTML
|
|
151
|
-
end
|
|
152
|
-
|
|
153
|
-
def log_index
|
|
154
|
-
@log.each_with_object([]) do |(k, v), m|
|
|
155
|
-
m << <<~HTML
|
|
156
|
-
<li><p><b><a href="##{to_ncname(k)}">#{k}</a></b>: #{index_severities(v)}</p></li>
|
|
157
|
-
HTML
|
|
158
|
-
end.join("\n")
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
def index_severities(entries)
|
|
162
|
-
s = entries.each_with_object({}) do |e, m|
|
|
163
|
-
m[e[:severity]] ||= 0
|
|
164
|
-
m[e[:severity]] += 1
|
|
165
|
-
end.compact
|
|
166
|
-
s.keys.sort.map do |k|
|
|
167
|
-
"Severity #{k}: <b>#{s[k]}</b> errors"
|
|
168
|
-
end.join("; ")
|
|
169
|
-
end
|
|
170
|
-
|
|
171
|
-
def write(file = nil)
|
|
172
|
-
(!file && @filename) or save_to(file || "metanorma", nil)
|
|
173
|
-
File.open(@filename, "w:UTF-8") do |f|
|
|
174
|
-
f.puts log_hdr(@filename)
|
|
175
|
-
@log.each_key { |key| write_key(f, key) }
|
|
176
|
-
f.puts "</body></html>\n"
|
|
177
|
-
end
|
|
183
|
+
def filter_locations?
|
|
184
|
+
@suppress_log[:locations] && !@suppress_log[:locations].empty? or return
|
|
185
|
+
@anchor_ranges or return
|
|
186
|
+
true
|
|
178
187
|
end
|
|
179
188
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
@log[key].sort_by { |a| [a[:line], a[:location], a[:message]] }
|
|
188
|
-
.each do |n|
|
|
189
|
-
write_entry(file, render_preproc_entry(n))
|
|
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
|
|
190
196
|
end
|
|
191
|
-
file.puts "</tbody></table>\n"
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def render_preproc_entry(entry)
|
|
195
|
-
ret = entry.dup
|
|
196
|
-
ret[:line] = nil if ret[:line] == "000000"
|
|
197
|
-
ret[:location] = loc_link(entry)
|
|
198
|
-
ret[:message] = break_up_long_str(entry[:message], 10, 2)
|
|
199
|
-
.gsub(/`([^`]+)`/, "<code>\\1</code>")
|
|
200
|
-
ret[:context] = context_render(entry)
|
|
201
|
-
ret.compact
|
|
202
|
-
end
|
|
203
|
-
|
|
204
|
-
def context_render(entry)
|
|
205
|
-
entry[:context] or return nil
|
|
206
|
-
entry[:context].split("\n").first(5)
|
|
207
|
-
.join("\n").gsub("><", "> <")
|
|
208
|
-
end
|
|
209
|
-
|
|
210
|
-
def mapid(old, new)
|
|
211
|
-
@mapid[old] = new
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def loc_link(entry)
|
|
215
|
-
loc = entry[:location]
|
|
216
|
-
loc.nil? || loc.empty? and loc = "--"
|
|
217
|
-
loc, url = loc_to_url(loc)
|
|
218
|
-
loc &&= break_up_long_str(loc, 10, 2)
|
|
219
|
-
url and loc = "<a href='#{url}'>#{loc}</a>"
|
|
220
|
-
loc
|
|
221
197
|
end
|
|
222
198
|
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
url = "#{@htmlfilename}##{to_ncname loc}"
|
|
228
|
-
[loc, url]
|
|
199
|
+
def entry_in_suppress_range_prep(entry)
|
|
200
|
+
entry[:to] ||= entry[:from]
|
|
201
|
+
entry[:error_ids] ||= []
|
|
202
|
+
entry
|
|
229
203
|
end
|
|
230
204
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
HTML
|
|
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
|
|
242
215
|
end
|
|
243
216
|
end
|
|
244
217
|
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
module Metanorma
|
|
2
|
+
module Utils
|
|
3
|
+
class Log
|
|
4
|
+
def to_ncname(tag)
|
|
5
|
+
::Metanorma::Utils.to_ncname(tag)
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def log_hdr(file)
|
|
9
|
+
<<~HTML
|
|
10
|
+
<html><head><title>#{file} errors</title>
|
|
11
|
+
<meta charset="UTF-8"/>
|
|
12
|
+
<style> pre { white-space: pre-wrap; }
|
|
13
|
+
thead th { font-weight: bold; background-color: aqua; }
|
|
14
|
+
.severity0 { font-weight: bold; background-color: lightpink }
|
|
15
|
+
.severity1 { font-weight: bold; }
|
|
16
|
+
.severity2 { }
|
|
17
|
+
.severity3 { font-style: italic; color: grey; }
|
|
18
|
+
</style>
|
|
19
|
+
</head><body><h1>#{file} errors</h1>
|
|
20
|
+
<ul>#{log_index}</ul>
|
|
21
|
+
HTML
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def log_index
|
|
25
|
+
@log.each_with_object([]) do |(k, v), m|
|
|
26
|
+
m << <<~HTML
|
|
27
|
+
<li><p><b><a href="##{to_ncname(k)}">#{k}</a></b>: #{index_severities(v)}</p></li>
|
|
28
|
+
HTML
|
|
29
|
+
end.join("\n")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def index_severities(entries)
|
|
33
|
+
s = entries.each_with_object({}) do |e, m|
|
|
34
|
+
m[e[:severity]] ||= 0
|
|
35
|
+
m[e[:severity]] += 1
|
|
36
|
+
end.compact
|
|
37
|
+
s.keys.sort.map do |k|
|
|
38
|
+
error = s[k] == 1 ? "error" : "errors"
|
|
39
|
+
"Severity #{k}: <b>#{s[k]}</b> #{error}"
|
|
40
|
+
end.join("; ")
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def write(file = nil)
|
|
44
|
+
(!file && @filename) or save_to(file || "metanorma", nil)
|
|
45
|
+
filter_locations
|
|
46
|
+
File.open(@filename, "w:UTF-8") do |f|
|
|
47
|
+
f.puts log_hdr(@filename)
|
|
48
|
+
@log.each_key { |key| write_key(f, key) }
|
|
49
|
+
f.puts "</body></html>\n"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def write_key(file, key)
|
|
54
|
+
file.puts <<~HTML
|
|
55
|
+
<h2 id="#{to_ncname(key)}">#{key}</h2>\n<table border="1">
|
|
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>
|
|
58
|
+
<tbody>
|
|
59
|
+
HTML
|
|
60
|
+
@log[key].sort_by { |a| [a[:line], a[:location], a[:error]] }
|
|
61
|
+
.each do |n|
|
|
62
|
+
write_entry(file, render_preproc_entry(n))
|
|
63
|
+
end
|
|
64
|
+
file.puts "</tbody></table>\n"
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_preproc_entry(entry)
|
|
68
|
+
ret = entry.dup
|
|
69
|
+
ret[:line] = nil if ret[:line] == "000000"
|
|
70
|
+
ret[:location] = loc_link(entry)
|
|
71
|
+
ret[:error] = break_up_long_str(entry[:error], 10, 2)
|
|
72
|
+
.gsub(/`([^`]+)`/, "<code>\\1</code>")
|
|
73
|
+
ret[:context] = context_render(entry)
|
|
74
|
+
ret.compact
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def context_render(entry)
|
|
78
|
+
entry[:context] or return nil
|
|
79
|
+
entry[:context].split("\n").first(5)
|
|
80
|
+
.join("\n").gsub("><", "> <")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def mapid(old, new)
|
|
84
|
+
@mapid[old] = new
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def loc_link(entry)
|
|
88
|
+
loc = entry[:location]
|
|
89
|
+
loc.nil? || loc.empty? and loc = "--"
|
|
90
|
+
loc, url = loc_to_url(loc)
|
|
91
|
+
loc &&= break_up_long_str(loc, 10, 2)
|
|
92
|
+
url and loc = "<a href='#{url}'>#{loc}</a>"
|
|
93
|
+
loc
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def loc_to_url(loc)
|
|
97
|
+
/^ID /.match?(loc) or return [loc, nil]
|
|
98
|
+
loc.sub!(/^ID /, "")
|
|
99
|
+
loc = @mapid[loc] while @mapid[loc]
|
|
100
|
+
url = "#{@htmlfilename}##{to_ncname loc}"
|
|
101
|
+
[loc, url]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def break_up_long_str(str, threshold, punct)
|
|
105
|
+
Metanorma::Utils.break_up_long_str(str, threshold, punct)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def write_entry(file, entry)
|
|
109
|
+
entry[:context] &&= @c.encode(break_up_long_str(entry[:context], 40, 2))
|
|
110
|
+
file.print <<~HTML
|
|
111
|
+
<tr class="severity#{entry[:severity]}">
|
|
112
|
+
<td>#{entry[:line]}</td><th><code>#{entry[:location]}</code></th><td>#{entry[:error_id]}</td>
|
|
113
|
+
<td>#{entry[:error]}</td><td><pre>#{entry[:context]}</pre></td><td>#{entry[:severity]}</td></tr>
|
|
114
|
+
HTML
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def display_messages
|
|
118
|
+
grouped = group_messages_by_category
|
|
119
|
+
grouped.map { |cat, keys| format_category_section(cat, keys) }
|
|
120
|
+
.join("\n\n")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def group_messages_by_category
|
|
124
|
+
sort_messages_by_category_and_key
|
|
125
|
+
.group_by { |k| @msg[k][:category] }
|
|
126
|
+
.sort_by { |cat, _| cat }
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def format_category_section(category, keys)
|
|
130
|
+
lines = keys.map { |k| format_error_line(k) }
|
|
131
|
+
"#{category}:\n#{lines.join("\n")}"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def format_error_line(key)
|
|
135
|
+
padded_key = key.to_s.ljust(12)
|
|
136
|
+
"\t#{padded_key}: #{@msg[key][:error].gsub("\n", ' ')}"
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def sort_messages_by_category_and_key
|
|
140
|
+
@msg.keys.sort do |a, b|
|
|
141
|
+
cat_cmp = @msg[a][:category] <=> @msg[b][:category]
|
|
142
|
+
a_parts = parse_message_key(a)
|
|
143
|
+
b_parts = parse_message_key(b)
|
|
144
|
+
cat_cmp.zero? ? compare_key_parts(a_parts, b_parts) : cat_cmp
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def parse_message_key(key)
|
|
149
|
+
match = key.to_s.match(/^(.+?)_(\d+)$/)
|
|
150
|
+
match ? [match[1], match[2].to_i] : [key.to_s, nil]
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def compare_key_parts(a_parts, b_parts)
|
|
154
|
+
a_str, a_num = a_parts
|
|
155
|
+
b_str, b_num = b_parts
|
|
156
|
+
if a_num.nil? || b_num.nil?
|
|
157
|
+
a_str <=> b_str
|
|
158
|
+
else
|
|
159
|
+
str_cmp = a_str <=> b_str
|
|
160
|
+
str_cmp.zero? ? a_num <=> b_num : str_cmp
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
data/lib/utils/version.rb
CHANGED
data/metanorma-utils.gemspec
CHANGED
|
@@ -46,6 +46,6 @@ Gem::Specification.new do |spec|
|
|
|
46
46
|
spec.add_development_dependency "simplecov", "~> 0.15"
|
|
47
47
|
spec.add_development_dependency "timecop", "~> 0.9"
|
|
48
48
|
spec.add_development_dependency "webmock"
|
|
49
|
-
spec.add_development_dependency "canon"
|
|
49
|
+
spec.add_development_dependency "canon", "= 0.1.3"
|
|
50
50
|
# spec.metadata["rubygems_mfa_required"] = "true"
|
|
51
51
|
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:
|
|
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
|
+
date: 2025-11-17 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: asciidoctor
|
|
@@ -280,16 +280,16 @@ dependencies:
|
|
|
280
280
|
name: canon
|
|
281
281
|
requirement: !ruby/object:Gem::Requirement
|
|
282
282
|
requirements:
|
|
283
|
-
- -
|
|
283
|
+
- - '='
|
|
284
284
|
- !ruby/object:Gem::Version
|
|
285
|
-
version:
|
|
285
|
+
version: 0.1.3
|
|
286
286
|
type: :development
|
|
287
287
|
prerelease: false
|
|
288
288
|
version_requirements: !ruby/object:Gem::Requirement
|
|
289
289
|
requirements:
|
|
290
|
-
- -
|
|
290
|
+
- - '='
|
|
291
291
|
- !ruby/object:Gem::Version
|
|
292
|
-
version:
|
|
292
|
+
version: 0.1.3
|
|
293
293
|
description: 'metanorma-utils provides utilities for the Metanorma stack
|
|
294
294
|
|
|
295
295
|
'
|
|
@@ -308,11 +308,13 @@ 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
|
|
314
315
|
- lib/utils/linestatus.rb
|
|
315
316
|
- lib/utils/log.rb
|
|
317
|
+
- lib/utils/log_html.rb
|
|
316
318
|
- lib/utils/main.rb
|
|
317
319
|
- lib/utils/namespace.rb
|
|
318
320
|
- lib/utils/version.rb
|