lib/utils/log.rb
require "htmlentities"
module Metanorma
module Utils
class Log
attr_writer :xml
def initialize
@log = {}
@c = HTMLEntities.new
@mapid = {}
end
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
# severity: 0: abort; 1: serious; 2: not serious; 3: info only
def add(category, loc, msg, severity: 2)
@novalid and return
@log[category] ||= []
item = create_entry(loc, msg, severity)
@log[category] << item
loc = loc.nil? ? "" : "(#{current_location(loc)}): "
suppress_display?(category, loc, msg) or
warn "#{category}: #{loc}#{msg}"
end
def abort_messages
@log.values.each_with_object([]) do |v, m|
v.each do |e|
e[:severity].zero? and m << e[:message]
end
end
end
def suppress_display?(category, _loc, _msg)
["Metanorma XML Syntax"].include?(category)
end
def create_entry(loc, msg, severity)
msg = msg.encode("UTF-8", invalid: :replace, undef: :replace)
item = { location: current_location(loc), severity: severity,
message: msg, context: context(loc), line: line(loc, msg) }
if item[:message].include?(" :: ")
a = item[:message].split(" :: ", 2)
item[:context] = a[1]
item[:message] = a[0]
end
item
end
def current_location(node)
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)
while !node.nil? && node["id"].nil? && node.respond_to?(:parent)
node = node.parent
end
node.respond_to?(:parent) ? "ID #{node['id']}" : ""
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
return "Section: #{node.title}" if node.respond_to?(:context) &&
node&.context == :section
end
"??"
else "??"
end
end
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(/:.*$/, "")
else
"000000"
end
end
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
# try to approximate input, at least for maths
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
def log_hdr(file)
<<~HTML
<html><head><title>#{file} errors</title>
<meta charset="UTF-8"/>
<style> pre { white-space: pre-wrap; }
thead th { font-weight: bold; background-color: aqua; }
.severity0 { font-weight: bold; background-color: lightpink }
.severity1 { font-weight: bold; }
.severity2 { }
.severity3 { font-style: italic; color: grey; }
</style>
</head><body><h1>#{file} errors</h1>
HTML
end
def write(file = nil)
(!file && @filename) or save_to(file || "metanorma", nil)
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
def write_key(file, key)
file.puts <<~HTML
<h2>#{key}</h2>\n<table border="1">
<thead><th width="5%">Line</th><th width="20%">ID</th>
<th width="30%">Message</th><th width="40%">Context</th><th width="5%">Severity</th></thead>
<tbody>
HTML
@log[key].sort_by { |a| [a[:line], a[:location], a[:message]] }
.each do |n|
write_entry(file, render_preproc_entry(n))
end
file.puts "</tbody></table>\n"
end
def render_preproc_entry(entry)
ret = entry.dup
ret[:line] = nil if ret[:line] == "000000"
ret[:location] = loc_link(entry)
ret[:message] = break_up_long_str(entry[:message], 10, 2)
.gsub(/`([^`]+)`/, "<code>\\1</code>")
ret[:context] = context_render(entry)
ret.compact
end
def context_render(entry)
entry[:context] or return nil
entry[:context].split("\n").first(5)
.join("\n").gsub("><", "> <")
end
def mapid(old, new)
@mapid[old] = new
end
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
def loc_to_url(loc)
/^ID /.match?(loc) or return [loc, nil]
loc.sub!(/^ID /, "")
loc = @mapid[loc] while @mapid[loc]
url = "#{@htmlfilename}##{loc}"
[loc, url]
end
def break_up_long_str(str, threshold, punct)
Metanorma::Utils.break_up_long_str(str, threshold, punct)
end
def write_entry(file, entry)
entry[:context] &&= @c.encode(break_up_long_str(entry[:context], 40, 2))
file.print <<~HTML
<tr class="severity#{entry[:severity]}">
<td>#{entry[:line]}</td><th><code>#{entry[:location]}</code></th>
<td>#{entry[:message]}</td><td><pre>#{entry[:context]}</pre></td><td>#{entry[:severity]}</td></tr>
HTML
end
end
end
end