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 std::fmt::Write;
17
18// ---------------------------------------------------------------------------
19// Callback specification table
20// ---------------------------------------------------------------------------
21
22pub struct CallbackSpec {
23    /// Field name in `HTMHtmVisitorCallbacks`.
24    pub c_field: &'static str,
25    /// C# interface method name (PascalCase).
26    pub cs_method: &'static str,
27    /// XML doc summary.
28    pub doc: &'static str,
29    /// Extra parameters beyond `NodeContext` in the C# interface.
30    pub extra: &'static [ExtraParam],
31    /// If true, add `bool isHeader` (only visit_table_row).
32    pub has_is_header: bool,
33}
34
35pub struct ExtraParam {
36    /// C# parameter name in the interface.
37    pub cs_name: &'static str,
38    /// C# type in the interface method signature.
39    pub cs_type: &'static str,
40    /// P/Invoke types for each raw C parameter (one or more per Java param).
41    pub pinvoke_types: &'static [&'static str],
42    /// C# expression to decode the raw P/Invoke args (vars named `raw<CsName>N`).
43    pub decode: &'static str,
44}
45
46pub const CALLBACKS: &[CallbackSpec] = &[
47    CallbackSpec {
48        c_field: "visit_text",
49        cs_method: "VisitText",
50        doc: "Called for text nodes.",
51        extra: &[ExtraParam {
52            cs_name: "text",
53            cs_type: "string",
54            pinvoke_types: &["IntPtr"],
55            decode: "Marshal.PtrToStringAnsi(rawText0)!",
56        }],
57        has_is_header: false,
58    },
59    CallbackSpec {
60        c_field: "visit_element_start",
61        cs_method: "VisitElementStart",
62        doc: "Called before entering any element.",
63        extra: &[],
64        has_is_header: false,
65    },
66    CallbackSpec {
67        c_field: "visit_element_end",
68        cs_method: "VisitElementEnd",
69        doc: "Called after exiting any element; receives the default markdown output.",
70        extra: &[ExtraParam {
71            cs_name: "output",
72            cs_type: "string",
73            pinvoke_types: &["IntPtr"],
74            decode: "Marshal.PtrToStringAnsi(rawOutput0)!",
75        }],
76        has_is_header: false,
77    },
78    CallbackSpec {
79        c_field: "visit_link",
80        cs_method: "VisitLink",
81        doc: "Called for anchor links. title is null when the attribute is absent.",
82        extra: &[
83            ExtraParam {
84                cs_name: "href",
85                cs_type: "string",
86                pinvoke_types: &["IntPtr"],
87                decode: "Marshal.PtrToStringAnsi(rawHref0)!",
88            },
89            ExtraParam {
90                cs_name: "text",
91                cs_type: "string",
92                pinvoke_types: &["IntPtr"],
93                decode: "Marshal.PtrToStringAnsi(rawText0)!",
94            },
95            ExtraParam {
96                cs_name: "title",
97                cs_type: "string?",
98                pinvoke_types: &["IntPtr"],
99                decode: "rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawTitle0)",
100            },
101        ],
102        has_is_header: false,
103    },
104    CallbackSpec {
105        c_field: "visit_image",
106        cs_method: "VisitImage",
107        doc: "Called for images. title is null when absent.",
108        extra: &[
109            ExtraParam {
110                cs_name: "src",
111                cs_type: "string",
112                pinvoke_types: &["IntPtr"],
113                decode: "Marshal.PtrToStringAnsi(rawSrc0)!",
114            },
115            ExtraParam {
116                cs_name: "alt",
117                cs_type: "string",
118                pinvoke_types: &["IntPtr"],
119                decode: "Marshal.PtrToStringAnsi(rawAlt0)!",
120            },
121            ExtraParam {
122                cs_name: "title",
123                cs_type: "string?",
124                pinvoke_types: &["IntPtr"],
125                decode: "rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawTitle0)",
126            },
127        ],
128        has_is_header: false,
129    },
130    CallbackSpec {
131        c_field: "visit_heading",
132        cs_method: "VisitHeading",
133        doc: "Called for heading elements h1-h6. id is null when absent.",
134        extra: &[
135            ExtraParam {
136                cs_name: "level",
137                cs_type: "uint",
138                pinvoke_types: &["uint"],
139                decode: "rawLevel0",
140            },
141            ExtraParam {
142                cs_name: "text",
143                cs_type: "string",
144                pinvoke_types: &["IntPtr"],
145                decode: "Marshal.PtrToStringAnsi(rawText0)!",
146            },
147            ExtraParam {
148                cs_name: "id",
149                cs_type: "string?",
150                pinvoke_types: &["IntPtr"],
151                decode: "rawId0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawId0)",
152            },
153        ],
154        has_is_header: false,
155    },
156    CallbackSpec {
157        c_field: "visit_code_block",
158        cs_method: "VisitCodeBlock",
159        doc: "Called for code blocks. lang is null when absent.",
160        extra: &[
161            ExtraParam {
162                cs_name: "lang",
163                cs_type: "string?",
164                pinvoke_types: &["IntPtr"],
165                decode: "rawLang0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawLang0)",
166            },
167            ExtraParam {
168                cs_name: "code",
169                cs_type: "string",
170                pinvoke_types: &["IntPtr"],
171                decode: "Marshal.PtrToStringAnsi(rawCode0)!",
172            },
173        ],
174        has_is_header: false,
175    },
176    CallbackSpec {
177        c_field: "visit_code_inline",
178        cs_method: "VisitCodeInline",
179        doc: "Called for inline code elements.",
180        extra: &[ExtraParam {
181            cs_name: "code",
182            cs_type: "string",
183            pinvoke_types: &["IntPtr"],
184            decode: "Marshal.PtrToStringAnsi(rawCode0)!",
185        }],
186        has_is_header: false,
187    },
188    CallbackSpec {
189        c_field: "visit_list_item",
190        cs_method: "VisitListItem",
191        doc: "Called for list items.",
192        extra: &[
193            ExtraParam {
194                cs_name: "ordered",
195                cs_type: "bool",
196                pinvoke_types: &["int"],
197                decode: "rawOrdered0 != 0",
198            },
199            ExtraParam {
200                cs_name: "marker",
201                cs_type: "string",
202                pinvoke_types: &["IntPtr"],
203                decode: "Marshal.PtrToStringAnsi(rawMarker0)!",
204            },
205            ExtraParam {
206                cs_name: "text",
207                cs_type: "string",
208                pinvoke_types: &["IntPtr"],
209                decode: "Marshal.PtrToStringAnsi(rawText0)!",
210            },
211        ],
212        has_is_header: false,
213    },
214    CallbackSpec {
215        c_field: "visit_list_start",
216        cs_method: "VisitListStart",
217        doc: "Called before processing a list.",
218        extra: &[ExtraParam {
219            cs_name: "ordered",
220            cs_type: "bool",
221            pinvoke_types: &["int"],
222            decode: "rawOrdered0 != 0",
223        }],
224        has_is_header: false,
225    },
226    CallbackSpec {
227        c_field: "visit_list_end",
228        cs_method: "VisitListEnd",
229        doc: "Called after processing a list.",
230        extra: &[
231            ExtraParam {
232                cs_name: "ordered",
233                cs_type: "bool",
234                pinvoke_types: &["int"],
235                decode: "rawOrdered0 != 0",
236            },
237            ExtraParam {
238                cs_name: "output",
239                cs_type: "string",
240                pinvoke_types: &["IntPtr"],
241                decode: "Marshal.PtrToStringAnsi(rawOutput0)!",
242            },
243        ],
244        has_is_header: false,
245    },
246    CallbackSpec {
247        c_field: "visit_table_start",
248        cs_method: "VisitTableStart",
249        doc: "Called before processing a table.",
250        extra: &[],
251        has_is_header: false,
252    },
253    CallbackSpec {
254        c_field: "visit_table_row",
255        cs_method: "VisitTableRow",
256        doc: "Called for table rows. cells contains the cell text values.",
257        extra: &[ExtraParam {
258            cs_name: "cells",
259            cs_type: "string[]",
260            pinvoke_types: &["IntPtr", "UIntPtr"],
261            decode: "DecodeCells(rawCells0, (long)(ulong)rawCells1)",
262        }],
263        has_is_header: true,
264    },
265    CallbackSpec {
266        c_field: "visit_table_end",
267        cs_method: "VisitTableEnd",
268        doc: "Called after processing a table.",
269        extra: &[ExtraParam {
270            cs_name: "output",
271            cs_type: "string",
272            pinvoke_types: &["IntPtr"],
273            decode: "Marshal.PtrToStringAnsi(rawOutput0)!",
274        }],
275        has_is_header: false,
276    },
277    CallbackSpec {
278        c_field: "visit_blockquote",
279        cs_method: "VisitBlockquote",
280        doc: "Called for blockquote elements.",
281        extra: &[
282            ExtraParam {
283                cs_name: "content",
284                cs_type: "string",
285                pinvoke_types: &["IntPtr"],
286                decode: "Marshal.PtrToStringAnsi(rawContent0)!",
287            },
288            ExtraParam {
289                cs_name: "depth",
290                cs_type: "ulong",
291                pinvoke_types: &["UIntPtr"],
292                decode: "(ulong)rawDepth0",
293            },
294        ],
295        has_is_header: false,
296    },
297    CallbackSpec {
298        c_field: "visit_strong",
299        cs_method: "VisitStrong",
300        doc: "Called for strong/bold elements.",
301        extra: &[ExtraParam {
302            cs_name: "text",
303            cs_type: "string",
304            pinvoke_types: &["IntPtr"],
305            decode: "Marshal.PtrToStringAnsi(rawText0)!",
306        }],
307        has_is_header: false,
308    },
309    CallbackSpec {
310        c_field: "visit_emphasis",
311        cs_method: "VisitEmphasis",
312        doc: "Called for emphasis/italic elements.",
313        extra: &[ExtraParam {
314            cs_name: "text",
315            cs_type: "string",
316            pinvoke_types: &["IntPtr"],
317            decode: "Marshal.PtrToStringAnsi(rawText0)!",
318        }],
319        has_is_header: false,
320    },
321    CallbackSpec {
322        c_field: "visit_strikethrough",
323        cs_method: "VisitStrikethrough",
324        doc: "Called for strikethrough elements.",
325        extra: &[ExtraParam {
326            cs_name: "text",
327            cs_type: "string",
328            pinvoke_types: &["IntPtr"],
329            decode: "Marshal.PtrToStringAnsi(rawText0)!",
330        }],
331        has_is_header: false,
332    },
333    CallbackSpec {
334        c_field: "visit_underline",
335        cs_method: "VisitUnderline",
336        doc: "Called for underline elements.",
337        extra: &[ExtraParam {
338            cs_name: "text",
339            cs_type: "string",
340            pinvoke_types: &["IntPtr"],
341            decode: "Marshal.PtrToStringAnsi(rawText0)!",
342        }],
343        has_is_header: false,
344    },
345    CallbackSpec {
346        c_field: "visit_subscript",
347        cs_method: "VisitSubscript",
348        doc: "Called for subscript elements.",
349        extra: &[ExtraParam {
350            cs_name: "text",
351            cs_type: "string",
352            pinvoke_types: &["IntPtr"],
353            decode: "Marshal.PtrToStringAnsi(rawText0)!",
354        }],
355        has_is_header: false,
356    },
357    CallbackSpec {
358        c_field: "visit_superscript",
359        cs_method: "VisitSuperscript",
360        doc: "Called for superscript elements.",
361        extra: &[ExtraParam {
362            cs_name: "text",
363            cs_type: "string",
364            pinvoke_types: &["IntPtr"],
365            decode: "Marshal.PtrToStringAnsi(rawText0)!",
366        }],
367        has_is_header: false,
368    },
369    CallbackSpec {
370        c_field: "visit_mark",
371        cs_method: "VisitMark",
372        doc: "Called for mark/highlight elements.",
373        extra: &[ExtraParam {
374            cs_name: "text",
375            cs_type: "string",
376            pinvoke_types: &["IntPtr"],
377            decode: "Marshal.PtrToStringAnsi(rawText0)!",
378        }],
379        has_is_header: false,
380    },
381    CallbackSpec {
382        c_field: "visit_line_break",
383        cs_method: "VisitLineBreak",
384        doc: "Called for line break elements.",
385        extra: &[],
386        has_is_header: false,
387    },
388    CallbackSpec {
389        c_field: "visit_horizontal_rule",
390        cs_method: "VisitHorizontalRule",
391        doc: "Called for horizontal rule elements.",
392        extra: &[],
393        has_is_header: false,
394    },
395    CallbackSpec {
396        c_field: "visit_custom_element",
397        cs_method: "VisitCustomElement",
398        doc: "Called for custom or unknown elements.",
399        extra: &[
400            ExtraParam {
401                cs_name: "tagName",
402                cs_type: "string",
403                pinvoke_types: &["IntPtr"],
404                decode: "Marshal.PtrToStringAnsi(rawTagName0)!",
405            },
406            ExtraParam {
407                cs_name: "html",
408                cs_type: "string",
409                pinvoke_types: &["IntPtr"],
410                decode: "Marshal.PtrToStringAnsi(rawHtml0)!",
411            },
412        ],
413        has_is_header: false,
414    },
415    CallbackSpec {
416        c_field: "visit_definition_list_start",
417        cs_method: "VisitDefinitionListStart",
418        doc: "Called before a definition list.",
419        extra: &[],
420        has_is_header: false,
421    },
422    CallbackSpec {
423        c_field: "visit_definition_term",
424        cs_method: "VisitDefinitionTerm",
425        doc: "Called for definition term elements.",
426        extra: &[ExtraParam {
427            cs_name: "text",
428            cs_type: "string",
429            pinvoke_types: &["IntPtr"],
430            decode: "Marshal.PtrToStringAnsi(rawText0)!",
431        }],
432        has_is_header: false,
433    },
434    CallbackSpec {
435        c_field: "visit_definition_description",
436        cs_method: "VisitDefinitionDescription",
437        doc: "Called for definition description elements.",
438        extra: &[ExtraParam {
439            cs_name: "text",
440            cs_type: "string",
441            pinvoke_types: &["IntPtr"],
442            decode: "Marshal.PtrToStringAnsi(rawText0)!",
443        }],
444        has_is_header: false,
445    },
446    CallbackSpec {
447        c_field: "visit_definition_list_end",
448        cs_method: "VisitDefinitionListEnd",
449        doc: "Called after a definition list.",
450        extra: &[ExtraParam {
451            cs_name: "output",
452            cs_type: "string",
453            pinvoke_types: &["IntPtr"],
454            decode: "Marshal.PtrToStringAnsi(rawOutput0)!",
455        }],
456        has_is_header: false,
457    },
458    CallbackSpec {
459        c_field: "visit_form",
460        cs_method: "VisitForm",
461        doc: "Called for form elements. action and method may be null.",
462        extra: &[
463            ExtraParam {
464                cs_name: "action",
465                cs_type: "string?",
466                pinvoke_types: &["IntPtr"],
467                decode: "rawAction0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawAction0)",
468            },
469            ExtraParam {
470                cs_name: "method",
471                cs_type: "string?",
472                pinvoke_types: &["IntPtr"],
473                decode: "rawMethod0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawMethod0)",
474            },
475        ],
476        has_is_header: false,
477    },
478    CallbackSpec {
479        c_field: "visit_input",
480        cs_method: "VisitInput",
481        doc: "Called for input elements. name and value may be null.",
482        extra: &[
483            ExtraParam {
484                cs_name: "inputType",
485                cs_type: "string",
486                pinvoke_types: &["IntPtr"],
487                decode: "Marshal.PtrToStringAnsi(rawInputType0)!",
488            },
489            ExtraParam {
490                cs_name: "name",
491                cs_type: "string?",
492                pinvoke_types: &["IntPtr"],
493                decode: "rawName0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawName0)",
494            },
495            ExtraParam {
496                cs_name: "value",
497                cs_type: "string?",
498                pinvoke_types: &["IntPtr"],
499                decode: "rawValue0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawValue0)",
500            },
501        ],
502        has_is_header: false,
503    },
504    CallbackSpec {
505        c_field: "visit_button",
506        cs_method: "VisitButton",
507        doc: "Called for button elements.",
508        extra: &[ExtraParam {
509            cs_name: "text",
510            cs_type: "string",
511            pinvoke_types: &["IntPtr"],
512            decode: "Marshal.PtrToStringAnsi(rawText0)!",
513        }],
514        has_is_header: false,
515    },
516    CallbackSpec {
517        c_field: "visit_audio",
518        cs_method: "VisitAudio",
519        doc: "Called for audio elements. src may be null.",
520        extra: &[ExtraParam {
521            cs_name: "src",
522            cs_type: "string?",
523            pinvoke_types: &["IntPtr"],
524            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0)",
525        }],
526        has_is_header: false,
527    },
528    CallbackSpec {
529        c_field: "visit_video",
530        cs_method: "VisitVideo",
531        doc: "Called for video elements. src may be null.",
532        extra: &[ExtraParam {
533            cs_name: "src",
534            cs_type: "string?",
535            pinvoke_types: &["IntPtr"],
536            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0)",
537        }],
538        has_is_header: false,
539    },
540    CallbackSpec {
541        c_field: "visit_iframe",
542        cs_method: "VisitIframe",
543        doc: "Called for iframe elements. src may be null.",
544        extra: &[ExtraParam {
545            cs_name: "src",
546            cs_type: "string?",
547            pinvoke_types: &["IntPtr"],
548            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(rawSrc0)",
549        }],
550        has_is_header: false,
551    },
552    CallbackSpec {
553        c_field: "visit_details",
554        cs_method: "VisitDetails",
555        doc: "Called for details elements.",
556        extra: &[ExtraParam {
557            cs_name: "open",
558            cs_type: "bool",
559            pinvoke_types: &["int"],
560            decode: "rawOpen0 != 0",
561        }],
562        has_is_header: false,
563    },
564    CallbackSpec {
565        c_field: "visit_summary",
566        cs_method: "VisitSummary",
567        doc: "Called for summary elements.",
568        extra: &[ExtraParam {
569            cs_name: "text",
570            cs_type: "string",
571            pinvoke_types: &["IntPtr"],
572            decode: "Marshal.PtrToStringAnsi(rawText0)!",
573        }],
574        has_is_header: false,
575    },
576    CallbackSpec {
577        c_field: "visit_figure_start",
578        cs_method: "VisitFigureStart",
579        doc: "Called before a figure element.",
580        extra: &[],
581        has_is_header: false,
582    },
583    CallbackSpec {
584        c_field: "visit_figcaption",
585        cs_method: "VisitFigcaption",
586        doc: "Called for figcaption elements.",
587        extra: &[ExtraParam {
588            cs_name: "text",
589            cs_type: "string",
590            pinvoke_types: &["IntPtr"],
591            decode: "Marshal.PtrToStringAnsi(rawText0)!",
592        }],
593        has_is_header: false,
594    },
595    CallbackSpec {
596        c_field: "visit_figure_end",
597        cs_method: "VisitFigureEnd",
598        doc: "Called after a figure element.",
599        extra: &[ExtraParam {
600            cs_name: "output",
601            cs_type: "string",
602            pinvoke_types: &["IntPtr"],
603            decode: "Marshal.PtrToStringAnsi(rawOutput0)!",
604        }],
605        has_is_header: false,
606    },
607];
608
609// ---------------------------------------------------------------------------
610// Public API
611// ---------------------------------------------------------------------------
612
613/// Returns `(filename, content)` pairs for all visitor-related C# files.
614pub fn gen_visitor_files(namespace: &str) -> Vec<(String, String)> {
615    vec![
616        ("NodeContext.cs".to_string(), gen_node_context(namespace)),
617        ("VisitResult.cs".to_string(), gen_visit_result(namespace)),
618        ("IVisitor.cs".to_string(), gen_ivisitor(namespace)),
619        ("VisitorCallbacks.cs".to_string(), gen_visitor_callbacks(namespace)),
620    ]
621}
622
623/// Generate the P/Invoke declarations needed in NativeMethods.cs for visitor FFI.
624pub fn gen_native_methods_visitor(namespace: &str, lib_name: &str, prefix: &str) -> String {
625    let mut out = String::with_capacity(512);
626    writeln!(out).ok();
627    writeln!(out, "    // Visitor FFI").ok();
628    writeln!(
629        out,
630        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_visitor_create\")]"
631    )
632    .ok();
633    writeln!(
634        out,
635        "    internal static extern IntPtr VisitorCreate(IntPtr callbacks);"
636    )
637    .ok();
638    writeln!(out).ok();
639    writeln!(
640        out,
641        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_visitor_free\")]"
642    )
643    .ok();
644    writeln!(out, "    internal static extern void VisitorFree(IntPtr visitor);").ok();
645    writeln!(out).ok();
646    writeln!(
647        out,
648        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{prefix}_convert_with_visitor\")]"
649    )
650    .ok();
651    writeln!(
652        out,
653        "    internal static extern IntPtr ConvertWithVisitor([MarshalAs(UnmanagedType.LPStr)] string html, IntPtr options, IntPtr visitor);"
654    )
655    .ok();
656    let _ = namespace;
657    let _ = lib_name;
658    out
659}
660
661/// Generate the `ConvertWithVisitor` method to inject into the wrapper class.
662pub fn gen_convert_with_visitor_method(exception_name: &str, prefix: &str) -> String {
663    let mut out = String::with_capacity(2048);
664    writeln!(out, "    /// <summary>").ok();
665    writeln!(
666        out,
667        "    /// Convert HTML to Markdown, invoking visitor callbacks during processing."
668    )
669    .ok();
670    writeln!(out, "    /// </summary>").ok();
671    writeln!(
672        out,
673        "    public static ConversionResult? ConvertWithVisitor(string html, ConversionOptions? options, IVisitor visitor)"
674    )
675    .ok();
676    writeln!(out, "    {{").ok();
677    writeln!(out, "        ArgumentNullException.ThrowIfNull(html);").ok();
678    writeln!(out, "        ArgumentNullException.ThrowIfNull(visitor);").ok();
679    writeln!(out).ok();
680    writeln!(out, "        using var callbacks = new VisitorCallbacks(visitor);").ok();
681    writeln!(out).ok();
682    writeln!(out, "        var optionsHandle = IntPtr.Zero;").ok();
683    writeln!(out, "        if (options != null)").ok();
684    writeln!(out, "        {{").ok();
685    writeln!(
686        out,
687        "            var optionsJson = JsonSerializer.Serialize(options, JsonOptions);"
688    )
689    .ok();
690    writeln!(
691        out,
692        "            optionsHandle = NativeMethods.ConversionOptionsFromJson(optionsJson);"
693    )
694    .ok();
695    writeln!(out, "        }}").ok();
696    writeln!(out).ok();
697    writeln!(
698        out,
699        "        var visitorHandle = NativeMethods.VisitorCreate(callbacks.NativePtr);"
700    )
701    .ok();
702    writeln!(out, "        if (visitorHandle == IntPtr.Zero)").ok();
703    writeln!(out, "        {{").ok();
704    writeln!(
705        out,
706        "            if (optionsHandle != IntPtr.Zero) NativeMethods.ConversionOptionsFree(optionsHandle);"
707    )
708    .ok();
709    writeln!(out, "            throw GetLastError();").ok();
710    writeln!(out, "        }}").ok();
711    writeln!(out).ok();
712    writeln!(out, "        try").ok();
713    writeln!(out, "        {{").ok();
714    writeln!(
715        out,
716        "            var resultPtr = NativeMethods.ConvertWithVisitor(html, optionsHandle, visitorHandle);"
717    )
718    .ok();
719    writeln!(
720        out,
721        "            if (optionsHandle != IntPtr.Zero) NativeMethods.ConversionOptionsFree(optionsHandle);"
722    )
723    .ok();
724    writeln!(out, "            if (resultPtr == IntPtr.Zero)").ok();
725    writeln!(out, "            {{").ok();
726    writeln!(out, "                var err = GetLastError();").ok();
727    writeln!(out, "                if (err.Code != 0) throw err;").ok();
728    writeln!(out, "                return null;").ok();
729    writeln!(out, "            }}").ok();
730    writeln!(out, "            var json = Marshal.PtrToStringAnsi(resultPtr);").ok();
731    writeln!(out, "            NativeMethods.FreeString(resultPtr);").ok();
732    writeln!(
733        out,
734        "            return JsonSerializer.Deserialize<ConversionResult>(json!, JsonOptions);"
735    )
736    .ok();
737    writeln!(out, "        }}").ok();
738    writeln!(out, "        finally").ok();
739    writeln!(out, "        {{").ok();
740    writeln!(out, "            NativeMethods.VisitorFree(visitorHandle);").ok();
741    writeln!(out, "        }}").ok();
742    writeln!(out, "    }}").ok();
743    let _ = exception_name;
744    let _ = prefix;
745    out
746}
747
748// ---------------------------------------------------------------------------
749// Individual file generators
750// ---------------------------------------------------------------------------
751
752fn gen_node_context(namespace: &str) -> String {
753    let mut out = String::with_capacity(1024);
754    writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.").ok();
755    writeln!(out, "using System;").ok();
756    writeln!(out).ok();
757    writeln!(out, "namespace {namespace};").ok();
758    writeln!(out).ok();
759    writeln!(out, "/// <summary>Context passed to every visitor callback.</summary>").ok();
760    writeln!(out, "public record NodeContext(").ok();
761    writeln!(out, "    /// <summary>Coarse-grained node type tag.</summary>").ok();
762    writeln!(out, "    int NodeType,").ok();
763    writeln!(out, "    /// <summary>HTML element tag name (e.g. \"div\").</summary>").ok();
764    writeln!(out, "    string TagName,").ok();
765    writeln!(out, "    /// <summary>DOM depth (0 = root).</summary>").ok();
766    writeln!(out, "    ulong Depth,").ok();
767    writeln!(out, "    /// <summary>0-based sibling index.</summary>").ok();
768    writeln!(out, "    ulong IndexInParent,").ok();
769    writeln!(
770        out,
771        "    /// <summary>Parent element tag name, or null at the root.</summary>"
772    )
773    .ok();
774    writeln!(out, "    string? ParentTag,").ok();
775    writeln!(
776        out,
777        "    /// <summary>True when this element is treated as inline.</summary>"
778    )
779    .ok();
780    writeln!(out, "    bool IsInline").ok();
781    writeln!(out, ");").ok();
782    out
783}
784
785fn gen_visit_result(namespace: &str) -> String {
786    let mut out = String::with_capacity(2048);
787    writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.").ok();
788    writeln!(out, "using System;").ok();
789    writeln!(out).ok();
790    writeln!(out, "namespace {namespace};").ok();
791    writeln!(out).ok();
792    writeln!(
793        out,
794        "/// <summary>Controls how the visitor affects the conversion pipeline.</summary>"
795    )
796    .ok();
797    writeln!(out, "public abstract record VisitResult").ok();
798    writeln!(out, "{{").ok();
799    writeln!(out, "    private VisitResult() {{}}").ok();
800    writeln!(out).ok();
801    writeln!(out, "    /// <summary>Proceed with default conversion.</summary>").ok();
802    writeln!(out, "    public sealed record Continue : VisitResult;").ok();
803    writeln!(out).ok();
804    writeln!(
805        out,
806        "    /// <summary>Omit this element from output entirely.</summary>"
807    )
808    .ok();
809    writeln!(out, "    public sealed record Skip : VisitResult;").ok();
810    writeln!(out).ok();
811    writeln!(out, "    /// <summary>Keep original HTML verbatim.</summary>").ok();
812    writeln!(out, "    public sealed record PreserveHtml : VisitResult;").ok();
813    writeln!(out).ok();
814    writeln!(out, "    /// <summary>Replace with custom Markdown.</summary>").ok();
815    writeln!(out, "    public sealed record Custom(string Markdown) : VisitResult;").ok();
816    writeln!(out).ok();
817    writeln!(
818        out,
819        "    /// <summary>Abort conversion with an error message.</summary>"
820    )
821    .ok();
822    writeln!(out, "    public sealed record Error(string Message) : VisitResult;").ok();
823    writeln!(out, "}}").ok();
824    out
825}
826
827fn gen_ivisitor(namespace: &str) -> String {
828    let mut out = String::with_capacity(4096);
829    writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.").ok();
830    writeln!(out, "using System;").ok();
831    writeln!(out).ok();
832    writeln!(out, "namespace {namespace};").ok();
833    writeln!(out).ok();
834    writeln!(
835        out,
836        "/// <summary>Visitor interface for the HTML-to-Markdown conversion pipeline.</summary>"
837    )
838    .ok();
839    writeln!(out, "public interface IVisitor").ok();
840    writeln!(out, "{{").ok();
841    for spec in CALLBACKS {
842        let params = iface_param_str(spec);
843        writeln!(out, "    /// <summary>{}</summary>", spec.doc).ok();
844        writeln!(
845            out,
846            "    VisitResult {}({}) => VisitResult.Continue();",
847            spec.cs_method, params
848        )
849        .ok();
850    }
851    writeln!(out, "}}").ok();
852    out
853}
854
855/// Generate `VisitorCallbacks.cs` which holds `GCHandle`s for all 40 delegates and
856/// writes them into a `Marshal.AllocHGlobal` block matching the C struct layout.
857fn gen_visitor_callbacks(namespace: &str) -> String {
858    let mut out = String::with_capacity(32_768);
859    writeln!(out, "// This file is auto-generated by alef. DO NOT EDIT.").ok();
860    writeln!(out, "using System;").ok();
861    writeln!(out, "using System.Runtime.InteropServices;").ok();
862    writeln!(out).ok();
863    writeln!(out, "namespace {namespace};").ok();
864    writeln!(out).ok();
865    writeln!(out, "/// <summary>").ok();
866    writeln!(out, "/// Allocates P/Invoke delegates for a IVisitor and assembles").ok();
867    writeln!(out, "/// the C HTMHtmVisitorCallbacks struct in unmanaged memory.").ok();
868    writeln!(out, "/// </summary>").ok();
869    writeln!(out, "internal sealed class VisitorCallbacks : IDisposable").ok();
870    writeln!(out, "{{").ok();
871    writeln!(out, "    private readonly IVisitor _visitor;").ok();
872    writeln!(
873        out,
874        "    private readonly IntPtr _nativeStruct; // HTMHtmVisitorCallbacks"
875    )
876    .ok();
877    writeln!(out, "    private bool _disposed;").ok();
878    writeln!(out).ok();
879
880    // Declare delegate types and delegate fields
881    for spec in CALLBACKS {
882        let delegate_type = delegate_type_name(spec.cs_method);
883        let pinvoke_params = delegate_pinvoke_params(spec);
884        writeln!(out, "    [UnmanagedFunctionPointer(CallingConvention.Cdecl)]").ok();
885        writeln!(
886            out,
887            "    private delegate int {}Delegate({});",
888            delegate_type, pinvoke_params
889        )
890        .ok();
891        writeln!(
892            out,
893            "    private readonly {}Delegate _del{};",
894            delegate_type, spec.cs_method
895        )
896        .ok();
897    }
898
899    writeln!(out).ok();
900    writeln!(out, "    internal IntPtr NativePtr => _nativeStruct;").ok();
901    writeln!(out).ok();
902
903    // Constructor
904    let num_slots = CALLBACKS.len() + 1; // user_data + callbacks
905    writeln!(out, "    internal VisitorCallbacks(IVisitor visitor)").ok();
906    writeln!(out, "    {{").ok();
907    writeln!(out, "        _visitor = visitor;").ok();
908    writeln!(out).ok();
909
910    // Create delegates
911    for spec in CALLBACKS {
912        let dt = delegate_type_name(spec.cs_method);
913        writeln!(
914            out,
915            "        _del{} = new {}Delegate(Handle{});",
916            spec.cs_method, dt, spec.cs_method
917        )
918        .ok();
919    }
920
921    // Allocate struct: user_data + 40 IntPtr slots = 41 * IntPtr.Size
922    writeln!(out).ok();
923    writeln!(
924        out,
925        "        // HTMHtmVisitorCallbacks = user_data + {n} callback function pointers",
926        n = CALLBACKS.len()
927    )
928    .ok();
929    writeln!(
930        out,
931        "        _nativeStruct = Marshal.AllocHGlobal(IntPtr.Size * {num_slots});"
932    )
933    .ok();
934    writeln!(
935        out,
936        "        // Slot 0: user_data = IntPtr.Zero (visitor captured via delegate closure)"
937    )
938    .ok();
939    writeln!(out, "        Marshal.WriteIntPtr(_nativeStruct, 0, IntPtr.Zero);").ok();
940
941    for (i, spec) in CALLBACKS.iter().enumerate() {
942        let offset = (i + 1) * 8; // assuming 8-byte pointers (64-bit)
943        writeln!(
944            out,
945            "        Marshal.WriteIntPtr(_nativeStruct, {offset}, Marshal.GetFunctionPointerForDelegate(_del{}));",
946            spec.cs_method
947        )
948        .ok();
949    }
950
951    writeln!(out, "    }}").ok();
952    writeln!(out).ok();
953
954    // Handle methods
955    for spec in CALLBACKS {
956        gen_handle_method(&mut out, spec);
957    }
958
959    // DecodeNodeContext helper
960    writeln!(out, "    private static NodeContext DecodeNodeContext(IntPtr ctxPtr)").ok();
961    writeln!(out, "    {{").ok();
962    writeln!(
963        out,
964        "        // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,"
965    )
966    .ok();
967    writeln!(
968        out,
969        "        //                    uintptr index_in_parent, char* parent_tag, int32 is_inline"
970    )
971    .ok();
972    writeln!(out, "        int nodeType = Marshal.ReadInt32(ctxPtr, 0);").ok();
973    writeln!(out, "        var tagNamePtr = Marshal.ReadIntPtr(ctxPtr, 8);").ok();
974    writeln!(
975        out,
976        "        string tagName = Marshal.PtrToStringAnsi(tagNamePtr) ?? string.Empty;"
977    )
978    .ok();
979    writeln!(out, "        ulong depth = (ulong)(long)Marshal.ReadInt64(ctxPtr, 16);").ok();
980    writeln!(
981        out,
982        "        ulong indexInParent = (ulong)(long)Marshal.ReadInt64(ctxPtr, 24);"
983    )
984    .ok();
985    writeln!(out, "        var parentTagPtr = Marshal.ReadIntPtr(ctxPtr, 32);").ok();
986    writeln!(
987        out,
988        "        string? parentTag = parentTagPtr == IntPtr.Zero ? null : Marshal.PtrToStringAnsi(parentTagPtr);"
989    )
990    .ok();
991    writeln!(out, "        int isInlineRaw = Marshal.ReadInt32(ctxPtr, 40);").ok();
992    writeln!(
993        out,
994        "        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);"
995    )
996    .ok();
997    writeln!(out, "    }}").ok();
998    writeln!(out).ok();
999
1000    // DecodeCells helper
1001    writeln!(
1002        out,
1003        "    private static string[] DecodeCells(IntPtr cellsPtr, long count)"
1004    )
1005    .ok();
1006    writeln!(out, "    {{").ok();
1007    writeln!(out, "        var result = new string[count];").ok();
1008    writeln!(out, "        for (long i = 0; i < count; i++)").ok();
1009    writeln!(out, "        {{").ok();
1010    writeln!(
1011        out,
1012        "            var ptr = Marshal.ReadIntPtr(cellsPtr, (int)(i * IntPtr.Size));"
1013    )
1014    .ok();
1015    writeln!(
1016        out,
1017        "            result[i] = Marshal.PtrToStringAnsi(ptr) ?? string.Empty;"
1018    )
1019    .ok();
1020    writeln!(out, "        }}").ok();
1021    writeln!(out, "        return result;").ok();
1022    writeln!(out, "    }}").ok();
1023    writeln!(out).ok();
1024
1025    // EncodeVisitResult helper
1026    writeln!(
1027        out,
1028        "    private static int EncodeVisitResult(VisitResult result, IntPtr outCustom, IntPtr outLen)"
1029    )
1030    .ok();
1031    writeln!(out, "    {{").ok();
1032    writeln!(out, "        return result switch").ok();
1033    writeln!(out, "        {{").ok();
1034    writeln!(out, "            VisitResult.Continue => 0,").ok();
1035    writeln!(out, "            VisitResult.Skip => 1,").ok();
1036    writeln!(out, "            VisitResult.PreserveHtml => 2,").ok();
1037    writeln!(
1038        out,
1039        "            VisitResult.Custom c => EncodeString(c.Markdown, outCustom, outLen, 3),"
1040    )
1041    .ok();
1042    writeln!(
1043        out,
1044        "            VisitResult.Error e => EncodeString(e.Message, outCustom, outLen, 4),"
1045    )
1046    .ok();
1047    writeln!(out, "            _ => 0").ok();
1048    writeln!(out, "        }};").ok();
1049    writeln!(out, "    }}").ok();
1050    writeln!(out).ok();
1051
1052    writeln!(
1053        out,
1054        "    private static int EncodeString(string text, IntPtr outCustom, IntPtr outLen, int code)"
1055    )
1056    .ok();
1057    writeln!(out, "    {{").ok();
1058    writeln!(out, "        var bytes = System.Text.Encoding.UTF8.GetBytes(text);").ok();
1059    writeln!(out, "        var buf = Marshal.AllocHGlobal(bytes.Length + 1);").ok();
1060    writeln!(out, "        Marshal.Copy(bytes, 0, buf, bytes.Length);").ok();
1061    writeln!(out, "        Marshal.WriteByte(buf, bytes.Length, 0);").ok();
1062    writeln!(out, "        Marshal.WriteIntPtr(outCustom, buf);").ok();
1063    writeln!(out, "        Marshal.WriteInt64(outLen, (long)bytes.Length);").ok();
1064    writeln!(out, "        return code;").ok();
1065    writeln!(out, "    }}").ok();
1066    writeln!(out).ok();
1067
1068    // Dispose
1069    writeln!(out, "    public void Dispose()").ok();
1070    writeln!(out, "    {{").ok();
1071    writeln!(out, "        if (_disposed) return;").ok();
1072    writeln!(out, "        _disposed = true;").ok();
1073    writeln!(out, "        Marshal.FreeHGlobal(_nativeStruct);").ok();
1074    writeln!(out, "    }}").ok();
1075    writeln!(out, "}}").ok();
1076    out
1077}
1078
1079// ---------------------------------------------------------------------------
1080// Helpers
1081// ---------------------------------------------------------------------------
1082
1083fn delegate_type_name(cs_method: &str) -> String {
1084    cs_method.to_string()
1085}
1086
1087fn iface_param_str(spec: &CallbackSpec) -> String {
1088    let mut params = vec!["NodeContext context".to_string()];
1089    for ep in spec.extra {
1090        params.push(format!("{} {}", ep.cs_type, ep.cs_name));
1091    }
1092    if spec.has_is_header {
1093        params.push("bool isHeader".to_string());
1094    }
1095    params.join(", ")
1096}
1097
1098/// Build the P/Invoke delegate parameter list (all raw C types).
1099fn delegate_pinvoke_params(spec: &CallbackSpec) -> String {
1100    let mut params = vec!["IntPtr ctx".to_string(), "IntPtr userData".to_string()];
1101    for ep in spec.extra {
1102        for (idx, ptype) in ep.pinvoke_types.iter().enumerate() {
1103            params.push(format!("{ptype} {}", raw_var_name(ep.cs_name, idx)));
1104        }
1105    }
1106    if spec.has_is_header {
1107        params.push("int isHeader".to_string());
1108    }
1109    params.push("IntPtr outCustom".to_string());
1110    params.push("IntPtr outLen".to_string());
1111    params.join(", ")
1112}
1113
1114/// Generate one `Handle*` method inside `VisitorCallbacks`.
1115fn gen_handle_method(out: &mut String, spec: &CallbackSpec) {
1116    let params = delegate_pinvoke_params(spec);
1117    writeln!(out, "    private int Handle{}({})", spec.cs_method, params).ok();
1118    writeln!(out, "    {{").ok();
1119    writeln!(out, "        try").ok();
1120    writeln!(out, "        {{").ok();
1121    writeln!(out, "            var context = DecodeNodeContext(ctx);").ok();
1122
1123    // Decode extra params
1124    for ep in spec.extra {
1125        let mut decode = ep.decode.to_string();
1126        for (idx, _) in ep.pinvoke_types.iter().enumerate() {
1127            let placeholder = format!("raw{}{}", capitalize(ep.cs_name), idx);
1128            let var = raw_var_name(ep.cs_name, idx);
1129            decode = decode.replace(&placeholder, &var);
1130        }
1131        writeln!(out, "            var {} = {};", ep.cs_name, decode).ok();
1132    }
1133    if spec.has_is_header {
1134        writeln!(out, "            var goIsHeader = isHeader != 0;").ok();
1135    }
1136
1137    let mut call_args = vec!["context".to_string()];
1138    for ep in spec.extra {
1139        call_args.push(ep.cs_name.to_string());
1140    }
1141    if spec.has_is_header {
1142        call_args.push("goIsHeader".to_string());
1143    }
1144
1145    writeln!(
1146        out,
1147        "            var result = _visitor.{}({});",
1148        spec.cs_method,
1149        call_args.join(", ")
1150    )
1151    .ok();
1152    writeln!(out, "            return EncodeVisitResult(result, outCustom, outLen);").ok();
1153    writeln!(out, "        }}").ok();
1154    writeln!(out, "        catch").ok();
1155    writeln!(out, "        {{").ok();
1156    writeln!(out, "            return 0;").ok();
1157    writeln!(out, "        }}").ok();
1158    writeln!(out, "    }}").ok();
1159    writeln!(out).ok();
1160}
1161
1162fn raw_var_name(cs_name: &str, idx: usize) -> String {
1163    format!("raw{}{idx}", capitalize(cs_name))
1164}
1165
1166fn capitalize(s: &str) -> String {
1167    let mut chars = s.chars();
1168    match chars.next() {
1169        None => String::new(),
1170        Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
1171    }
1172}