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