1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
use crate::e2e::escape::escape_elixir;
use crate::e2e::fixture::CallbackAction;
use std::fmt::Write as FmtWrite;
/// Build an Elixir visitor map and add setup line. Returns the visitor variable name.
pub(super) fn build_elixir_visitor(
setup_lines: &mut Vec<String>,
visitor_spec: &crate::e2e::fixture::VisitorSpec,
) -> String {
let mut visitor_obj = String::new();
let _ = writeln!(visitor_obj, "%{{");
for (method_name, action) in &visitor_spec.callbacks {
emit_elixir_visitor_method(&mut visitor_obj, method_name, action);
}
let _ = writeln!(visitor_obj, " }}");
setup_lines.push(format!("visitor = {visitor_obj}"));
"visitor".to_string()
}
/// Emit an Elixir visitor method for a callback action.
pub(super) fn emit_elixir_visitor_method(out: &mut String, method_name: &str, action: &CallbackAction) {
// Elixir uses atom keys and handle_ prefix
let handle_method = format!("handle_{}", &method_name[6..]); // strip "visit_" prefix
// The Rust NIF bridge packages every visitor argument (`_ctx`, `_text`, ...) into a
// single map and invokes the user's anonymous function with that map. Generating
// multi-arity functions like `fn(_ctx, _text) ->` therefore raised BadArityError
// ("arity 2 called with 1 argument") at runtime. Generate arity-1 functions that
// accept the args map (and ignore it) to match the bridge's calling convention.
// CustomTemplate needs to read from args; other actions can ignore it.
let arg_binding = match action {
CallbackAction::CustomTemplate { .. } => "args",
_ => "_args",
};
let _ = writeln!(out, " :{handle_method} => fn({arg_binding}) ->");
match action {
CallbackAction::Skip => {
let _ = writeln!(out, " :skip");
}
CallbackAction::Continue => {
let _ = writeln!(out, " :continue");
}
CallbackAction::PreserveHtml => {
let _ = writeln!(out, " :preserve_html");
}
CallbackAction::Custom { output } => {
let escaped = escape_elixir(output);
let _ = writeln!(out, " {{:custom, \"{escaped}\"}}");
}
CallbackAction::CustomTemplate { template, .. } => {
// Build a <> concatenation expression so {key} placeholders are substituted
// from the args map at runtime without embedding double-quoted strings inside
// a double-quoted string literal.
let expr = template_to_elixir_concat(template);
let _ = writeln!(out, " {{:custom, {expr}}}");
}
}
let _ = writeln!(out, " end,");
}
/// Convert a template like `"_{text}_"` into an Elixir `<>` concat expression:
/// `"_" <> Map.get(args, "text", "") <> "_"`.
/// Static parts are escaped via `escape_elixir`; `{key}` placeholders become
/// `Map.get(args, "key", "")` lookups into the JSON-decoded args map.
pub(super) fn template_to_elixir_concat(template: &str) -> String {
let mut parts: Vec<String> = Vec::new();
let mut static_buf = String::new();
let mut chars = template.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '{' {
let mut key = String::new();
let mut closed = false;
for kc in chars.by_ref() {
if kc == '}' {
closed = true;
break;
}
key.push(kc);
}
if closed && !key.is_empty() {
if !static_buf.is_empty() {
let escaped = escape_elixir(&static_buf);
parts.push(format!("\"{escaped}\""));
static_buf.clear();
}
let escaped_key = escape_elixir(&key);
parts.push(format!("Map.get(args, \"{escaped_key}\", \"\")"));
} else {
static_buf.push('{');
static_buf.push_str(&key);
if !closed {
// unclosed brace - treat remaining as literal
}
}
} else {
static_buf.push(ch);
}
}
if !static_buf.is_empty() {
let escaped = escape_elixir(&static_buf);
parts.push(format!("\"{escaped}\""));
}
if parts.is_empty() {
return "\"\"".to_string();
}
parts.join(" <> ")
}