module Giblish # Builds an asciidoc page with an svg image with a # digraph showing how documents reference each other. # # Graphviz is used as the graph generator and must be available # as a valid engine via asciidoctor-diagram for this class to work. class GraphBuilderGraphviz # the dependency graph relies on graphwiz (dot), check if we can access that def self.supported !Giblish.which("dot").nil? end # Supported options: # :extension - file extension for URL links (default is .html) def initialize(processed_docs, paths, deployment_info, options = {}) # this class relies on graphwiz (dot), make sure we can access that raise "Could not find the 'dot' tool needed to generate a dependency graph!" unless GraphBuilderGraphviz.supported # require asciidoctor module needed for generating diagrams require "asciidoctor-diagram/graphviz" @noid_docs = {} @next_id = 0 @processed_docs = processed_docs @paths = paths @deployment_info = deployment_info @converter_options = options.dup @extension = @converter_options.key?(:extension) ? options[:extension] : "html" @docid_cache = DocidCollector.docid_cache @docid_deps = DocidCollector.docid_deps @dep_graph = build_dep_graph @search_opts = { web_assets_top: @deployment_info.web_path, search_assets_top: @deployment_info.search_assets_path } end # get the asciidoc source for the document. def source(make_searchable: false) s = <<~DOC_STR #{generate_header} #{add_search_box if make_searchable} #{generate_graph_header} #{generate_labels} #{generate_deps} #{generate_footer} DOC_STR s end def cleanup # remove cache dir and svg image created by asciidoctor-diagram # when creating the document dependency graph adoc_diag_cache = @paths.dst_root_abs.join(".asciidoctor") FileUtils.remove_dir(adoc_diag_cache) if adoc_diag_cache.directory? Giblog.logger.info { "Removing cached files at: #{@paths.dst_root_abs.join('docdeps.svg')}" } @paths.dst_root_abs.join("docdeps.svg").delete end private # build a hash with {DocInfo => [doc_id array]} def build_dep_graph result = {} @docid_deps.each do |src_file, id_array| d = @processed_docs.find do |doc| doc.src_file.to_s.eql? src_file end raise "Inconsistent docs when building graph!! found no match for #{src_file}" if d.nil? result[d] = id_array if d.converted end result end def generate_graph_header <<~DOC_STR Below is a graph that visualizes what documents (by doc-id) a specific document references. [graphviz,"docdeps","svg",options="inline"] .... digraph notebook { bgcolor="#33333310" node [shape=note, fillcolor="#ebf26680", style="filled,solid" ] rankdir="LR" DOC_STR end def generate_header t = Time.now <<~DOC_STR = Document-id reference graph from #{@paths.src_root_abs} Generated by Giblish at:: #{t.strftime('%Y-%m-%d %H:%M')} DOC_STR end def generate_footer <<~DOC_STR } .... DOC_STR end def add_search_box Giblish.generate_search_box_html( @converter_options[:attributes]["stylesheet"], "/cgi-bin/giblish-search.cgi", @search_opts ) end def make_dot_entry(doc_dict, info) # split title into multiple rows if it is too long line_length = 15 lines = [""] unless info&.title.nil? info.title.split(" ").inject("") do |l, w| line = "#{l} #{w}" lines[-1] = line if line.length > line_length # create a new, empty, line lines << "" "" else line end end end title = lines.select { |l| l.length.positive? }.map { |l| l }.join("\n") # create the label used to display the node in the graph dot_entry = if info.doc_id.nil? doc_id = next_fake_id @noid_docs[info] = doc_id "\"#{doc_id}\"[label=\"-\\n#{title}\"" else doc_id = info.doc_id "\"#{info.doc_id}\"[label=\"#{info.doc_id}\\n#{title}\"" end # add clickable links in the case of html output (this is not supported # out-of-the-box for pdf). rp = info.rel_path.sub_ext(".#{@extension}") dot_entry += case @extension when "html" ", URL=\"#{rp}\" ]" else " ]" end doc_dict[doc_id] = dot_entry end def generate_labels # create an entry in the 'dot' description for each # document, sort them according to descending doc id to # get them displayed in the opposite order in the graph node_dict = {} @dep_graph.each_key do |info| make_dot_entry node_dict, info end # sort the nodes by reverse doc id node_dict = node_dict.sort.reverse.to_h # produce the string with all node entries node_dict.map do |_k, v| v end.join("\n") end def generate_deps dep_str = "" @dep_graph.each do |info, targets| # set either the real or the generated id as source src_part = if info.doc_id.nil? "\"#{@noid_docs[info]}\"" else "\"#{info.doc_id}\"" end if targets.length.zero? dep_str += "#{src_part}\n" next end dep_str += "#{src_part} -> {" + targets.reduce("") do |acc, target| acc + " \"#{target}\"" end # replace last comma with newline dep_str += "}\n" end dep_str end def next_fake_id @next_id += 1 "_generated_id_#{@next_id.to_s.rjust(4, '0')}" end end # specializes generation of a document dependency graph for # docs rendered from a git repo. class GitGraphBuilderGraphviz < GraphBuilderGraphviz def initialize(processed_docs, paths, deployment_info, options = {}, _git_repo) super(processed_docs, paths, deployment_info, options) end end end