1use serde_json::Value;
6
7const CSS: &str = include_str!("viewer/style.css");
8const JS: &str = include_str!("viewer/app.js");
9
10pub fn render_html(projection: &Value) -> String {
12 let payload = embed_json(projection);
13 let mut html = String::with_capacity(8192 + CSS.len() + JS.len() + payload.len());
14 html.push_str("<!DOCTYPE html>\n<html lang=\"en\" data-viewer=\"candle-graph\">\n<head>\n");
15 html.push_str("<meta charset=\"utf-8\"/>\n");
16 html.push_str("<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"/>\n");
17 html.push_str("<title>candle-graph visualizer</title>\n<style>");
18 html.push_str(CSS);
19 html.push_str("</style>\n</head>\n<body>\n");
20 html.push_str("<a class=\"skip\" href=\"#canvas-pane\">Skip to canvas</a>\n");
21
22 html.push_str("<header class=\"top\" role=\"banner\">\n");
24 html.push_str(" <div class=\"brand\"><span class=\"brand-mark\" aria-hidden=\"true\"></span>candle-graph</div>\n");
25 html.push_str(" <div class=\"cov\" data-coverage role=\"status\" aria-live=\"polite\"></div>\n");
26 html.push_str(" <div class=\"top-actions\">\n");
27 html.push_str(" <button type=\"button\" class=\"btn\" id=\"print-btn\" aria-label=\"Toggle publication preview\">Preview</button>\n");
28 html.push_str(" <button type=\"button\" class=\"btn primary\" id=\"export-btn\" aria-label=\"Export graph as SVG\">Export SVG</button>\n");
29 html.push_str(" <button type=\"button\" class=\"btn\" id=\"theme-btn\" data-theme-toggle aria-label=\"Toggle color theme\">Theme</button>\n");
30 html.push_str(" </div>\n");
31 html.push_str("</header>\n");
32
33 html.push_str("<div class=\"layout\" id=\"app\">\n");
35
36 html.push_str(" <nav class=\"pane pane-sidebar\" data-pane=\"sidebar\" aria-label=\"Navigation\">\n");
38 html.push_str(" <div class=\"pane-h\">Views</div>\n");
39 html.push_str(" <div class=\"tabs\" data-view-tabs role=\"tablist\" aria-label=\"Graph views\"></div>\n");
40 html.push_str(" <div class=\"search-box\" id=\"search-section\">\n");
41 html.push_str(" <label class=\"sr\" for=\"node-search\">Search graph nodes</label>\n");
42 html.push_str(" <input id=\"node-search\" type=\"search\" placeholder=\"Search nodes…\" autocomplete=\"off\"/>\n");
43 html.push_str(" </div>\n");
44 html.push_str(" <div id=\"tree-section\">\n");
45 html.push_str(" <div class=\"pane-h sub\">Module tree</div>\n");
46 html.push_str(" <div class=\"search-box\">\n");
47 html.push_str(" <label class=\"sr\" for=\"mod-search\">Search modules</label>\n");
48 html.push_str(" <input id=\"mod-search\" data-module-search type=\"search\" placeholder=\"Filter modules…\" autocomplete=\"off\"/>\n");
49 html.push_str(" </div>\n");
50 html.push_str(" <div id=\"module-tree\" class=\"scroll-area\" data-module-tree tabindex=\"0\" role=\"tree\" aria-label=\"Modules\"></div>\n");
51 html.push_str(" </div>\n");
52 html.push_str(" <div class=\"pane-h sub\">Findings</div>\n");
53 html.push_str(" <div id=\"findings-list\" class=\"scroll-area\" data-findings-list role=\"list\" aria-label=\"Findings\"></div>\n");
54 html.push_str(" <div class=\"diag\" data-diagnostics aria-label=\"Diagnostics\"></div>\n");
55 html.push_str(" </nav>\n");
56
57 html.push_str(" <div class=\"resize-handle\" data-side=\"left\" aria-hidden=\"true\"></div>\n");
58
59 html.push_str(" <main class=\"pane pane-canvas\" data-pane=\"canvas\" id=\"canvas-pane\" aria-label=\"Graph canvas\">\n");
61 html.push_str(" <div class=\"pane-h row\"><span data-canvas-title>Graph</span><span class=\"graph-stats\" data-graph-stats></span></div>\n");
62 html.push_str(" <div class=\"graph-toolbar\" id=\"graph-toolbar\" hidden>\n");
63 html.push_str(" <label class=\"sr\" for=\"fn-filter\">Function</label>\n");
64 html.push_str(" <select id=\"fn-filter\" data-function-filter aria-label=\"Filter by function\"></select>\n");
65 html.push_str(" <button type=\"button\" class=\"btn\" id=\"fit-btn\" aria-label=\"Fit graph to view\">Fit</button>\n");
66 html.push_str(" <button type=\"button\" class=\"btn\" id=\"reset-btn\" aria-label=\"Reset zoom and pan\">Reset</button>\n");
67 html.push_str(" </div>\n");
68 html.push_str(" <div class=\"graph-banner\" id=\"graph-banner\" hidden role=\"status\"></div>\n");
69 html.push_str(" <div class=\"canvas-wrap\" id=\"canvas-wrap\">\n");
70 html.push_str(" <div id=\"empty-graph\" class=\"empty\" data-empty-state hidden>\n");
71 html.push_str(" <p><strong>No graph data</strong></p>\n");
72 html.push_str(" <p>This view has no nodes to display for the current analysis.</p>\n");
73 html.push_str(" </div>\n");
74 html.push_str(" <svg id=\"graph-canvas\" data-canvas role=\"img\" aria-label=\"Graph\" tabindex=\"0\"></svg>\n");
75 html.push_str(" <div id=\"graph-tooltip\" class=\"graph-tooltip\" role=\"tooltip\" aria-hidden=\"true\"></div>\n");
76 html.push_str(" <div class=\"legend-float\" data-legend role=\"list\" aria-label=\"Graph legend\">\n");
77 html.push_str(" <button type=\"button\" class=\"legend-toggle\" id=\"legend-toggle\" aria-expanded=\"true\">Legend</button>\n");
78 html.push_str(" <div class=\"legend-body\">\n");
79 html.push_str(" <span role=\"listitem\" class=\"lg kind-op\">Operation</span>\n");
80 html.push_str(" <span role=\"listitem\" class=\"lg kind-param\">Parameter</span>\n");
81 html.push_str(" <span role=\"listitem\" class=\"lg kind-tensor\">Tensor</span>\n");
82 html.push_str(" <span role=\"listitem\" class=\"lg kind-module\">Module</span>\n");
83 for state in [
84 "Trainable",
85 "Frozen",
86 "Differentiable",
87 "Severed",
88 "LayoutDependent",
89 "Unknown",
90 ] {
91 html.push_str(&format!(
92 " <span role=\"listitem\" data-legend-item=\"{state}\" class=\"lg {state}\">{state}</span>\n"
93 ));
94 }
95 html.push_str(" </div>\n");
96 html.push_str(" </div>\n");
97 html.push_str(" <div class=\"canvas-controls\" aria-label=\"Zoom controls\">\n");
98 html.push_str(" <button type=\"button\" class=\"btn icon\" id=\"zoom-in\" aria-label=\"Zoom in\" title=\"Zoom in (+)\">+</button>\n");
99 html.push_str(" <div class=\"zoom-label\" id=\"zoom-label\">100%</div>\n");
100 html.push_str(" <button type=\"button\" class=\"btn icon\" id=\"zoom-out\" aria-label=\"Zoom out\" title=\"Zoom out (−)\">−</button>\n");
101 html.push_str(" <button type=\"button\" class=\"btn icon\" id=\"zoom-fit\" aria-label=\"Fit to view\" title=\"Fit (F)\">⤢</button>\n");
102 html.push_str(" </div>\n");
103 html.push_str(" <p class=\"canvas-hint\" id=\"canvas-hint\"><kbd>U</kbd> clear · click background · <kbd>F</kbd> fit · <kbd>+</kbd><kbd>−</kbd> zoom</p>\n");
104 html.push_str(" </div>\n");
105 html.push_str(" </main>\n");
106
107 html.push_str(" <div class=\"resize-handle\" data-side=\"right\" aria-hidden=\"true\"></div>\n");
108
109 html.push_str(" <aside class=\"pane pane-inspector\" data-pane=\"inspector\" aria-label=\"Inspector\">\n");
111 html.push_str(" <div class=\"pane-h\">Inspector</div>\n");
112 html.push_str(" <dl id=\"inspector\" data-inspector>\n");
113 html.push_str(" <div class=\"section-title\">Identity</div>\n");
114 for (field, title) in [
115 ("label", "Label"),
116 ("kind", "Kind"),
117 ("qualified", "Qualified name"),
118 ("source", "Source"),
119 ] {
120 html.push_str(&format!(
121 " <div><dt>{title}</dt><dd data-field=\"{field}\">—</dd></div>\n"
122 ));
123 }
124 html.push_str(" <div class=\"section-title\">Tensor</div>\n");
125 for (field, title) in [
126 ("shape", "Shape"),
127 ("dtype", "Dtype"),
128 ("grad", "Grad state"),
129 ("role", "Role"),
130 ] {
131 html.push_str(&format!(
132 " <div><dt>{title}</dt><dd data-field=\"{field}\">—</dd></div>\n"
133 ));
134 }
135 html.push_str(" <div class=\"section-title\">Analysis</div>\n");
136 for (field, title) in [
137 ("root", "Builder root"),
138 ("confidence", "Confidence"),
139 ("timing", "Timing (avg · min–max)"),
140 ("function", "Function"),
141 ("severity", "Severity"),
142 ("rule", "Rule"),
143 ] {
144 html.push_str(&format!(
145 " <div><dt>{title}</dt><dd data-field=\"{field}\">—</dd></div>\n"
146 ));
147 }
148 html.push_str(" </dl>\n");
149 html.push_str(" </aside>\n");
150 html.push_str("</div>\n");
151
152 html.push_str("<script id=\"cg-payload\" type=\"application/json\">");
153 html.push_str(&payload);
154 html.push_str("</script>\n<script>");
155 html.push_str(include_str!("viewer/dagre.min.js"));
156 html.push_str("</script>\n<script>");
157 html.push_str(include_str!("viewer/layout.js"));
158 html.push_str("</script>\n<script>");
159 html.push_str(JS);
160 html.push_str("</script>\n</body>\n</html>\n");
161 html
162}
163
164pub fn embed_json(value: &Value) -> String {
166 escape_for_script(&value.to_string())
167}
168
169pub fn escape_for_script(s: &str) -> String {
171 let mut out = String::with_capacity(s.len());
172 for ch in s.chars() {
173 match ch {
174 '<' => out.push_str("\\u003c"),
175 '>' => out.push_str("\\u003e"),
176 '\u{2028}' => out.push_str("\\u2028"),
177 '\u{2029}' => out.push_str("\\u2029"),
178 _ => out.push(ch),
179 }
180 }
181 out
182}