Class: Metanorma::Utils::Log

Inherits:
Object
  • Object
show all
Defined in:
lib/utils/log.rb,
lib/utils/log_html.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(messages = {}) ⇒ Log

messages: hash of message IDs to severity, category severity: 0: abort; 1: serious; 2: not serious; 3: info only



11
12
13
14
15
16
17
18
19
20
21
# File 'lib/utils/log.rb', line 11

def initialize(messages = {})
  @log = {}
  @c = HTMLEntities.new
  @mapid = {}
  @suppress_log = { severity: 4, category: [], error_ids: [],
                    locations: [] }
  @msg = messages.each_value do |v|
    v[:error] = v[:error]
      .encode("UTF-8", invalid: :replace, undef: :replace)
  end
end

Instance Attribute Details

#suppress_logObject

Returns the value of attribute suppress_log.



7
8
9
# File 'lib/utils/log.rb', line 7

def suppress_log
  @suppress_log
end

Instance Method Details

#abort_messagesObject



58
59
60
61
62
63
64
# File 'lib/utils/log.rb', line 58

def abort_messages
  @log.values.each_with_object([]) do |v, m|
    v.each do |e|
      e[:severity].zero? and m << e[:error]
    end
  end
end

#add(id, loc, display: true, params: []) ⇒ Object



49
50
51
52
53
54
55
56
# File 'lib/utils/log.rb', line 49

def add(id, loc, display: true, params: [])
  m = add_prep(id) or return
  msg = create_entry(loc, m[:error], m[:severity], id, params)
  @log[m[:category]] << msg
  loc = loc.nil? ? "" : "(#{current_location(loc)[0]}): "
  suppress_display?(m[:category], loc, msg, display) or
    warn "#{m[:category]}: #{loc}#{msg[:error]}"
end

#add_error_ranges(xml) ⇒ Object

pass Nokogiri XML in, to record where all the anchors and ids are in the target document



29
30
31
# File 'lib/utils/log.rb', line 29

def add_error_ranges(xml)
  @anchor_ranges = AnchorRanges.new(xml)
end

#add_msg(messages) ⇒ Object



23
24
25
# File 'lib/utils/log.rb', line 23

def add_msg(messages)
  @msg.merge!(messages)
end

#add_prep(id) ⇒ Object



41
42
43
44
45
46
47
# File 'lib/utils/log.rb', line 41

def add_prep(id)
  id = id.to_sym
  @msg[id] or raise "Logging: Error #{id} is not defined!"
  @novalid || suppress_log?(id) and return nil
  @log[@msg[id][:category]] ||= []
  @msg[id]
end

#break_up_long_str(str, threshold, punct) ⇒ Object



104
105
106
# File 'lib/utils/log_html.rb', line 104

def break_up_long_str(str, threshold, punct)
  Metanorma::Utils.break_up_long_str(str, threshold, punct)
end

#compare_key_parts(a_parts, b_parts) ⇒ Object



153
154
155
156
157
158
159
160
161
162
# File 'lib/utils/log_html.rb', line 153

def compare_key_parts(a_parts, b_parts)
  a_str, a_num = a_parts
  b_str, b_num = b_parts
  if a_num.nil? || b_num.nil?
    a_str <=> b_str
  else
    str_cmp = a_str <=> b_str
    str_cmp.zero? ? a_num <=> b_num : str_cmp
  end
end

#context(node) ⇒ Object



166
167
168
169
170
171
# File 'lib/utils/log.rb', line 166

def context(node)
  node.is_a? String and return nil
  node.respond_to?(:to_xml) and return human_readable_xml(node)
  node.respond_to?(:to_s) and return node.to_s
  nil
end

#context_render(entry) ⇒ Object



77
78
79
80
81
# File 'lib/utils/log_html.rb', line 77

def context_render(entry)
  entry[:context] or return nil
  entry[:context].split("\n").first(5)
    .join("\n").gsub("><", "> <")
end

#create_entry(loc, msg, severity, error_id, params) ⇒ Object



87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/utils/log.rb', line 87

def create_entry(loc, msg, severity, error_id, params)
  loc_str, anchor, node_id = current_location(loc)
  item = { error_id: error_id, location: loc_str, severity: severity,
           error: interpolate_msg(msg, params), context: context(loc),
           line: line(loc, msg), anchor: anchor, id: node_id }
  if item[:error].include?(" :: ")
    a = item[:error].split(" :: ", 2)
    item[:context] = a[1]
    item[:error] = a[0]
  end
  item
