1use jigs_core::JigMeta;
4use std::collections::BTreeMap;
5use std::path::{Path, PathBuf};
6
7const TEMPLATE: &str = include_str!("template.html");
8
9pub fn to_html(entry: Option<&str>, title: &str, editor: Option<&str>) -> String {
24 let all: BTreeMap<&'static str, &'static JigMeta> =
25 jigs_core::all_jigs().map(|m| (m.name, m)).collect();
26 let entry = entry
27 .map(str::to_string)
28 .or_else(|| all.keys().next().map(|s| s.to_string()))
29 .unwrap_or_default();
30 let visible = reachable(&all, &entry);
31 let data = encode(&visible, &entry, title, editor);
32 TEMPLATE
33 .replace("__TITLE__", &esc_attr(title))
34 .replace("__DATA__", &data)
35}
36
37fn reachable(
38 all: &BTreeMap<&'static str, &'static JigMeta>,
39 entry: &str,
40) -> BTreeMap<String, &'static JigMeta> {
41 let mut out = BTreeMap::new();
42 let mut stack = vec![entry.to_string()];
43 while let Some(name) = stack.pop() {
44 if out.contains_key(&name) {
45 continue;
46 }
47 if let Some(m) = all.get(name.as_str()) {
48 for c in m.chain {
49 stack.push(c.name.to_string());
50 }
51 out.insert(name, *m);
52 }
53 }
54 out
55}
56
57fn encode(
58 visible: &BTreeMap<String, &'static JigMeta>,
59 entry: &str,
60 title: &str,
61 editor: Option<&str>,
62) -> String {
63 let mut s = String::new();
64 s.push_str("{\"entry\":");
65 push_json_str(&mut s, entry);
66 s.push_str(",\"title\":");
67 push_json_str(&mut s, title);
68 s.push_str(",\"editor\":");
69 match editor {
70 Some(t) => push_json_str(&mut s, t),
71 None => s.push_str("null"),
72 }
73 s.push_str(",\"nodes\":{");
74 for (i, m) in visible.values().enumerate() {
75 if i > 0 {
76 s.push(',');
77 }
78 push_json_str(&mut s, m.name);
79 s.push_str(":{\"file\":");
80 push_json_str(&mut s, m.file);
81 s.push_str(",\"line\":");
82 s.push_str(&m.line.to_string());
83 s.push_str(",\"kind\":");
84 push_json_str(&mut s, m.kind);
85 s.push_str(",\"input\":");
86 push_json_str(&mut s, m.input);
87 s.push_str(",\"async\":");
88 s.push_str(if m.is_async { "true" } else { "false" });
89 s.push_str(",\"file_abs\":");
90 push_json_str(&mut s, &absolutize(m.file));
91 s.push_str(",\"basename\":");
92 push_json_str(&mut s, basename(m.file));
93 s.push_str(",\"children\":[");
94 for (j, c) in m.chain.iter().enumerate() {
95 if j > 0 {
96 s.push(',');
97 }
98 push_json_str(&mut s, c.name);
99 }
100 s.push_str("],\"child_kinds\":[");
101 for (j, c) in m.chain.iter().enumerate() {
102 if j > 0 {
103 s.push(',');
104 }
105 let k = match c.kind {
106 jigs_core::ChainKind::Then => "then",
107 jigs_core::ChainKind::Fork => "fork",
108 };
109 push_json_str(&mut s, k);
110 }
111 s.push_str("]}");
112 }
113 s.push_str("}}");
114 s
115}
116
117fn push_json_str(out: &mut String, s: &str) {
118 out.push('"');
119 for ch in s.chars() {
120 match ch {
121 '"' => out.push_str("\\\""),
122 '\\' => out.push_str("\\\\"),
123 '\n' => out.push_str("\\n"),
124 '\r' => out.push_str("\\r"),
125 '\t' => out.push_str("\\t"),
126 '<' => out.push_str("\\u003c"),
127 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
128 c => out.push(c),
129 }
130 }
131 out.push('"');
132}
133
134fn basename(file: &str) -> &str {
135 Path::new(file)
136 .file_name()
137 .and_then(|n| n.to_str())
138 .unwrap_or(file)
139}
140
141fn absolutize(file: &str) -> String {
142 let p = Path::new(file);
143 if p.is_absolute() {
144 return file.to_string();
145 }
146 match std::env::current_dir() {
147 Ok(cwd) => {
148 let joined: PathBuf = cwd.join(p);
149 joined.to_string_lossy().into_owned()
150 }
151 Err(_) => file.to_string(),
152 }
153}
154
155fn esc_attr(s: &str) -> String {
156 s.replace('&', "&")
157 .replace('<', "<")
158 .replace('>', ">")
159 .replace('"', """)
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 fn meta(name: &'static str, kind: &'static str, chain: &[&'static str]) -> JigMeta {
167 let v: Vec<jigs_core::ChainStep> = chain
168 .iter()
169 .map(|n| jigs_core::ChainStep {
170 name: n,
171 kind: jigs_core::ChainKind::Then,
172 })
173 .collect();
174 let leaked: &'static [jigs_core::ChainStep] = Box::leak(v.into_boxed_slice());
175 JigMeta {
176 name,
177 file: "test.rs",
178 line: 1,
179 kind,
180 input: "Request",
181 is_async: false,
182 chain: leaked,
183 }
184 }
185
186 fn fake(items: Vec<JigMeta>) -> BTreeMap<&'static str, &'static JigMeta> {
187 let leaked: Vec<&'static JigMeta> = items
188 .into_iter()
189 .map(|m| Box::leak(Box::new(m)) as &'static _)
190 .collect();
191 leaked.into_iter().map(|m| (m.name, m)).collect()
192 }
193
194 #[test]
195 fn reachable_filters_to_entry_subgraph() {
196 let all = fake(vec![
197 meta("root", "Response", &["a", "b"]),
198 meta("a", "Request", &[]),
199 meta("b", "Branch", &[]),
200 meta("orphan", "Other", &[]),
201 ]);
202 let r = reachable(&all, "root");
203 assert!(r.contains_key("root"));
204 assert!(r.contains_key("a"));
205 assert!(r.contains_key("b"));
206 assert!(!r.contains_key("orphan"));
207 }
208
209 #[test]
210 fn reachable_handles_cycles() {
211 let all = fake(vec![meta("a", "Other", &["b"]), meta("b", "Other", &["a"])]);
212 let r = reachable(&all, "a");
213 assert_eq!(r.len(), 2);
214 }
215
216 #[test]
217 fn encode_emits_structure() {
218 let all = fake(vec![
219 meta("root", "Response", &["a"]),
220 meta("a", "Request", &[]),
221 ]);
222 let visible = reachable(&all, "root");
223 let json = encode(&visible, "root", "demo", None);
224 assert!(json.contains("\"entry\":\"root\""));
225 assert!(json.contains("\"root\":{"));
226 assert!(json.contains("\"children\":[\"a\"]"));
227 assert!(json.contains("\"editor\":null"));
228 }
229
230 #[test]
231 fn editor_template_is_embedded_when_set() {
232 let all = fake(vec![meta("root", "Response", &[])]);
233 let visible = reachable(&all, "root");
234 let tmpl = "vscodium://file/{path}:{line}";
235 let json = encode(&visible, "root", "demo", Some(tmpl));
236 assert!(json.contains("\"editor\":\"vscodium://file/{path}:{line}\""));
237 }
238
239 #[test]
240 fn json_escapes_script_close() {
241 let all = fake(vec![meta("</script>", "Other", &[])]);
242 let visible = reachable(&all, "</script>");
243 let json = encode(&visible, "</script>", "t", None);
244 assert!(!json.contains("</script>"));
245 assert!(json.contains("\\u003c/script"));
246 }
247}