Skip to main content

alef_backend_csharp/
gen_visitor.rs

1/// Generate C# visitor support: IVisitor interface, NodeContext/VisitResult records,
2/// VisitorCallbacks (P/Invoke delegate struct), and ConvertWithVisitor method.
3///
4/// # P/Invoke delegate callback strategy
5///
6/// C# uses `[UnmanagedFunctionPointer]` delegate types to create `IntPtr` function pointers
7/// that can be passed through the `HTMHtmVisitorCallbacks` C struct.
8///
9/// - `NodeContext`: a `record` with fields from `HTMHtmNodeContext`.
10/// - `VisitResult`: a discriminated union using a record class hierarchy.
11/// - `IVisitor`: an interface with default no-op implementations for all 40 callbacks.
12/// - `VisitorCallbacks`: an internal class that allocates `GCHandle`s for all delegate
13///   instances and writes them into a marshalled struct layout matching the C struct.
14/// - `ConvertWithVisitor`: static method on the wrapper class that creates the delegate
15///   struct, calls `htm_visitor_create`, `htm_convert_with_visitor`, deserialises JSON.
16use alef_core::hash::{self, CommentStyle};
17use heck::ToSnakeCase;
18
19// ---------------------------------------------------------------------------
20// Callback specification table
21// ---------------------------------------------------------------------------
22
23pub struct CallbackSpec {
24    /// Field name in `HTMHtmVisitorCallbacks`.
25    pub c_field: String,
26    /// C# interface method name (PascalCase).
27    pub cs_method: String,
28    /// XML doc summary.
29    pub doc: String,
30    /// Extra parameters beyond `NodeContext` in the C# interface.
31    pub extra: Vec<ExtraParam>,
32    /// If true, add `bool isHeader` (only visit_table_row).
33    pub has_is_header: bool,
34}
35
36pub struct ExtraParam {
37    /// C# parameter name in the interface.
38    pub cs_name: String,
39    /// C# type in the interface method signature.
40    pub cs_type: String,
41    /// P/Invoke types for each raw C parameter (one or more per Java param).
42    pub pinvoke_types: Vec<String>,
43    /// C# expression to decode the raw P/Invoke args (vars named `raw<CsName>N`).
44    pub decode: String,
45}
46
47// ---------------------------------------------------------------------------
48// IR-driven callback spec builder
49// ---------------------------------------------------------------------------
50
51/// Convert snake_case to lowerCamelCase for C# parameter names.
52/// E.g. "tag_name" → "tagName", "inputType" → "inputType" (passthrough).
53fn 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
69/// Build a `Vec<CallbackSpec>` from a trait's IR definition for the C# backend.
70///
71/// Derives all language-specific C# fields (method names, P/Invoke types, decode
72/// expressions) from `TypeRef` + `optional` flag. Methods with unsupported parameter
73/// types are skipped with a warning.
74pub(crate) fn callback_specs_from_trait(trait_def: &alef_core::ir::TypeDef) -> Vec<CallbackSpec> {
75    use alef_codegen::naming::to_csharp_name;
76    use alef_core::ir::{PrimitiveType, TypeRef};
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 = to_csharp_name(&m.name);
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                // Context parameter — skip, handled separately
97                continue;
98            }
99            let raw_name = p.name.trim_start_matches('_').to_string();
100            let cs_name = snake_to_lower_camel(&raw_name);
101            // Capitalise for the raw var names (e.g. "text" → "Text", "inputType" → "InputType")
102            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
208// ---------------------------------------------------------------------------
209// Public API
210// ---------------------------------------------------------------------------
211
212/// Returns `(filename, content)` pairs for all visitor-related C# files.
213///
214/// IVisitor.cs and VisitorCallbacks.cs are superseded by IVisitor and VisitorCallbacks
215/// in TraitBridges.cs which use the HtmlVisitorBridge approach. They are intentionally
216/// excluded here; stale committed copies are removed by delete_superseded_visitor_files.
217pub fn gen_visitor_files(namespace: &str, trait_def: &alef_core::ir::TypeDef) -> Vec<(String, String)> {
218    // callback_specs_from_trait(trait_def) drives future expansion of NodeContext.cs
219    // and VisitResult.cs when those files need IR-derived per-method data.
220    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
227/// Generate the P/Invoke declarations needed in NativeMethods.cs for visitor FFI.
228///
229/// Parameters:
230/// - `namespace`: C# namespace (unused, kept for compatibility)
231/// - `lib_name`: Native library name (unused, kept for compatibility)
232/// - `prefix`: C FFI function name prefix (e.g., "htm")
233/// - `trait_name`: Name of the visitor trait (e.g., "HtmlVisitor") for bridge function names
234/// - `options_field`: Field name in options to set visitor on (e.g., "visitor")
235pub 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    // Generate function names:
246    // htm_htm_html_visitor_bridge_new, htm_htm_html_visitor_bridge_free, htm_options_set_visitor
247    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/// DEPRECATED: gen_convert_with_visitor_method is no longer used.
269/// The visitor logic is now integrated into the main Convert() method in gen_wrapper_function,
270/// which creates the HtmlVisitorBridge and uses htm_options_set_visitor instead.
271#[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
278// ---------------------------------------------------------------------------
279// Individual file generators
280// ---------------------------------------------------------------------------
281
282fn 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// gen_ivisitor and gen_visitor_callbacks were removed: IVisitor and VisitorCallbacks
365// are now handwritten in TraitBridges.cs (HtmlVisitorBridge pattern). Generating them
366// here produced dead code that conflicted with the handwritten implementations.