# encoding: utf-8 require 'rexml/document' require 'rexml/formatters/transitive' require 'base64' module XhtmlReportGenerator VERSION = '4.0.1' # This is the main generator class. It can be instanced with custom javascript, css, and ruby files to allow # generation of arbitrary reports. # @attr [REXML::Document] document This is the html document / actual report # @attr [String] file path to the file where this report is saved to. Default: nil # @attr [Boolean] sync if true, the report will be written to disk after every modificaiton. Default: false # Note that sync = true can have severe performance impact, since the complete # html file is written to disk after each change. class Generator attr_accessor :document, :file, :sync # @param opts [Hash] See the example for an explanation of the valid symbols # @example Valid keys for the opts Hash # :title Title in the header section, defaults to "Title" # :js if specified, array of javascript files which are inlined into the html header section, after # the default included js files (check in sourcecode below). # :css if specified, array of css files which are inlined into the html header section after # the default included css files (check in sourcecode below). # :css_print if specified, array of css files which are inlined into the html header section with media=print # after the default included print css files (check in sourcecode below). def initialize(opts = {}) # define the default values resources = File.expand_path("../../resource/", __FILE__) defaults = { :title => "Title", :js => [ File.expand_path("js/jquery-3.2.1.min.js", resources), File.expand_path("d3v5.7.0/d3.min.js", resources), File.expand_path("c3v0.6.12/c3.min.js", resources), File.expand_path("js/split.min.js", resources), File.expand_path("js/layout_split.js", resources), File.expand_path("js/table_of_contents.js", resources), File.expand_path("js/toggle_linewrap.js", resources), ], :css => [ File.expand_path("css/style.css", resources), File.expand_path("c3v0.6.12/c3.min.css", resources), ], :css_print => [ File.expand_path("css/print.css", resources) ], #:custom_rb => File.expand_path("../custom.rb", __FILE__), } @sync = false opts[:title] = defaults[:title] if !opts.key?(:title) if opts.key?(:js) opts[:js] = defaults[:js] + opts[:js] else opts[:js] = defaults[:js] end if opts.key?(:css) opts[:css] = defaults[:css] + opts[:css] else opts[:css] = defaults[:css] end if opts.key?(:css_print) opts[:css_print] = defaults[:css_print] + opts[:css_print] else opts[:css_print] = defaults[:css_print] end @document = Generator.create_xhtml_document(opts[:title]) head = @document.elements["//head"] head.add_element("meta", {"charset" => "utf-8"}) # insert css opts[:css].each do |css_path| style = head.add_element("style", {"type" => "text/css"}) cdata(File.read(css_path), style) end # insert css for printing opts[:css_print].each do |css_path| style = head.add_element("style", {"type" => "text/css", "media"=>"print"}) cdata(File.read(css_path), style) end # inster js files opts[:js].each do |js_path| script = head.add_element("script", {"type" => "text/javascript"}) cdata(File.read(js_path), script) end document_changed() end # Surrounds CData tag with c-style comments to remain compatible with normal html. # For plain xhtml documents this is not needed. # Example /**/ # @param str [String] the string to be enclosed in cdata # @param parent_element [REXML::Element] the element to which cdata should be added # @return [REXML::Element] parent_element def cdata(str, parent_element) # somehow there is a problem with CDATA, any text added after will automatically go into the CDATA # so we have do add a dummy node after the CDATA and then add the text. parent_element.add_text("/*") parent_element.add(REXML::CData.new("*/\n"+str+"\n/*")) parent_element.add(REXML::Comment.new("dummy comment to make c-style comments for cdata work")) parent_element.add_text("*/") end # Check if the give string is a valid UTF-8 byte sequence. If it is not valid UTF-8, then # all invalid bytes are replaced by "\u2e2e" (\xe2\xb8\xae) ('REVERSED QUESTION MARK') because the default # replacement character "\uFFFD" ('QUESTION MARK IN DIAMOND BOX') is two slots wide and might # destroy mono spaced formatting # @param str [String] of any encoding # @return [String] UTF-8 encoded valid string def encoding_fixer(str) str = str.to_s # catch str = nil #if !str.force_encoding('UTF-8').valid_encoding? # str.encode!('UTF-8', 'ISO-8859-1', {:invalid => :replace, :undef => :replace, :xml => :text}) #end tmp = str.force_encoding('UTF-8').encode('UTF-8',{:invalid => :replace, :undef => :replace, :replace => "\u2e2e"}) # replace all special control chars as well but keep newline and whitespace "\u2e2e" tmp.force_encoding('binary').gsub!(/[\x00-\x07\x0C-\x1F]|\xef\xbf\xbe|\xef\xbf\xbf/n, "\xe2\xb8\xae".force_encoding('binary')) return tmp.force_encoding('UTF-8') end # Creates a minimal valid xhtml document including header title and body elements # @param title [String] Title in the header section def self.create_xhtml_document(title) # don't use version 1.1 - firefox has not yet a parser for xml 1.1 # https://bugzilla.mozilla.org/show_bug.cgi?id=233154 header = '' # change of doctype to for html5 compatibility header << '' doc = REXML::Document.new(header) html = doc.add_element("html", {"xmlns" => "http://www.w3.org/1999/xhtml"}) # create header head = html.add_element("head") t = head.add_element("title") t.text = title html.add_element("body") return doc end # returns the string representation of the xml document # @param indent [Number] indent for child elements. defaults to 0. # Note: if you change the indet this might destroy formatting of
 sections
    # @return [String] formatted xml document
    def to_s(indent = 0)
      output = ""
      # note :  transitive is needed to preserve newlines in 
 tags
      # note2:  the hash options syntax is supported only from ruby version >= 2.0.0 we need the old style
      #         for compatibility with 1.9.3
      # @document.write({:output=>output, :indent=>indent, :transitive=>true})
      # change to Formatters since document.write is deprecated
      f = REXML::Formatters::Transitive.new(indent)
      f.write(@document, output)
      return output
    end

    # Saves the xml document to a file. If no file is given, the file which was used most recently for this Generator
    # object will be overwritten.
    # @param file [String] absolute or relative path to the file to which will be written. Default: last file used.
    # @param mode [String] defaults to 'w', one of the file open modes that allows writing ['r+','w','w+','a','a+']
    def write(file=@file, mode='w')
      # instance variables are nil if they were never initialized
      if file.nil?
        raise "no valid file given: '#{file}'"
      end
      @file = file
      File.open(file, "#{mode}:UTF-8") {|f| f.write(self.to_s.force_encoding(Encoding::UTF_8))}
    end

    # This method should be called after every change to the document.
    # Here we ensure the report is written to disk after each change
    # if #sync is true. If #sync is false this method does nothing
    def document_changed()
      if @sync
        if @file.nil?
          raise "You must call #write at least once before you can enable synced mode"
        end
        write()
      end
    end

    # creates the basic page layout and sets the current Element to the main content area (middle div)
    # @example The middle div is matched by the following xPath
    #   //body/div[@id='middle']
    # @param title [String] the title of the document
    # @param layout [Fixnum] one of 0,1,2,3 where 0 means minimal layout without left and right table of contents,
    #   1 means only left toc, 2 means only right toc, and 3 means full layout with left and right toc.
    def create_layout(title, layout=3)
      raise "invalid layout selector, choose from 0..3" if (layout < 0) || (layout > 3)

      @body = @document.elements["//body"]
      # only add the layout if it is not already there
      if !@layout
        head = @body.add_element("div", {"class" => "head", "id" => "head"})
        head.add_element("button", {"id" => "pre_toggle_linewrap"}).add_text("Toggle Linewrap")

        if (layout & 0x1) != 0
        div = @body.add_element("div", {"class" => "lefttoc split split-horizontal", "id" => "ltoc"})
        div.add_text("Table of Contents")
        div.add_element("br")
        end

        @div_middle = @body.add_element("div", {"class" => "middle split split-horizontal", "id" => "middle"})

        if (layout & 0x2) != 0
        div = @body.add_element("div", {"class" => "righttoc split split-horizontal", "id" => "rtoc"})
        div.add_text("Quick Links")
        div.add_element("br");div.add_element("br")
        end

        @body.add_element("p", {"class" => "#{layout}", "id" => "layout"}).add_text("this text should be hidden")

        @layout = true
      end
      @current = @document.elements["//body/div[@id='middle']"]
      set_title(title)
      document_changed()
    end

    # sets the title of the document in the  section as well as in the layout header div
    # create_layout must be called before!
    # @param title [String] the text which will be insertead
    def set_title(title)
      if !@layout
        raise "call create_layout first"
      end
      pagetitle = @document.elements["//head/title"]
      pagetitle.text = title
      div = @document.elements["//body/div[@id='head']"]
      div.text = title
      document_changed()
    end

    # returns the title text of the report
    # @return [String] The title of the report
    def get_title()
      pagetitle = @document.elements["//head/title"]
      return pagetitle.text
    end

    # set the current element to the element or first element matched by the xpath expression.
    # The current element is the one which can be modified through highlighting.
    # @param xpath [REXML::Element|String] the element or an xpath string
    def set_current!(xpath)
      if xpath.is_a?(REXML::Element)
        @current = xpath
      elsif xpath.is_a?(String)
        @current = @document.elements[xpath]
      else
        raise "xpath is neither a String nor a REXML::Element"
      end
    end

    # returns the current xml element
    # @return [REXML::Element] the xml element after which the following elements will be added
    def get_current()
      return @current
    end

    # returns the plain text without any xml tags of the specified element and all its children
    # @param el [REXML::Element] The element from which to fetch the text children. Defaults to @current
    # @param recursive [Boolean] whether or not to recurse into the children of the given "el"
    # @return [String] text contents of xml node
    def get_element_text(el = @current, recursive = true)
      out = ""
      el.to_a.each { |child|
        if child.is_a?(REXML::Text)
          out << child.value()
        else
          if recursive
            out << get_element_text(child, true)
          end
        end
      }
      return out
    end

    # @param elem [REXML::Element]
    # @return [String]
    def element_to_string(elem)
      f = REXML::Formatters::Transitive.new(0) # use Transitive to preserve source formatting (e.g. 
 tags)
      out = ""
      f.write(elem, out)
      return out
    end

    # @see #code
    # Instead of adding content to the report, this method returns the produced html code as a string.
    # This can be used to insert code into #custom_table (with the option data_is_xhtml: true)
    # @return [String] the code including 
 tags as a string
    def get_code_html(attrs={}, &block)
      temp = REXML::Element.new("pre")
      temp.add_attributes(attrs)
      raise "Block argument is mandatory" unless block_given?
      text = encoding_fixer(block.call())
      temp.add_text(text)
      element_to_string(temp)
    end

    # Appends a 
 node after the @current node
    # @param attrs [Hash] attributes for the 
 element. The following classes can be passed as attributes and are predefined with a different
    #                     background for your convenience !{"class" => "code0"} (light-blue), !{"class" => "code1"} (red-brown),
    #                     !{"class" => "code2"} (light-green), !{"class" => "code3"} (light-yellow). You may also specify your own background
    #                     as follows: !{"style" => "background: #FF00FF;"}.
    # @yieldreturn [String] the text to be added to the 
 element
    # @return [REXML::Element] the Element which was just added
    def code(attrs={}, &block)
      temp = REXML::Element.new("pre")
      temp.add_attributes(attrs)
      @div_middle.insert_after(@current, temp)
      @current = temp
      raise "Block argument is mandatory" unless block_given?
      text = encoding_fixer(block.call())
      @current.add_text(text)
      document_changed()
      return @current
    end

    # Appends a