Module: Rigor::ModuleGraph::Viewer::Html

Defined in:
lib/rigor/module_graph/viewer/html.rb

Constant Summary collapse

TEMPLATE_DIR =
File.expand_path("../templates", __dir__)
TEMPLATE_PATH =
File.join(TEMPLATE_DIR, "viewer.html.erb")
CSS_PATH =
File.join(TEMPLATE_DIR, "viewer.css")
VIEWER_JS_PATH =
File.join(TEMPLATE_DIR, "viewer.js")
CYTOSCAPE_JS_PATH =
File.join(TEMPLATE_DIR, "vendor", "cytoscape.min.js")
CONSTANT_KINDS =

Node kinds that map to top-level Cytoscape nodes. Method / attribute nodes are out of scope for the graph viewer (they belong to the class diagram, not the dependency graph).

%w[class module].freeze

Class Method Summary collapse

Class Method Details

.build_data(edges:, nodes:, path_mode:, open_with:) ⇒ Object

Builds the ‘edges:, options:` payload the inline init JS reads from `<script type=“application/json” id=“rmg-data”>`.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# File 'lib/rigor/module_graph/viewer/html.rb', line 62

def build_data(edges:, nodes:, path_mode:, open_with:)
  node_meta = {}
  nodes.each do |node|
    next unless CONSTANT_KINDS.include?(node.kind)

    key = fully_qualified(node)
    # First definition wins; class re-opens still resolve
    # to one Cytoscape node, matching the dedup contract
    # in `Edge#dedup_key`.
    node_meta[key] ||= {
      # Cytoscape resolves `edge.source` / `edge.target`
      # against `node.data.id`, so the constant name has
      # to be the id (not just a display field).
      id: key,
      name: key,
      kind: node.kind,
      path: path_for(node.path, path_mode),
      line: node.line
    }
  end

  # Every edge endpoint becomes a node, even when the
  # constant has no definition in the analysed paths
  # (e.g. `ApplicationRecord` from a Rails gem). These
  # get the `external` kind so the styling can dim them.
  edges.flat_map { |e| [e.from, e.to] }.uniq.each do |name|
    node_meta[name] ||= { id: name, name: name, kind: "external" }
  end

  {
    nodes: node_meta.values.map { |n| { data: n } },
    edges: edges.each_with_index.map do |edge, i|
      {
        data: {
          id: "e#{i}",
          source: edge.from,
          target: edge.to,
          kind: edge.kind,
          confidence: edge.confidence
        }
      }
    end,
    options: { open_with: open_with&.to_s }
  }
end

.fully_qualified(node) ⇒ Object



108
109
110
111
# File 'lib/rigor/module_graph/viewer/html.rb', line 108

def fully_qualified(node)
  owner = node.owner
  owner && !owner.empty? ? "#{owner}::#{node.name}" : node.name
end

.path_for(path, mode) ⇒ Object



113
114
115
116
117
118
119
120
# File 'lib/rigor/module_graph/viewer/html.rb', line 113

def path_for(path, mode)
  return nil if path.nil? || mode == :none

  case mode
  when :absolute then File.expand_path(path)
  when :relative then path
  end
end

.render(edges:, nodes:, title:, subtitle: nil, path_mode: :relative, open_with: nil) ⇒ String

Returns complete HTML document.

Parameters:

  • edges (Array<Edge>)

    dependency edges

  • nodes (Array<Node>)

    node metadata (for click-through)

  • title (String)

    page title

  • subtitle (String, nil) (defaults to: nil)

    optional subtitle line

  • path_mode (:relative, :absolute, :none) (defaults to: :relative)

    how ‘data.path` is reported to click handlers. `:none` strips it entirely so HTML shared externally doesn’t leak filesystem layout.

  • open_with (Symbol, nil) (defaults to: nil)

    when ‘:vscode`, node click opens `vscode://file/<path>:<line>` instead of writing to clipboard.

Returns:

  • (String)

    complete HTML document



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/rigor/module_graph/viewer/html.rb', line 43

def render(edges:, nodes:, title:, subtitle: nil, path_mode: :relative, open_with: nil)
  data = build_data(
    edges: edges, nodes: nodes,
    path_mode: path_mode, open_with: open_with
  )
  template = ERB.new(File.read(TEMPLATE_PATH), trim_mode: "-")
  template.result_with_hash(
    title: title,
    subtitle: subtitle,
    data_json: safe_json(data),
    css: File.read(CSS_PATH),
    cytoscape: File.read(CYTOSCAPE_JS_PATH),
    viewer: File.read(VIEWER_JS_PATH)
  )
end

.safe_json(value) ⇒ Object

JSON embedded in ‘<script>` must not contain `</` (would break out of the surrounding tag). `JSON.generate` does not escape it by default; rewriting the literal pair `</` → `</` is the standard safety pass.



126
127
128
# File 'lib/rigor/module_graph/viewer/html.rb', line 126

def safe_json(value)
  JSON.generate(value).gsub("</", "<\\/")
end