use std::sync::LazyLock;
pub const D3_JS: &str = include_str!("d3.min.js");
pub const GRAPH_CSS: &str = include_str!("graph.css");
pub const LIVE_JS: &str = include_str!("live.js");
const ICON_FOLDER: &str = include_str!("icons/folder.svg");
const ICON_CODE: &str = include_str!("icons/code.svg");
const ICON_FILTER: &str = include_str!("icons/filter.svg");
const ICON_SIDEBAR: &str = include_str!("icons/sidebar-icon.svg");
pub const APP_LOGO: &str = include_str!("icons/app-logo.svg");
static FAVICON_DATA_URI: LazyLock<String> = LazyLock::new(|| {
format!("data:image/svg+xml;base64,{}", to_base64(APP_LOGO.as_bytes()))
});
fn to_base64(bytes: &[u8]) -> String {
const B64: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut out = String::with_capacity((bytes.len() + 2) / 3 * 4);
for chunk in bytes.chunks(3) {
let b0 = chunk[0];
let b1 = if chunk.len() > 1 { chunk[1] } else { 0 };
let b2 = if chunk.len() > 2 { chunk[2] } else { 0 };
out.push(B64[(b0 >> 2) as usize] as char);
out.push(B64[(((b0 & 3) << 4) | (b1 >> 4)) as usize] as char);
if chunk.len() > 1 {
out.push(B64[(((b1 & 0x0f) << 2) | (b2 >> 6)) as usize] as char);
} else {
out.push('=');
}
if chunk.len() > 2 {
out.push(B64[(b2 & 0x3f) as usize] as char);
} else {
out.push('=');
}
}
out
}
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"),
include_str!("graph/95-contextmenu.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">
<div id="workbench">
<aside id="leftPane" class="collapsed">
<div class="sidebar-header">
<button id="toggleLeftPane" type="button" class="btn-toggle-left" title="Toggle sidebar">__ICON_SIDEBAR__</button>
<span class="project-name" id="projectName"></span>
</div>
<div class="sidebar-body">
<div class="icon-rail">
<button type="button" class="rail-btn active" data-panel="tree" title="Files">__ICON_FOLDER__</button>
<button type="button" class="rail-btn" data-panel="code" title="Code">__ICON_CODE__</button>
<button type="button" class="rail-btn" data-panel="filters" title="Filters">__ICON_FILTER__</button>
</div>
<div class="sidebar-panel-content">
<div id="treeView" class="sidebar-panel">
<div class="tree-search-bar">
<input type="search" id="search" placeholder="filter & isolate nodes…">
</div>
<div id="treeList" class="tree-list"></div>
</div>
<div id="codeView" class="sidebar-panel" hidden>
<div class="code-bar" id="codeBar" hidden>
<span id="codePath"></span>
<span id="codeLine"></span>
</div>
<div id="codeContent" class="code-lines">
<div class="code-empty">No file selected — pick a file from the tree or click a node in the graph</div>
</div>
</div>
<div id="filtersView" class="sidebar-panel" hidden>
<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>
<label><input type="checkbox" id="colorByCommunity"> color by subsystem</label>
<button id="fitBtn" type="button">fit</button>
<span id="focusCtl" hidden>
<span id="focusLabel"></span>
<button id="focusShallower" type="button" title="shallower">−</button>
<span id="focusDepth">2</span>
<button id="focusDeeper" type="button" title="deeper">+</button>
<button id="focusClear" type="button">clear focus</button>
</span>
</div>
</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>"#;
pub fn graph_page(data_json: &str, stat: &str, delivery: Delivery) -> String {
let safe = data_json.replace('<', "\\u003c");
let body = BODY
.replace("__STAT__", stat)
.replace("__ICON_FOLDER__", ICON_FOLDER)
.replace("__ICON_CODE__", ICON_CODE)
.replace("__ICON_FILTER__", ICON_FILTER)
.replace("__ICON_SIDEBAR__", ICON_SIDEBAR);
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(),
),
};
let favicon = &*FAVICON_DATA_URI;
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\
<link rel=\"icon\" type=\"image/svg+xml\" href=\"{favicon}\">\n\
{head}\n\
</head>\n\
<body>\n\
{body}\n\
{tail}\n\
</body>\n\
</html>\n"
)
}
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(),
}
}