codebase-recall 0.7.3

CLI based application for codebase dumper for LLMs and fast review/recall project
//! Front-end assets for the graph view, embedded into the binary at compile time.
//!
//! The same markup + view script drives two delivery modes:
//! * [`Delivery::Inline`] — one self-contained `.html` file (`code-rcl graph`).
//! * [`Delivery::Server`] — assets fetched from the local `code-rcl serve`
//!   process, plus a tiny script that keeps the server alive only while the tab is.

use std::sync::LazyLock;

/// Vendored d3 v7 (UMD). Pinned; see README for the exact version and source.
pub const D3_JS: &str = include_str!("d3.min.js");
pub const GRAPH_CSS: &str = include_str!("graph.css");
/// Heartbeat client used only by `code-ctx serve`.
pub const LIVE_JS: &str = include_str!("live.js");

/// The graph view script, kept as small single-responsibility source files under
/// `graph/` and stitched together — in this order — inside one IIFE at first use.
/// Delivered as a single `<script>` (inline mode) or one `/assets/graph-view.js`
/// route (serve mode), so there is no browser module loader involved.
const GRAPH_VIEW_PARTS: &[&str] = &[
    include_str!("graph/00-data.js"),
    include_str!("graph/10-state.js"),
    include_str!("graph/20-theme.js"),
    include_str!("graph/30-model.js"),
    include_str!("graph/40-sim.js"),
    include_str!("graph/50-render.js"),
    include_str!("graph/60-interaction.js"),
    include_str!("graph/70-tooltip-panel.js"),
    include_str!("graph/80-controls.js"),
    include_str!("graph/85-legend.js"),
    include_str!("graph/90-main.js"),
];

pub static GRAPH_VIEW_JS: LazyLock<String> = LazyLock::new(|| {
    format!(
        "(function () {{\n\"use strict\";\n\n{}\n}})();\n",
        GRAPH_VIEW_PARTS.join("\n")
    )
});

#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Delivery {
    #[allow(dead_code)]
    Inline,
    Server,
    Separated,
}

const BODY: &str = r#"<div id="app">
  <header>
    <button id="toggleLeftPane" type="button" class="btn-toggle-left" title="Toggle files/code panel">☰</button>
    <h1>code-rcl graph</h1>
    <span class="stat">__STAT__</span>
    <label><input type="checkbox" data-kind="imports" checked> imports</label>
    <label><input type="checkbox" data-kind="calls" checked> calls</label>
    <label><input type="checkbox" data-kind="references"> references</label>
    <label><input type="checkbox" id="expandAll"> expand all</label>
    <input type="search" id="search" placeholder="filter &amp; isolate nodes&hellip;">
    <button id="fitBtn" type="button">fit</button>
    <span id="focusCtl" hidden>
      <span id="focusLabel"></span>
      <button id="focusShallower" type="button" title="shallower">&minus;</button>
      <span id="focusDepth">2</span>
      <button id="focusDeeper" type="button" title="deeper">+</button>
      <button id="focusClear" type="button">clear focus</button>
    </span>
  </header>
  <div id="workbench">
    <aside id="leftPane" class="collapsed">
      <div class="pane-tabs">
        <button type="button" class="pane-tab active" data-tab="tree">Files</button>
        <button type="button" class="pane-tab" data-tab="code">Code</button>
      </div>
      <div id="treeView" class="pane-tab-content"></div>
      <div id="codeView" class="pane-tab-content" hidden>
        <div class="code-bar">
          <span id="codePath">no file selected</span>
          <span id="codeLine"></span>
        </div>
        <div id="codeContent" class="code-lines"></div>
      </div>
    </aside>
    <div id="stage">
      <canvas id="scene"></canvas>
      <aside id="sidePanel" hidden></aside>
      <div class="legend" id="legend"></div>
      <div id="status"></div>
    </div>
  </div>
</div>"#;

/// Build the full HTML document. `data_json` is the serialized `CodeGraph`;
/// `stat` is the short "N nodes / M edges" line shown in the header.
pub fn graph_page(data_json: &str, stat: &str, delivery: Delivery) -> String {
    // `<` only ever occurs inside JSON string values, so this keeps the blob
    // valid JSON while making it impossible to break out of the <script> tag.
    let safe = data_json.replace('<', "\\u003c");
    let body = BODY.replace("__STAT__", stat);

    let (head, tail) = match delivery {
        Delivery::Inline => (
            format!("<style>{GRAPH_CSS}</style>"),
            format!(
                "<script id=\"graph-data\" type=\"application/json\">{safe}</script>\n\
                 <script>{D3_JS}</script>\n\
                 <script>{}</script>",
                &*GRAPH_VIEW_JS
            ),
        ),
        Delivery::Server => (
            "<link rel=\"stylesheet\" href=\"/assets/graph.css\">".to_string(),
            format!(
                "<script id=\"graph-data\" type=\"application/json\">{safe}</script>\n\
                 <script src=\"/assets/d3.min.js\"></script>\n\
                 <script src=\"/assets/graph-view.js\"></script>\n\
                 <script src=\"/assets/live.js\"></script>"
            ),
        ),
        Delivery::Separated => (
            "<link rel=\"stylesheet\" href=\"./style.css\">".to_string(),
            "<script src=\"./data.js\"></script>\n\
             <script src=\"./js/d3.min.js\"></script>\n\
             <script src=\"./js/graph-view.js\"></script>"
                .to_string(),
        ),
    };

    format!(
        "<!doctype html>\n\
<html lang=\"en\">\n\
<head>\n\
<meta charset=\"utf-8\">\n\
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n\
<title>codebase recall graph</title>\n\
{head}\n\
</head>\n\
<body>\n\
{body}\n\
{tail}\n\
</body>\n\
</html>\n"
    )
}

/// Bundle of separated assets for offline directory export.
pub struct HtmlBundle {
    pub html: String,
    pub css: &'static str,
    pub data_js: String,
    pub d3_js: &'static str,
    pub graph_view_js: String,
}

pub fn graph_separated_bundle(data_json: &str, stat: &str) -> HtmlBundle {
    let html = graph_page(data_json, stat, Delivery::Separated);
    let data_js = format!("window.__GRAPH_DATA__ = {data_json};\n");
    HtmlBundle {
        html,
        css: GRAPH_CSS,
        data_js,
        d3_js: D3_JS,
        graph_view_js: GRAPH_VIEW_JS.clone(),
    }
}