#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RowShape {
pub template: String,
pub walk: Vec<String>,
pub bindings: Vec<String>,
}
pub fn emitted_row(client_js: &str) -> RowShape {
let template = template_argument(client_js)
.unwrap_or_else(|| panic!("the emitted module has no `template(…)` call:\n{client_js}"));
let (walk, bindings) = statements(client_js.lines());
RowShape {
template,
walk,
bindings,
}
}
pub fn benchmark_row(benchmark_js: &str) -> RowShape {
let template = row_html_constant(benchmark_js)
.unwrap_or_else(|| panic!("`js/benchmark.js` has no `const ROW_HTML = '…';`"));
let body = between(benchmark_js, "// ZDC-EMITTED-BEGIN", "// ZDC-EMITTED-END")
.unwrap_or_else(|| panic!("`js/benchmark.js` has no ZDC-EMITTED-BEGIN/END region"));
let (walk, bindings) = statements(body.lines());
RowShape {
template,
walk,
bindings,
}
}
fn template_argument(source: &str) -> Option<String> {
let start = source.find("template('")? + "template('".len();
let end = source[start..].find("')")? + start;
Some(source[start..end].to_string())
}
fn row_html_constant(source: &str) -> Option<String> {
let line = source
.lines()
.find(|line| line.starts_with("const ROW_HTML = '"))?;
let start = line.find('\'')? + 1;
let end = line.rfind('\'')?;
(end > start).then(|| line[start..end].to_string())
}
fn between<'a>(source: &'a str, open: &str, close: &str) -> Option<&'a str> {
let start = source.find(open)? + open.len();
let end = source[start..].find(close)? + start;
Some(&source[start..end])
}
fn statements<'a>(lines: impl Iterator<Item = &'a str>) -> (Vec<String>, Vec<String>) {
let mut walk = Vec::new();
let mut bindings = Vec::new();
for line in lines {
let line = line.trim();
if let Some(rest) = line.strip_prefix("const $n") {
walk.push(format!("const $n{rest}"));
continue;
}
for name in ["bindText", "bindAttr", "bindStyle", "on"] {
if line.starts_with(&format!("{name}(")) {
bindings.push(without_last_argument(line));
}
}
}
(walk, bindings)
}
fn without_last_argument(call: &str) -> String {
let Some(open) = call.find('(') else {
return call.to_string();
};
let name = &call[..open];
let inner = &call[open + 1..];
let mut arguments: Vec<String> = Vec::new();
let mut current = String::new();
let mut depth = 0usize;
let mut quote: Option<char> = None;
for character in inner.chars() {
if let Some(open_quote) = quote {
current.push(character);
if character == open_quote {
quote = None;
}
continue;
}
match character {
'\'' | '"' => {
quote = Some(character);
current.push(character);
}
'(' | '[' | '{' => {
depth += 1;
current.push(character);
}
')' if depth == 0 => break,
')' | ']' | '}' => {
depth = depth.saturating_sub(1);
current.push(character);
}
',' if depth == 0 => {
arguments.push(current.trim().to_string());
current.clear();
}
_ => current.push(character),
}
}
if !current.trim().is_empty() {
arguments.push(current.trim().to_string());
}
arguments.pop();
format!("{name}({})", arguments.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_last_argument_is_dropped_whatever_it_contains() {
assert_eq!(
without_last_argument("on($n2, 'click', () => f(a, b));"),
"on($n2, 'click')"
);
assert_eq!(
without_last_argument("bindText($n1.firstChild, rowId);"),
"bindText($n1.firstChild)"
);
assert_eq!(
without_last_argument("bindAttr($n0, 'class', () => 'a, b' + (c)());"),
"bindAttr($n0, 'class')"
);
}
#[test]
fn a_walk_and_its_bindings_are_separated() {
let (walk, bindings) = statements(
[
" const $n0 = $r.firstChild;",
" bindText($n0, x);",
" return mount($r, c);",
]
.into_iter(),
);
assert_eq!(walk, vec!["const $n0 = $r.firstChild;"]);
assert_eq!(bindings, vec!["bindText($n0)"]);
}
#[test]
fn the_template_argument_is_read_out_of_an_emission() {
assert_eq!(
template_argument("const $t0 = template('<div></div>');\n").as_deref(),
Some("<div></div>")
);
}
#[test]
fn a_missing_marker_is_not_silently_an_empty_region() {
assert!(between("nothing here", "// A", "// B").is_none());
}
}