end

#current_location(node) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
# File 'lib/utils/log.rb', line 111

def current_location(node)
  anchor = nil
  id = nil
  ret = if node.nil? then ""
        elsif node.respond_to?(:id) && !node.id.nil? then "ID #{node.id}"
        elsif node.respond_to?(:id) && node.id.nil? &&
            node.respond_to?(:parent)
          while !node.nil? && node.id.nil?
            node = node.parent
          end
          node.nil? ? "" : "ID #{node.id}"
        elsif node.respond_to?(:to_xml) && node.respond_to?(:parent)
          loc, anchor, id = xml_current_location(node)
          loc
        elsif node.is_a? String then node
        elsif node.respond_to?(:lineno) && !node.lineno.nil? &&
            !node.lineno.empty?
          "Asciidoctor Line #{'%06d' % node.lineno}"
        elsif node.respond_to?(:line) && !node.line.nil?
          "XML Line #{'%06d' % node.line}"
        elsif node.respond_to?(:parent)
          while !node.nil? &&
              (!node.respond_to?(:level) || node.level.positive?) &&
              (!node.respond_to?(:context) || node.context != :section)
            node = node.parent
            node.respond_to?(:context) && node&.context == :section and
              return "Section: #{node.title}"
          end
          "??"
        else "??"
        end
  [ret, anchor, id]
end

#display_messagesObject



117
118
119
120
121
# File 'lib/utils/log_html.rb', line 117

def display_messages
  grouped = group_messages_by_category
  grouped.map { |cat, keys| format_category_section(cat, keys) }
    .join("\n\n")
end

#entry_in_suppress_range?(entry, id) ⇒ Boolean

Returns:

  • (Boolean)


206
207
208
209
210
211
212
213
214
215
216
# File 'lib/utils/log.rb', line 206

def entry_in_suppress_range?(entry, id)
  # Use anchor if present, otherwise use id
  id.nil? and return false
  @suppress_log[:locations].each do |loc|
    entry_in_suppress_range_prep(loc)
    @anchor_ranges.in_range?(id, loc[:from], loc[:to]) or next
    loc[:error_ids].empty? || loc[:error_ids]
      .include?(entry[:error_id].to_s) and return true
  end
  false
end

#entry_in_suppress_range_prep(entry) ⇒ Object



200
201
202
203
204
# File 'lib/utils/log.rb', line 200

def entry_in_suppress_range_prep(entry)
  entry[:to] ||= entry[:from]
  entry[:error_ids] ||= []
  entry
end

#filter_locationsObject



190
191
192
193
194
195
196
197
198
# File 'lib/utils/log.rb', line 190

def filter_locations
  filter_locations? or return
  @log.transform_values! do |entries|
    entries.reject do |entry|
      # Use anchor if present, otherwise use id
      entry_in_suppress_range?(entry, entry[:anchor] || entry[:id])
    end
  end
end

#filter_locations?Boolean

Returns:

  • (Boolean)


184
185
186
187
188
# File 'lib/utils/log.rb', line 184

def filter_locations?
  @suppress_log[:locations] && !@suppress_log[:locations].empty? or return
  @anchor_ranges or return
  true
end

#format_category_section(category, keys) ⇒ Object



129
130
131
132
# File 'lib/utils/log_html.rb', line 129

def format_category_section(category, keys)
  lines = keys.map { |k| format_error_line(k) }
  "#{category}:\n#{lines.join("\n")}"
end

#format_error_line(key) ⇒ Object



134
135
136
137
# File 'lib/utils/log_html.rb', line 134

def format_error_line(key)
  padded_key = key.to_s.ljust(12)
  "\t#{padded_key}: #{@msg[key][:error].gsub("\n", ' ')}"
end

#group_messages_by_categoryObject



123
124
125
126
127
# File 'lib/utils/log_html.rb', line 123

def group_messages_by_category
  sort_messages_by_category_and_key
    .group_by { |k| @msg[k][:category] }
    .sort_by { |cat, _| cat }
end

#human_readable_xml(node) ⇒ Object

try to approximate input, at least for maths



174
175
176
177
178
179
180
181
182
# File 'lib/utils/log.rb', line 174

def human_readable_xml(node)
  ret = node.dup
  ret.xpath(".//*[local-name() = 'stem']").each do |s|
    sub = s.at("./*[local-name() = 'asciimath'] | " \
               "./*[local-name() = 'latexmath']")
    sub and s.replace(sub)
  end
  ret.to_xml
