1use alef_core::hash::{self, CommentStyle};
17use heck::ToSnakeCase;
18
19pub struct CallbackSpec {
24 pub c_field: String,
26 pub cs_method: String,
28 pub doc: String,
30 pub extra: Vec<ExtraParam>,
32 pub has_is_header: bool,
34}
35
36pub struct ExtraParam {
37 pub cs_name: String,
39 pub cs_type: String,
41 pub pinvoke_types: Vec<String>,
43 pub decode: String,
45}
46
47fn snake_to_lower_camel(s: &str) -> String {
54 let mut result = String::with_capacity(s.len());
55 let mut next_upper = false;
56 for ch in s.chars() {
57 if ch == '_' {
58 next_upper = true;
59 } else if next_upper {
60 result.extend(ch.to_uppercase());
61 next_upper = false;
62 } else {
63 result.push(ch);
64 }
65 }
66 result
67}
68
69pub(crate) fn callback_specs_from_trait(trait_def: &alef_core::ir::TypeDef) -> Vec<CallbackSpec> {
75 use alef_core::ir::{PrimitiveType, TypeRef};
76 use heck::ToPascalCase;
77
78 let mut specs = Vec::with_capacity(trait_def.methods.len());
79 'methods: for m in &trait_def.methods {
80 if m.trait_source.is_some() {
81 continue;
82 }
83 let cs_method = m.name.to_pascal_case();
84 let first_line = m.doc.lines().next().unwrap_or("").trim().to_string();
85 let doc = if first_line.is_empty() {
86 format!("Called for {} elements.", m.name.replace('_', " "))
87 } else {
88 first_line
89 };
90
91 let mut extra = Vec::new();
92 let mut has_is_header = false;
93
94 for p in &m.params {
95 if matches!(&p.ty, TypeRef::Named(_)) {
96 continue;
98 }
99 let raw_name = p.name.trim_start_matches('_').to_string();
100 let cs_name = snake_to_lower_camel(&raw_name);
101 let cs_name_pascal: String = {
103 let mut chars = cs_name.chars();
104 match chars.next() {
105 None => String::new(),
106 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
107 }
108 };
109
110 match (&p.ty, p.optional) {
111 (TypeRef::String, false) => {
112 let raw_var = format!("raw{cs_name_pascal}0");
113 extra.push(ExtraParam {
114 cs_name,
115 cs_type: "string".to_string(),
116 pinvoke_types: vec!["IntPtr".to_string()],
117 decode: format!("Marshal.PtrToStringUTF8({raw_var})!"),
118 });
119 }
120 (TypeRef::String, true) => {
121 let raw_var = format!("raw{cs_name_pascal}0");
122 extra.push(ExtraParam {
123 cs_name,
124 cs_type: "string?".to_string(),
125 pinvoke_types: vec!["IntPtr".to_string()],
126 decode: format!("{raw_var} == IntPtr.Zero ? null : Marshal.PtrToStringUTF8({raw_var})"),
127 });
128 }
129 (TypeRef::Primitive(PrimitiveType::Bool), false) => {
130 let raw_var = format!("raw{cs_name_pascal}0");
131 extra.push(ExtraParam {
132 cs_name,
133 cs_type: "bool".to_string(),
134 pinvoke_types: vec!["int".to_string()],
135 decode: format!("{raw_var} != 0"),
136 });
137 }
138 (
139 TypeRef::Primitive(
140 PrimitiveType::U32
141 | PrimitiveType::I32
142 | PrimitiveType::U16
143 | PrimitiveType::I16
144 | PrimitiveType::U8
145 | PrimitiveType::I8,
146 ),
147 false,
148 ) => {
149 let raw_var = format!("raw{cs_name_pascal}0");
150 extra.push(ExtraParam {
151 cs_name,
152 cs_type: "uint".to_string(),
153 pinvoke_types: vec!["uint".to_string()],
154 decode: raw_var,
155 });
156 }
157 (TypeRef::Primitive(PrimitiveType::Usize | PrimitiveType::U64 | PrimitiveType::I64), false) => {
158 let raw_var = format!("raw{cs_name_pascal}0");
159 extra.push(ExtraParam {
160 cs_name,
161 cs_type: "ulong".to_string(),
162 pinvoke_types: vec!["UIntPtr".to_string()],
163 decode: format!("(ulong){raw_var}"),
164 });
165 }
166 (TypeRef::Vec(inner), false) => match inner.as_ref() {
167 TypeRef::String => {
168 let raw_ptr = format!("raw{cs_name_pascal}0");
169 let raw_len = format!("raw{cs_name_pascal}1");
170 extra.push(ExtraParam {
171 cs_name,
172 cs_type: "string[]".to_string(),
173 pinvoke_types: vec!["IntPtr".to_string(), "UIntPtr".to_string()],
174 decode: format!("DecodeCells({raw_ptr}, (long)(ulong){raw_len})"),
175 });
176 has_is_header = true;
177 break;
178 }
179 _ => {
180 eprintln!(
181 "[alef] gen_visitor(csharp): skip method `{}` — unsupported Vec param `{}`",
182 m.name, p.name
183 );
184 continue 'methods;
185 }
186 },
187 _ => {
188 eprintln!(
189 "[alef] gen_visitor(csharp): skip method `{}` — unsupported param `{}: {:?}`",
190 m.name, p.name, p.ty
191 );
192 continue 'methods;
193 }
194 }
195 }
196
197 specs.push(CallbackSpec {
198 c_field: m.name.clone(),
199 cs_method,
200 doc,
201 extra,
202 has_is_header,
203 });
204 }
205 specs
206}
207
208pub fn gen_visitor_files(namespace: &str, trait_def: &alef_core::ir::TypeDef) -> Vec<(String, String)> {
218 let _ = callback_specs_from_trait(trait_def);
221 vec![
222 ("NodeContext.cs".to_string(), gen_node_context(namespace)),
223 ("VisitResult.cs".to_string(), gen_visit_result(namespace)),
224 ]
225}
226
227pub fn gen_native_methods_visitor(
236 namespace: &str,
237 lib_name: &str,
238 prefix: &str,
239 trait_name: &str,
240 options_field: &str,
241) -> String {
242 use crate::template_env::render;
243 use minijinja::Value;
244
245 let trait_snake = trait_name.to_snake_case();
248 let bridge_snake = format!("{prefix}_{trait_snake}_bridge");
249 let fn_bridge_new = format!("{prefix}_{bridge_snake}_new");
250 let fn_bridge_free = format!("{prefix}_{bridge_snake}_free");
251 let fn_options_set = format!("{prefix}_options_set_{options_field}");
252
253 let mut out = String::from("\n");
254 out.push_str(&render(
255 "native_methods_visitor.jinja",
256 Value::from_serialize(serde_json::json!({
257 "fn_bridge_new": fn_bridge_new,
258 "fn_bridge_free": fn_bridge_free,
259 "fn_options_set": fn_options_set,
260 })),
261 ));
262
263 let _ = namespace;
264 let _ = lib_name;
265 out
266}
267
268#[allow(dead_code)]
272pub fn gen_convert_with_visitor_method(exception_name: &str, prefix: &str) -> String {
273 let _ = exception_name;
274 let _ = prefix;
275 String::new()
276}
277
278fn gen_node_context(namespace: &str) -> String {
283 use crate::template_env::render;
284 use minijinja::Value;
285
286 let mut out = String::with_capacity(1024);
287 out.push_str(&hash::header(CommentStyle::DoubleSlash));
288 out.push_str("#nullable enable\n");
289 out.push('\n');
290 out.push_str("using System;\n");
291 out.push('\n');
292 out.push_str(&render(
293 "namespace_decl.jinja",
294 Value::from_serialize(serde_json::json!({
295 "namespace": namespace,
296 })),
297 ));
298 out.push_str("/// <summary>Context passed to every visitor callback.</summary>\n");
299 out.push_str("public record NodeContext(\n");
300 out.push_str(" /// <summary>Coarse-grained node type tag.</summary>\n");
301 out.push_str(" NodeType NodeType,\n");
302 out.push_str(" /// <summary>HTML element tag name (e.g. \"div\").</summary>\n");
303 out.push_str(" string TagName,\n");
304 out.push_str(" /// <summary>DOM depth (0 = root).</summary>\n");
305 out.push_str(" ulong Depth,\n");
306 out.push_str(" /// <summary>0-based sibling index.</summary>\n");
307 out.push_str(" ulong IndexInParent,\n");
308 out.push_str(" /// <summary>Parent element tag name, or null at the root.</summary>\n");
309 out.push_str(" string? ParentTag,\n");
310 out.push_str(" /// <summary>True when this element is treated as inline.</summary>\n");
311 out.push_str(" bool IsInline\n");
312 out.push_str(");\n");
313 out
314}
315
316fn gen_visit_result(namespace: &str) -> String {
317 use crate::template_env::render;
318 use minijinja::Value;
319
320 let mut out = String::with_capacity(2048);
321 out.push_str(&hash::header(CommentStyle::DoubleSlash));
322 out.push_str("#nullable enable\n");
323 out.push('\n');
324 out.push_str("using System;\n");
325 out.push('\n');
326 out.push_str(&render(
327 "namespace_decl.jinja",
328 Value::from_serialize(serde_json::json!({
329 "namespace": namespace,
330 })),
331 ));
332 out.push_str("/// <summary>Controls how the visitor affects the conversion pipeline.</summary>\n");
333 out.push_str("public abstract record VisitResult\n");
334 out.push_str("{\n");
335 out.push_str(" private VisitResult() {}\n");
336 out.push('\n');
337 out.push_str(" /// <summary>Proceed with default conversion.</summary>\n");
338 out.push_str(" public sealed record Continue : VisitResult;\n");
339 out.push('\n');
340 out.push_str(" /// <summary>Omit this element from output entirely.</summary>\n");
341 out.push_str(" public sealed record Skip : VisitResult;\n");
342 out.push('\n');
343 out.push_str(" /// <summary>Keep original HTML verbatim.</summary>\n");
344 out.push_str(" public sealed record PreserveHtml : VisitResult;\n");
345 out.push('\n');
346 out.push_str(" /// <summary>Replace with custom Markdown.</summary>\n");
347 out.push_str(" public sealed record Custom(string Markdown) : VisitResult;\n");
348 out.push('\n');
349 out.push_str(" /// <summary>Abort conversion with an error message.</summary>\n");
350 out.push_str(" public sealed record Error(string Message) : VisitResult;\n");
351 out.push('\n');
352 out.push_str(" internal string ToFfiJson() => this switch {\n");
353 out.push_str(" VisitResult.Continue => \"\\\"Continue\\\"\",\n");
354 out.push_str(" VisitResult.Skip => \"\\\"Skip\\\"\",\n");
355 out.push_str(" VisitResult.PreserveHtml => \"\\\"PreserveHtml\\\"\",\n");
356 out.push_str(" VisitResult.Custom c => \"{\\\"Custom\\\":\" + System.Text.Json.JsonSerializer.Serialize(c.Markdown) + \"}\",\n");
357 out.push_str(" VisitResult.Error e => \"{\\\"Error\\\":\" + System.Text.Json.JsonSerializer.Serialize(e.Message) + \"}\",\n");
358 out.push_str(" _ => \"\\\"Continue\\\"\"\n");
359 out.push_str(" };\n");
360 out.push_str("}\n");
361 out
362}
363
364