1#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct RowShape {
19 pub template: String,
21 pub walk: Vec<String>,
23 pub bindings: Vec<String>,
25}
26
27pub fn emitted_row(client_js: &str) -> RowShape {
29 let template = template_argument(client_js)
30 .unwrap_or_else(|| panic!("the emitted module has no `template(…)` call:\n{client_js}"));
31 let (walk, bindings) = statements(client_js.lines());
32 RowShape {
33 template,
34 walk,
35 bindings,
36 }
37}
38
39pub fn benchmark_row(benchmark_js: &str) -> RowShape {
41 let template = row_html_constant(benchmark_js)
42 .unwrap_or_else(|| panic!("`js/benchmark.js` has no `const ROW_HTML = '…';`"));
43 let body = between(benchmark_js, "// ZDC-EMITTED-BEGIN", "// ZDC-EMITTED-END")
44 .unwrap_or_else(|| panic!("`js/benchmark.js` has no ZDC-EMITTED-BEGIN/END region"));
45 let (walk, bindings) = statements(body.lines());
46 RowShape {
47 template,
48 walk,
49 bindings,
50 }
51}
52
53fn template_argument(source: &str) -> Option<String> {
55 let start = source.find("template('")? + "template('".len();
56 let end = source[start..].find("')")? + start;
57 Some(source[start..end].to_string())
58}
59
60fn row_html_constant(source: &str) -> Option<String> {
62 let line = source
63 .lines()
64 .find(|line| line.starts_with("const ROW_HTML = '"))?;
65 let start = line.find('\'')? + 1;
66 let end = line.rfind('\'')?;
67 (end > start).then(|| line[start..end].to_string())
68}
69
70fn between<'a>(source: &'a str, open: &str, close: &str) -> Option<&'a str> {
71 let start = source.find(open)? + open.len();
72 let end = source[start..].find(close)? + start;
73 Some(&source[start..end])
74}
75
76fn statements<'a>(lines: impl Iterator<Item = &'a str>) -> (Vec<String>, Vec<String>) {
83 let mut walk = Vec::new();
84 let mut bindings = Vec::new();
85 for line in lines {
86 let line = line.trim();
87 if let Some(rest) = line.strip_prefix("const $n") {
88 walk.push(format!("const $n{rest}"));
89 continue;
90 }
91 for name in ["bindText", "bindAttr", "bindStyle", "on"] {
92 if line.starts_with(&format!("{name}(")) {
93 bindings.push(without_last_argument(line));
94 }
95 }
96 }
97 (walk, bindings)
98}
99
100fn without_last_argument(call: &str) -> String {
103 let Some(open) = call.find('(') else {
104 return call.to_string();
105 };
106 let name = &call[..open];
107 let inner = &call[open + 1..];
108 let mut arguments: Vec<String> = Vec::new();
109 let mut current = String::new();
110 let mut depth = 0usize;
111 let mut quote: Option<char> = None;
112
113 for character in inner.chars() {
114 if let Some(open_quote) = quote {
115 current.push(character);
116 if character == open_quote {
117 quote = None;
118 }
119 continue;
120 }
121 match character {
122 '\'' | '"' => {
123 quote = Some(character);
124 current.push(character);
125 }
126 '(' | '[' | '{' => {
127 depth += 1;
128 current.push(character);
129 }
130 ')' if depth == 0 => break,
131 ')' | ']' | '}' => {
132 depth = depth.saturating_sub(1);
133 current.push(character);
134 }
135 ',' if depth == 0 => {
136 arguments.push(current.trim().to_string());
137 current.clear();
138 }
139 _ => current.push(character),
140 }
141 }
142 if !current.trim().is_empty() {
143 arguments.push(current.trim().to_string());
144 }
145 arguments.pop();
146 format!("{name}({})", arguments.join(", "))
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152
153 #[test]
154 fn the_last_argument_is_dropped_whatever_it_contains() {
155 assert_eq!(
156 without_last_argument("on($n2, 'click', () => f(a, b));"),
157 "on($n2, 'click')"
158 );
159 assert_eq!(
160 without_last_argument("bindText($n1.firstChild, rowId);"),
161 "bindText($n1.firstChild)"
162 );
163 assert_eq!(
164 without_last_argument("bindAttr($n0, 'class', () => 'a, b' + (c)());"),
165 "bindAttr($n0, 'class')"
166 );
167 }
168
169 #[test]
170 fn a_walk_and_its_bindings_are_separated() {
171 let (walk, bindings) = statements(
172 [
173 " const $n0 = $r.firstChild;",
174 " bindText($n0, x);",
175 " return mount($r, c);",
176 ]
177 .into_iter(),
178 );
179 assert_eq!(walk, vec!["const $n0 = $r.firstChild;"]);
180 assert_eq!(bindings, vec!["bindText($n0)"]);
181 }
182
183 #[test]
184 fn the_template_argument_is_read_out_of_an_emission() {
185 assert_eq!(
186 template_argument("const $t0 = template('<div></div>');\n").as_deref(),
187 Some("<div></div>")
188 );
189 }
190
191 #[test]
192 fn a_missing_marker_is_not_silently_an_empty_region() {
193 assert!(between("nothing here", "// A", "// B").is_none());
194 }
195}