end

#index_severities(entries) ⇒ Object



32
33
34
35
36
37
38
39
40
41
# File 'lib/utils/log_html.rb', line 32

def index_severities(entries)
  s = entries.each_with_object({}) do |e, m|
    m[e[:severity]] ||= 0
    m[e[:severity]] += 1
  end.compact
  s.keys.sort.map do |k|
    error = s[k] == 1 ? "error" : "errors"
    "Severity #{k}: <b>#{s[k]}</b> #{error}"
  end.join("; ")
end

#interpolate_msg(msg, params) ⇒ Object



100
101
102
103
104
105
106
107
108
109
# File 'lib/utils/log.rb', line 100

def interpolate_msg(msg, params)
  # Count %s placeholders in the message
  placeholder_count = msg.scan(/%s/).length
  interpolation_params = if params.empty?
                           ::Array.new(placeholder_count, "")
                         else
                           params
                         end
  placeholder_count.zero? ? msg : (msg % interpolation_params)
end

#line(node, msg) ⇒ Object



156
157
158
159
160
161
162
163
164
# File 'lib/utils/log.rb', line 156

def line(node, msg)
  if node.respond_to?(:line) && !node.line.nil?
    "#{'%06d' % node.line}"
  elsif /^XML Line /.match?(msg)
    msg.sub(/^XML Line /, "").sub(/(^[^:]+):.*$/, "\\1")
  else
    "000000"
  end
end


87
88
89
90
91
92
93
94
# File 'lib/utils/log_html.rb', line 87

def loc_link(entry)
  loc = entry[:location]
  loc.nil? || loc.empty? and loc = "--"
  loc, url = loc_to_url(loc)
  loc &&= break_up_long_str(loc, 10, 2)
  url and loc = "<a href='#{url}'>#{loc}</a>"
  loc
end

#loc_to_url(loc) ⇒ Object



96
97
98
99
100
101
102
# File 'lib/utils/log_html.rb', line 96

def loc_to_url(loc)
  /^ID /.match?(loc) or return [loc, nil]
  loc.sub!(/^ID /, "")
  loc = @mapid[loc] while @mapid[loc]
  url = "#{@htmlfilename}##{to_ncname loc}"
  [loc, url]
end

#log_hdr(file) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# File 'lib/utils/log_html.rb', line 8

def log_hdr(file)
  "    <html><head><title>\#{file} errors</title>\n    <meta charset=\"UTF-8\"/>\n    <style> pre { white-space: pre-wrap; }\n    thead th { font-weight: bold; background-color: aqua; }\n    .severity0 { font-weight: bold; background-color: lightpink }\n    .severity1 { font-weight: bold; }\n    .severity2 { }\n    .severity3 { font-style: italic; color: grey; }\n    </style>\n    </head><body><h1>\#{file} errors</h1>\n    <ul>\#{log_index}</ul>\n  HTML\nend\n"

#log_indexObject



24
25
26
27
28
29
30
# File 'lib/utils/log_html.rb', line 24

def log_index
  @log.each_with_object([]) do |(k, v), m|
    m << "      <li><p><b><a href=\"#\#{to_ncname(k)}\">\#{k}</a></b>: \#{index_severities(v)}</p></li>\n    HTML\n  end.join(\"\\n\")\nend\n"

#mapid(old, new) ⇒ Object



83
84
85
# File 'lib/utils/log_html.rb', line 83

def mapid(old, new)
  @mapid[old] = new
end

#messagesObject



66
67
68
69
70
71
72
# File 'lib/utils/log.rb', line 66

def messages
  @log.values.each_with_object([]) do |v, m|
    v.each do |e|
      m << e
    end
  end
end

#parse_message_key(key) ⇒ Object



148
149
150
151
# File 'lib/utils/log_html.rb', line 148

def parse_message_key(key)
  match = key.to_s.match(/^(.+?)_(\d+)$/)
  match ? [match[1], match[2].to_i] : [key.to_s, nil]
end

#render_preproc_entry(entry) ⇒ Object



67
68
69
70
71
72
73
74
75
# File 'lib/utils/log_html.rb', line 67

