Skip to main content

code_ranker_viewer/
lib.rs

1//! Self-contained HTML viewer for Code Ranker: embeds one or two snapshots into
2//! a single interactive HTML file (no CDN, no external requests), and extracts
3//! a snapshot back out of a generated report.
4
5use anyhow::{Context, Result};
6use code_ranker_graph::snapshot::Snapshot;
7
8/// Pull the JSON out of `<script type="application/json" id="{id}">…</script>`
9/// and parse it into a `Snapshot`. Returns `None` if the tag is absent or holds
10/// `null`.
11pub fn extract_embedded_snapshot(html: &str, id: &str) -> Option<Result<Snapshot>> {
12    let needle = format!("id=\"{id}\">");
13    let start = html.find(&needle)? + needle.len();
14    let end = start + html[start..].find("</script>")?;
15    let body = html[start..end].trim();
16    if body.is_empty() || body == "null" {
17        return None;
18    }
19    // Undo the `</` → `<\/` escaping applied when embedding.
20    let json = body.replace("<\\/", "</");
21    Some(serde_json::from_str(&json).with_context(|| format!("parsing embedded snapshot `{id}`")))
22}
23
24// ── Assets embedded at compile time ──────────────────────────────────────────
25// The stylesheet is split into concern files; concatenated below in source order
26// to preserve the CSS cascade (a single inlined <style>, no extra requests →
27// keeps the offline guarantee).
28const ASSET_CSS_BASE: &str = include_str!("assets/base.css");
29const ASSET_CSS_MAP: &str = include_str!("assets/map.css");
30const ASSET_CSS_MODAL: &str = include_str!("assets/modal.css");
31const ASSET_CSS_TABLES: &str = include_str!("assets/tables.css");
32const ASSET_CSS_EXPORT: &str = include_str!("assets/export.css");
33const ASSET_CSS_SNAP: &str = include_str!("assets/snap.css");
34const ASSET_CSS_MAP_SVG: &str = include_str!("assets/map-svg.css");
35const ASSET_GV: &str = include_str!("assets/graphviz.umd.js");
36const ASSET_SNARKDOWN: &str = include_str!("assets/snarkdown.umd.js");
37const ASSET_SCHEMA: &str = include_str!("assets/schema.js");
38const ASSET_GROUPING: &str = include_str!("assets/grouping.js");
39const ASSET_DIFF: &str = include_str!("assets/diff.js");
40const ASSET_LAYOUT: &str = include_str!("assets/layout.js");
41const ASSET_UTILS: &str = include_str!("assets/utils.js");
42const ASSET_TOOLTIP: &str = include_str!("assets/tooltip.js");
43const ASSET_MODAL: &str = include_str!("assets/modal.js");
44const ASSET_PANZOOM: &str = include_str!("assets/panzoom.js");
45const ASSET_SOURCE_LINKS: &str = include_str!("assets/source-links.js");
46const ASSET_NODE_POPUP: &str = include_str!("assets/node-popup.js");
47const ASSET_MODAL_CONTENT: &str = include_str!("assets/modal-content.js");
48const ASSET_MAP_INTERACTIONS: &str = include_str!("assets/map-interactions.js");
49const ASSET_MAP_RENDER: &str = include_str!("assets/map-render.js");
50const ASSET_UI: &str = include_str!("assets/ui.js");
51const ASSET_SUMMARY: &str = include_str!("assets/summary.js");
52const ASSET_EXPORT_POPUP: &str = include_str!("assets/export-popup.js");
53const ASSET_NODE_TABLE: &str = include_str!("assets/node-table.js");
54const ASSET_NAV: &str = include_str!("assets/nav.js");
55const ASSET_VIEW_STATE: &str = include_str!("assets/view-state.js");
56const ASSET_SNAP_CONTROLS: &str = include_str!("assets/snap-controls.js");
57const ASSET_APP: &str = include_str!("assets/app.js");
58const ASSET_HTML: &str = include_str!("assets/index.html");
59
60/// Render a self-contained viewer with the snapshot data embedded inline. The
61/// snapshots are stored in `<script type="application/json">` tags
62/// (`cs-baseline` / `cs-current`) so they can be both read by the viewer and
63/// extracted from the HTML later (see [`extract_embedded_snapshot`]).
64/// `current` only → review; both → diff.
65pub fn render_html_viewer(baseline: Option<&Snapshot>, current: Option<&Snapshot>) -> String {
66    // Embed as JSON in a typed script tag. Escape `</` so an embedded string can never
67    // close the tag early; `JSON.parse` and serde both read `<\/` back as `</`.
68    let embed = |id: &str, snap: Option<&Snapshot>| {
69        let json = match snap {
70            Some(s) => {
71                code_ranker_graph::serialize::to_canonical_string(s).expect("serialize snapshot")
72            }
73            None => "null".to_string(),
74        };
75        format!(
76            "<script type=\"application/json\" id=\"{id}\">{}</script>",
77            json.replace("</", "<\\/")
78        )
79    };
80    let data_script = format!(
81        "{}\n{}",
82        embed("cs-baseline", baseline),
83        embed("cs-current", current),
84    );
85
86    ASSET_HTML
87        .replace(
88            r#"<link rel="stylesheet" href="./index.css">"#,
89            &format!(
90                "<style>{}{}{}{}{}{}{}</style>",
91                ASSET_CSS_BASE,
92                ASSET_CSS_MAP,
93                ASSET_CSS_MODAL,
94                ASSET_CSS_TABLES,
95                ASSET_CSS_EXPORT,
96                ASSET_CSS_SNAP,
97                ASSET_CSS_MAP_SVG,
98            ),
99        )
100        .replace(
101            r#"<script src="./graphviz.umd.js"></script>"#,
102            &format!("<script>{}</script>", ASSET_GV),
103        )
104        .replace(
105            r#"<script src="./snarkdown.umd.js"></script>"#,
106            &format!("<script>{}</script>", ASSET_SNARKDOWN),
107        )
108        .replace(r#"<script src="./data.js"></script>"#, &data_script)
109        .replace(
110            r#"<script src="./schema.js"></script>"#,
111            &format!("<script>{}</script>", ASSET_SCHEMA),
112        )
113        .replace(
114            r#"<script src="./grouping.js"></script>"#,
115            &format!("<script>{}</script>", ASSET_GROUPING),
116        )
117        .replace(
118            r#"<script src="./diff.js"></script>"#,
119            &format!("<script>{}</script>", ASSET_DIFF),
120        )
121        .replace(
122            r#"<script src="./layout.js"></script>"#,
123            &format!("<script>{}</script>", ASSET_LAYOUT),
124        )
125        .replace(
126            r#"<script src="./utils.js"></script>"#,
127            &format!("<script>{}</script>", ASSET_UTILS),
128        )
129        .replace(
130            r#"<script src="./tooltip.js"></script>"#,
131            &format!("<script>{}</script>", ASSET_TOOLTIP),
132        )
133        .replace(
134            r#"<script src="./modal.js"></script>"#,
135            &format!("<script>{}</script>", ASSET_MODAL),
136        )
137        .replace(
138            r#"<script src="./panzoom.js"></script>"#,
139            &format!("<script>{}</script>", ASSET_PANZOOM),
140        )
141        .replace(
142            r#"<script src="./source-links.js"></script>"#,
143            &format!("<script>{}</script>", ASSET_SOURCE_LINKS),
144        )
145        .replace(
146            r#"<script src="./node-popup.js"></script>"#,
147            &format!("<script>{}</script>", ASSET_NODE_POPUP),
148        )
149        .replace(
150            r#"<script src="./modal-content.js"></script>"#,
151            &format!("<script>{}</script>", ASSET_MODAL_CONTENT),
152        )
153        .replace(
154            r#"<script src="./map-interactions.js"></script>"#,
155            &format!("<script>{}</script>", ASSET_MAP_INTERACTIONS),
156        )
157        .replace(
158            r#"<script src="./map-render.js"></script>"#,
159            &format!("<script>{}</script>", ASSET_MAP_RENDER),
160        )
161        .replace(
162            r#"<script src="./ui.js"></script>"#,
163            &format!("<script>{}</script>", ASSET_UI),
164        )
165        .replace(
166            r#"<script src="./summary.js"></script>"#,
167            &format!("<script>{}</script>", ASSET_SUMMARY),
168        )
169        .replace(
170            r#"<script src="./export-popup.js"></script>"#,
171            &format!("<script>{}</script>", ASSET_EXPORT_POPUP),
172        )
173        .replace(
174            r#"<script src="./node-table.js"></script>"#,
175            &format!("<script>{}</script>", ASSET_NODE_TABLE),
176        )
177        .replace(
178            r#"<script src="./nav.js"></script>"#,
179            &format!("<script>{}</script>", ASSET_NAV),
180        )
181        .replace(
182            r#"<script src="./view-state.js"></script>"#,
183            &format!("<script>{}</script>", ASSET_VIEW_STATE),
184        )
185        .replace(
186            r#"<script src="./snap-controls.js"></script>"#,
187            &format!("<script>{}</script>", ASSET_SNAP_CONTROLS),
188        )
189        .replace(
190            r#"<script src="./app.js"></script>"#,
191            &format!("<script>{}</script>", ASSET_APP),
192        )
193}