use alef_core::hash::{self, CommentStyle};
use heck::ToSnakeCase;
pub struct CallbackSpec {
pub c_field: String,
pub cs_method: String,
pub doc: String,
pub extra: Vec<ExtraParam>,
pub has_is_header: bool,
}
pub struct ExtraParam {
pub cs_name: String,
pub cs_type: String,
pub pinvoke_types: Vec<String>,
pub decode: String,
}
fn snake_to_lower_camel(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut next_upper = false;
for ch in s.chars() {
if ch == '_' {
next_upper = true;
} else if next_upper {
result.extend(ch.to_uppercase());
next_upper = false;
} else {
result.push(ch);
}
}
result
}
pub(crate) fn callback_specs_from_trait(trait_def: &alef_core::ir::TypeDef) -> Vec<CallbackSpec> {
use alef_core::ir::{PrimitiveType, TypeRef};
use heck::ToPascalCase;
let mut specs = Vec::with_capacity(trait_def.methods.len());
'methods: for m in &trait_def.methods {
if m.trait_source.is_some() {
continue;
}
let cs_method = m.name.to_pascal_case();
let first_line = m.doc.lines().next().unwrap_or("").trim().to_string();
let doc = if first_line.is_empty() {
format!("Called for {} elements.", m.name.replace('_', " "))
} else {
first_line
};
let mut extra = Vec::new();
let mut has_is_header = false;
for p in &m.params {
if matches!(&p.ty, TypeRef::Named(_)) {
continue;
}
let raw_name = p.name.trim_start_matches('_').to_string();
let cs_name = snake_to_lower_camel(&raw_name);
let cs_name_pascal: String = {
let mut chars = cs_name.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
};
match (&p.ty, p.optional) {
(TypeRef::String, false) => {
let raw_var = format!("raw{cs_name_pascal}0");
extra.push(ExtraParam {
cs_name,
cs_type: "string".to_string(),
pinvoke_types: vec!["IntPtr".to_string()],
decode: format!("Marshal.PtrToStringUTF8({raw_var})!"),
});
}
(TypeRef::String, true) => {
let raw_var = format!("raw{cs_name_pascal}0");
extra.push(ExtraParam {
cs_name,
cs_type: "string?".to_string(),
pinvoke_types: vec!["IntPtr".to_string()],
decode: format!("{raw_var} == IntPtr.Zero ? null : Marshal.PtrToStringUTF8({raw_var})"),
});
}
(TypeRef::Primitive(PrimitiveType::Bool), false) => {
let raw_var = format!("raw{cs_name_pascal}0");
extra.push(ExtraParam {
cs_name,
cs_type: "bool".to_string(),
pinvoke_types: vec!["int".to_string()],
decode: format!("{raw_var} != 0"),
});
}
(
TypeRef::Primitive(
PrimitiveType::U32
| PrimitiveType::I32
| PrimitiveType::U16
| PrimitiveType::I16
| PrimitiveType::U8
| PrimitiveType::I8,
),
false,
) => {
let raw_var = format!("raw{cs_name_pascal}0");
extra.push(ExtraParam {
cs_name,
cs_type: "uint".to_string(),
pinvoke_types: vec!["uint".to_string()],
decode: raw_var,
});
}
(TypeRef::Primitive(PrimitiveType::Usize | PrimitiveType::U64 | PrimitiveType::I64), false) => {
let raw_var = format!("raw{cs_name_pascal}0");
extra.push(ExtraParam {
cs_name,
cs_type: "ulong".to_string(),
pinvoke_types: vec!["UIntPtr".to_string()],
decode: format!("(ulong){raw_var}"),
});
}
(TypeRef::Vec(inner), false) => match inner.as_ref() {
TypeRef::String => {
let raw_ptr = format!("raw{cs_name_pascal}0");
let raw_len = format!("raw{cs_name_pascal}1");
extra.push(ExtraParam {
cs_name,
cs_type: "string[]".to_string(),
pinvoke_types: vec!["IntPtr".to_string(), "UIntPtr".to_string()],
decode: format!("DecodeCells({raw_ptr}, (long)(ulong){raw_len})"),
});
has_is_header = true;
break;
}
_ => {
eprintln!(
"[alef] gen_visitor(csharp): skip method `{}` — unsupported Vec param `{}`",
m.name, p.name
);
continue 'methods;
}
},
_ => {
eprintln!(
"[alef] gen_visitor(csharp): skip method `{}` — unsupported param `{}: {:?}`",
m.name, p.name, p.ty
);
continue 'methods;
}
}
}
specs.push(CallbackSpec {
c_field: m.name.clone(),
cs_method,
doc,
extra,
has_is_header,
});
}
specs
}
pub fn gen_visitor_files(namespace: &str, trait_def: &alef_core::ir::TypeDef) -> Vec<(String, String)> {
let _ = callback_specs_from_trait(trait_def);
vec![
("NodeContext.cs".to_string(), gen_node_context(namespace)),
("VisitResult.cs".to_string(), gen_visit_result(namespace)),
]
}
pub fn gen_native_methods_visitor(
namespace: &str,
lib_name: &str,
prefix: &str,
trait_name: &str,
options_field: &str,
) -> String {
use crate::template_env::render;
use minijinja::Value;
let trait_snake = trait_name.to_snake_case();
let bridge_snake = format!("{prefix}_{trait_snake}_bridge");
let fn_bridge_new = format!("{prefix}_{bridge_snake}_new");
let fn_bridge_free = format!("{prefix}_{bridge_snake}_free");
let fn_options_set = format!("{prefix}_options_set_{options_field}");
let mut out = String::from("\n");
out.push_str(&render(
"native_methods_visitor.jinja",
Value::from_serialize(serde_json::json!({
"fn_bridge_new": fn_bridge_new,
"fn_bridge_free": fn_bridge_free,
"fn_options_set": fn_options_set,
})),
));
let _ = namespace;
let _ = lib_name;
out
}
#[allow(dead_code)]
pub fn gen_convert_with_visitor_method(exception_name: &str, prefix: &str) -> String {
let _ = exception_name;
let _ = prefix;
String::new()
}
fn gen_node_context(namespace: &str) -> String {
use crate::template_env::render;
use minijinja::Value;
let mut out = String::with_capacity(1024);
out.push_str(&hash::header(CommentStyle::DoubleSlash));
out.push_str("#nullable enable\n");
out.push('\n');
out.push_str("using System;\n");
out.push('\n');
out.push_str(&render(
"namespace_decl.jinja",
Value::from_serialize(serde_json::json!({
"namespace": namespace,
})),
));
out.push_str("/// <summary>Context passed to every visitor callback.</summary>\n");
out.push_str("public record NodeContext(\n");
out.push_str(" /// <summary>Coarse-grained node type tag.</summary>\n");
out.push_str(" NodeType NodeType,\n");
out.push_str(" /// <summary>HTML element tag name (e.g. \"div\").</summary>\n");
out.push_str(" string TagName,\n");
out.push_str(" /// <summary>DOM depth (0 = root).</summary>\n");
out.push_str(" ulong Depth,\n");
out.push_str(" /// <summary>0-based sibling index.</summary>\n");
out.push_str(" ulong IndexInParent,\n");
out.push_str(" /// <summary>Parent element tag name, or null at the root.</summary>\n");
out.push_str(" string? ParentTag,\n");
out.push_str(" /// <summary>True when this element is treated as inline.</summary>\n");
out.push_str(" bool IsInline\n");
out.push_str(");\n");
out
}
fn gen_visit_result(namespace: &str) -> String {
use crate::template_env::render;
use minijinja::Value;
let mut out = String::with_capacity(2048);
out.push_str(&hash::header(CommentStyle::DoubleSlash));
out.push_str("#nullable enable\n");
out.push('\n');
out.push_str("using System;\n");
out.push('\n');
out.push_str(&render(
"namespace_decl.jinja",
Value::from_serialize(serde_json::json!({
"namespace": namespace,
})),
));
out.push_str("/// <summary>Controls how the visitor affects the conversion pipeline.</summary>\n");
out.push_str("public abstract record VisitResult\n");
out.push_str("{\n");
out.push_str(" private VisitResult() {}\n");
out.push('\n');
out.push_str(" /// <summary>Proceed with default conversion.</summary>\n");
out.push_str(" public sealed record Continue : VisitResult;\n");
out.push('\n');
out.push_str(" /// <summary>Omit this element from output entirely.</summary>\n");
out.push_str(" public sealed record Skip : VisitResult;\n");
out.push('\n');
out.push_str(" /// <summary>Keep original HTML verbatim.</summary>\n");
out.push_str(" public sealed record PreserveHtml : VisitResult;\n");
out.push('\n');
out.push_str(" /// <summary>Replace with custom Markdown.</summary>\n");
out.push_str(" public sealed record Custom(string Markdown) : VisitResult;\n");
out.push('\n');
out.push_str(" /// <summary>Abort conversion with an error message.</summary>\n");
out.push_str(" public sealed record Error(string Message) : VisitResult;\n");
out.push('\n');
out.push_str(" internal string ToFfiJson() => this switch {\n");
out.push_str(" VisitResult.Continue => \"\\\"Continue\\\"\",\n");
out.push_str(" VisitResult.Skip => \"\\\"Skip\\\"\",\n");
out.push_str(" VisitResult.PreserveHtml => \"\\\"PreserveHtml\\\"\",\n");
out.push_str(" VisitResult.Custom c => \"{{\\\"Custom\\\":\" + System.Text.Json.JsonSerializer.Serialize(c.Markdown) + \"}}\",\n");
out.push_str(" VisitResult.Error e => \"{{\\\"Error\\\":\" + System.Text.Json.JsonSerializer.Serialize(e.Message) + \"}}\",\n");
out.push_str(" _ => \"\\\"Continue\\\"\"\n");
out.push_str(" };\n");
out.push_str("}\n");
out
}