metanorma/metanorma-utils

View on GitHub
lib/utils/log.rb

Summary

Maintainability
B
6 hrs
Test Coverage
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