def render_preproc_entry(entry)
  ret = entry.dup
  ret[:line] = nil if ret[:line] == "000000"
  ret[:location] = loc_link(entry)
  ret[:error] = break_up_long_str(entry[:error], 10, 2)
    .gsub(/`([^`]+)`/, "<code>\\1</code>")
  ret[:context] = context_render(entry)
  ret.compact
end

#save_to(filename, dir = nil) ⇒ Object



33
34
35
36
37
38
39
# File 'lib/utils/log.rb', line 33

def save_to(filename, dir = nil)
  dir ||= File.dirname(filename)
  new_fn = filename.sub(/\.err\.html$/, ".html")
  b = File.join(dir, File.basename(new_fn, ".*"))
  @filename = "#{b}.err.html"
  @htmlfilename = "#{b}.html"
end

#sort_messages_by_category_and_keyObject



139
140
141
142
143
144
145
146
# File 'lib/utils/log_html.rb', line 139

def sort_messages_by_category_and_key
  @msg.keys.sort do |a, b|
    cat_cmp = @msg[a][:category] <=> @msg[b][:category]
    a_parts = parse_message_key(a)
    b_parts = parse_message_key(b)
    cat_cmp.zero? ? compare_key_parts(a_parts, b_parts) : cat_cmp
  end
end

#suppress_display?(category, _loc, _msg, display) ⇒ Boolean

Returns:

  • (Boolean)


82
83
84
85
# File 'lib/utils/log.rb', line 82

def suppress_display?(category, _loc, _msg, display)
  ["Metanorma XML Syntax", "Relaton"].include?(category) ||
    !display
end

#suppress_log?(id) ⇒ Boolean

Returns:

  • (Boolean)


74
75
76
77
78
79
80
# File 'lib/utils/log.rb', line 74

def suppress_log?(id)
  category =  @msg[id][:category]
  category && /^Fetching /.match?(@msg[id][:error]) ||
    @suppress_log[:severity] <= @msg[id][:severity] ||
    @suppress_log[:category].include?(category) ||
    @suppress_log[:error_ids].include?(id.to_s)
end

#to_ncname(tag) ⇒ Object



4
5
6
# File 'lib/utils/log_html.rb', line 4

def to_ncname(tag)
  ::Metanorma::Utils.to_ncname(tag)
end

#write(file = nil) ⇒ Object



43
44
45
46
47
48
49
50
51
# File 'lib/utils/log_html.rb', line 43

def write(file = nil)
  (!file && @filename) or save_to(file || "metanorma", nil)
  filter_locations
  File.open(@filename, "w:UTF-8") do |f|
    f.puts log_hdr(@filename)
    @log.each_key { |key| write_key(f, key) }
    f.puts "</body></html>\n"
  end
end

#write_entry(file, entry) ⇒ Object



108
109
110
111
112
113
114
115
# File 'lib/utils/log_html.rb', line 108

def write_entry(file, entry)
  entry[:context] &&= @c.encode(break_up_long_str(entry[:context], 40, 2))
  file.print "    <tr class=\"severity\#{entry[:severity]}\">\n    <td>\#{entry[:line]}</td><th><code>\#{entry[:location]}</code></th><td>\#{entry[:error_id]}</td>\n    <td>\#{entry[:error]}</td><td><pre>\#{entry[:context]}</pre></td><td>\#{entry[:severity]}</td></tr>\n  HTML\nend\n"

#write_key(file, key) ⇒ Object



53
54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/utils/log_html.rb', line 53

def write_key(file, key)
  file.puts "    <h2 id=\"\#{to_ncname(key)}\">\#{key}</h2>\\n<table border=\"1\">\n    <thead><th width=\"5%\">Line</th><th width=\"20%\">ID</th><th width=\"10%\">Error</th>\n    <th width=\"20%\">Message</th><th width=\"40%\">Context</th><th width=\"5%\">Severity</th></thead>\n    <tbody>\n  HTML\n  @log[key].sort_by { |a| [a[:line], a[:location], a[:error]] }\n    .each do |n|\n    write_entry(file, render_preproc_entry(n))\n  end\n  file.puts \"</tbody></table>\\n\"\nend\n"

#xml_current_location(node) ⇒ Object



145
146
147
148
149
150
151
152
153
154
# File 'lib/utils/log.rb', line 145

def xml_current_location(node)
  while !node.nil? && node["id"].nil? && node.respond_to?(:parent)
    node.parent.nil? and break
    node = node.parent
  end
  anchor = node["anchor"]
  id = node["id"]
  loc = node.respond_to?(:parent) ? "ID #{anchor || id}" : ""
  [loc, anchor, id]
end