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;
18use std::fmt::Write;
19
20// ---------------------------------------------------------------------------
21// Callback specification table
22// ---------------------------------------------------------------------------
23
24pub struct CallbackSpec {
25    /// Field name in `HTMHtmVisitorCallbacks`.
26    pub c_field: &'static str,
27    /// C# interface method name (PascalCase).
28    pub cs_method: &'static str,
29    /// XML doc summary.
30    pub doc: &'static str,
31    /// Extra parameters beyond `NodeContext` in the C# interface.
32    pub extra: &'static [ExtraParam],
33    /// If true, add `bool isHeader` (only visit_table_row).
34    pub has_is_header: bool,
35}
36
37pub struct ExtraParam {
38    /// C# parameter name in the interface.
39    pub cs_name: &'static str,
40    /// C# type in the interface method signature.
41    pub cs_type: &'static str,
42    /// P/Invoke types for each raw C parameter (one or more per Java param).
43    pub pinvoke_types: &'static [&'static str],
44    /// C# expression to decode the raw P/Invoke args (vars named `raw<CsName>N`).
45    pub decode: &'static str,
46}
47
48pub const CALLBACKS: &[CallbackSpec] = &[
49    CallbackSpec {
50        c_field: "visit_text",
51        cs_method: "VisitText",
52        doc: "Called for text nodes.",
53        extra: &[ExtraParam {
54            cs_name: "text",
55            cs_type: "string",
56            pinvoke_types: &["IntPtr"],
57            decode: "Marshal.PtrToStringUTF8(rawText0)!",
58        }],
59        has_is_header: false,
60    },
61    CallbackSpec {
62        c_field: "visit_element_start",
63        cs_method: "VisitElementStart",
64        doc: "Called before entering any element.",
65        extra: &[],
66        has_is_header: false,
67    },
68    CallbackSpec {
69        c_field: "visit_element_end",
70        cs_method: "VisitElementEnd",
71        doc: "Called after exiting any element; receives the default markdown output.",
72        extra: &[ExtraParam {
73            cs_name: "output",
74            cs_type: "string",
75            pinvoke_types: &["IntPtr"],
76            decode: "Marshal.PtrToStringUTF8(rawOutput0)!",
77        }],
78        has_is_header: false,
79    },
80    CallbackSpec {
81        c_field: "visit_link",
82        cs_method: "VisitLink",
83        doc: "Called for anchor links. title is null when the attribute is absent.",
84        extra: &[
85            ExtraParam {
86                cs_name: "href",
87                cs_type: "string",
88                pinvoke_types: &["IntPtr"],
89                decode: "Marshal.PtrToStringUTF8(rawHref0)!",
90            },
91            ExtraParam {
92                cs_name: "text",
93                cs_type: "string",
94                pinvoke_types: &["IntPtr"],
95                decode: "Marshal.PtrToStringUTF8(rawText0)!",
96            },
97            ExtraParam {
98                cs_name: "title",
99                cs_type: "string?",
100                pinvoke_types: &["IntPtr"],
101                decode: "rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawTitle0)",
102            },
103        ],
104        has_is_header: false,
105    },
106    CallbackSpec {
107        c_field: "visit_image",
108        cs_method: "VisitImage",
109        doc: "Called for images. title is null when absent.",
110        extra: &[
111            ExtraParam {
112                cs_name: "src",
113                cs_type: "string",
114                pinvoke_types: &["IntPtr"],
115                decode: "Marshal.PtrToStringUTF8(rawSrc0)!",
116            },
117            ExtraParam {
118                cs_name: "alt",
119                cs_type: "string",
120                pinvoke_types: &["IntPtr"],
121                decode: "Marshal.PtrToStringUTF8(rawAlt0)!",
122            },
123            ExtraParam {
124                cs_name: "title",
125                cs_type: "string?",
126                pinvoke_types: &["IntPtr"],
127                decode: "rawTitle0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawTitle0)",
128            },
129        ],
130        has_is_header: false,
131    },
132    CallbackSpec {
133        c_field: "visit_heading",
134        cs_method: "VisitHeading",
135        doc: "Called for heading elements h1-h6. id is null when absent.",
136        extra: &[
137            ExtraParam {
138                cs_name: "level",
139                cs_type: "uint",
140                pinvoke_types: &["uint"],
141                decode: "rawLevel0",
142            },
143            ExtraParam {
144                cs_name: "text",
145                cs_type: "string",
146                pinvoke_types: &["IntPtr"],
147                decode: "Marshal.PtrToStringUTF8(rawText0)!",
148            },
149            ExtraParam {
150                cs_name: "id",
151                cs_type: "string?",
152                pinvoke_types: &["IntPtr"],
153                decode: "rawId0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawId0)",
154            },
155        ],
156        has_is_header: false,
157    },
158    CallbackSpec {
159        c_field: "visit_code_block",
160        cs_method: "VisitCodeBlock",
161        doc: "Called for code blocks. lang is null when absent.",
162        extra: &[
163            ExtraParam {
164                cs_name: "lang",
165                cs_type: "string?",
166                pinvoke_types: &["IntPtr"],
167                decode: "rawLang0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawLang0)",
168            },
169            ExtraParam {
170                cs_name: "code",
171                cs_type: "string",
172                pinvoke_types: &["IntPtr"],
173                decode: "Marshal.PtrToStringUTF8(rawCode0)!",
174            },
175        ],
176        has_is_header: false,
177    },
178    CallbackSpec {
179        c_field: "visit_code_inline",
180        cs_method: "VisitCodeInline",
181        doc: "Called for inline code elements.",
182        extra: &[ExtraParam {
183            cs_name: "code",
184            cs_type: "string",
185            pinvoke_types: &["IntPtr"],
186            decode: "Marshal.PtrToStringUTF8(rawCode0)!",
187        }],
188        has_is_header: false,
189    },
190    CallbackSpec {
191        c_field: "visit_list_item",
192        cs_method: "VisitListItem",
193        doc: "Called for list items.",
194        extra: &[
195            ExtraParam {
196                cs_name: "ordered",
197                cs_type: "bool",
198                pinvoke_types: &["int"],
199                decode: "rawOrdered0 != 0",
200            },
201            ExtraParam {
202                cs_name: "marker",
203                cs_type: "string",
204                pinvoke_types: &["IntPtr"],
205                decode: "Marshal.PtrToStringUTF8(rawMarker0)!",
206            },
207            ExtraParam {
208                cs_name: "text",
209                cs_type: "string",
210                pinvoke_types: &["IntPtr"],
211                decode: "Marshal.PtrToStringUTF8(rawText0)!",
212            },
213        ],
214        has_is_header: false,
215    },
216    CallbackSpec {
217        c_field: "visit_list_start",
218        cs_method: "VisitListStart",
219        doc: "Called before processing a list.",
220        extra: &[ExtraParam {
221            cs_name: "ordered",
222            cs_type: "bool",
223            pinvoke_types: &["int"],
224            decode: "rawOrdered0 != 0",
225        }],
226        has_is_header: false,
227    },
228    CallbackSpec {
229        c_field: "visit_list_end",
230        cs_method: "VisitListEnd",
231        doc: "Called after processing a list.",
232        extra: &[
233            ExtraParam {
234                cs_name: "ordered",
235                cs_type: "bool",
236                pinvoke_types: &["int"],
237                decode: "rawOrdered0 != 0",
238            },
239            ExtraParam {
240                cs_name: "output",
241                cs_type: "string",
242                pinvoke_types: &["IntPtr"],
243                decode: "Marshal.PtrToStringUTF8(rawOutput0)!",
244            },
245        ],
246        has_is_header: false,
247    },
248    CallbackSpec {
249        c_field: "visit_table_start",
250        cs_method: "VisitTableStart",
251        doc: "Called before processing a table.",
252        extra: &[],
253        has_is_header: false,
254    },
255    CallbackSpec {
256        c_field: "visit_table_row",
257        cs_method: "VisitTableRow",
258        doc: "Called for table rows. cells contains the cell text values.",
259        extra: &[ExtraParam {
260            cs_name: "cells",
261            cs_type: "string[]",
262            pinvoke_types: &["IntPtr", "UIntPtr"],
263            decode: "DecodeCells(rawCells0, (long)(ulong)rawCells1)",
264        }],
265        has_is_header: true,
266    },
267    CallbackSpec {
268        c_field: "visit_table_end",
269        cs_method: "VisitTableEnd",
270        doc: "Called after processing a table.",
271        extra: &[ExtraParam {
272            cs_name: "output",
273            cs_type: "string",
274            pinvoke_types: &["IntPtr"],
275            decode: "Marshal.PtrToStringUTF8(rawOutput0)!",
276        }],
277        has_is_header: false,
278    },
279    CallbackSpec {
280        c_field: "visit_blockquote",
281        cs_method: "VisitBlockquote",
282        doc: "Called for blockquote elements.",
283        extra: &[
284            ExtraParam {
285                cs_name: "content",
286                cs_type: "string",
287                pinvoke_types: &["IntPtr"],
288                decode: "Marshal.PtrToStringUTF8(rawContent0)!",
289            },
290            ExtraParam {
291                cs_name: "depth",
292                cs_type: "ulong",
293                pinvoke_types: &["UIntPtr"],
294                decode: "(ulong)rawDepth0",
295            },
296        ],
297        has_is_header: false,
298    },
299    CallbackSpec {
300        c_field: "visit_strong",
301        cs_method: "VisitStrong",
302        doc: "Called for strong/bold elements.",
303        extra: &[ExtraParam {
304            cs_name: "text",
305            cs_type: "string",
306            pinvoke_types: &["IntPtr"],
307            decode: "Marshal.PtrToStringUTF8(rawText0)!",
308        }],
309        has_is_header: false,
310    },
311    CallbackSpec {
312        c_field: "visit_emphasis",
313        cs_method: "VisitEmphasis",
314        doc: "Called for emphasis/italic elements.",
315        extra: &[ExtraParam {
316            cs_name: "text",
317            cs_type: "string",
318            pinvoke_types: &["IntPtr"],
319            decode: "Marshal.PtrToStringUTF8(rawText0)!",
320        }],
321        has_is_header: false,
322    },
323    CallbackSpec {
324        c_field: "visit_strikethrough",
325        cs_method: "VisitStrikethrough",
326        doc: "Called for strikethrough elements.",
327        extra: &[ExtraParam {
328            cs_name: "text",
329            cs_type: "string",
330            pinvoke_types: &["IntPtr"],
331            decode: "Marshal.PtrToStringUTF8(rawText0)!",
332        }],
333        has_is_header: false,
334    },
335    CallbackSpec {
336        c_field: "visit_underline",
337        cs_method: "VisitUnderline",
338        doc: "Called for underline elements.",
339        extra: &[ExtraParam {
340            cs_name: "text",
341            cs_type: "string",
342            pinvoke_types: &["IntPtr"],
343            decode: "Marshal.PtrToStringUTF8(rawText0)!",
344        }],
345        has_is_header: false,
346    },
347    CallbackSpec {
348        c_field: "visit_subscript",
349        cs_method: "VisitSubscript",
350        doc: "Called for subscript elements.",
351        extra: &[ExtraParam {
352            cs_name: "text",
353            cs_type: "string",
354            pinvoke_types: &["IntPtr"],
355            decode: "Marshal.PtrToStringUTF8(rawText0)!",
356        }],
357        has_is_header: false,
358    },
359    CallbackSpec {
360        c_field: "visit_superscript",
361        cs_method: "VisitSuperscript",
362        doc: "Called for superscript elements.",
363        extra: &[ExtraParam {
364            cs_name: "text",
365            cs_type: "string",
366            pinvoke_types: &["IntPtr"],
367            decode: "Marshal.PtrToStringUTF8(rawText0)!",
368        }],
369        has_is_header: false,
370    },
371    CallbackSpec {
372        c_field: "visit_mark",
373        cs_method: "VisitMark",
374        doc: "Called for mark/highlight elements.",
375        extra: &[ExtraParam {
376            cs_name: "text",
377            cs_type: "string",
378            pinvoke_types: &["IntPtr"],
379            decode: "Marshal.PtrToStringUTF8(rawText0)!",
380        }],
381        has_is_header: false,
382    },
383    CallbackSpec {
384        c_field: "visit_line_break",
385        cs_method: "VisitLineBreak",
386        doc: "Called for line break elements.",
387        extra: &[],
388        has_is_header: false,
389    },
390    CallbackSpec {
391        c_field: "visit_horizontal_rule",
392        cs_method: "VisitHorizontalRule",
393        doc: "Called for horizontal rule elements.",
394        extra: &[],
395        has_is_header: false,
396    },
397    CallbackSpec {
398        c_field: "visit_custom_element",
399        cs_method: "VisitCustomElement",
400        doc: "Called for custom or unknown elements.",
401        extra: &[
402            ExtraParam {
403                cs_name: "tagName",
404                cs_type: "string",
405                pinvoke_types: &["IntPtr"],
406                decode: "Marshal.PtrToStringUTF8(rawTagName0)!",
407            },
408            ExtraParam {
409                cs_name: "html",
410                cs_type: "string",
411                pinvoke_types: &["IntPtr"],
412                decode: "Marshal.PtrToStringUTF8(rawHtml0)!",
413            },
414        ],
415        has_is_header: false,
416    },
417    CallbackSpec {
418        c_field: "visit_definition_list_start",
419        cs_method: "VisitDefinitionListStart",
420        doc: "Called before a definition list.",
421        extra: &[],
422        has_is_header: false,
423    },
424    CallbackSpec {
425        c_field: "visit_definition_term",
426        cs_method: "VisitDefinitionTerm",
427        doc: "Called for definition term elements.",
428        extra: &[ExtraParam {
429            cs_name: "text",
430            cs_type: "string",
431            pinvoke_types: &["IntPtr"],
432            decode: "Marshal.PtrToStringUTF8(rawText0)!",
433        }],
434        has_is_header: false,
435    },
436    CallbackSpec {
437        c_field: "visit_definition_description",
438        cs_method: "VisitDefinitionDescription",
439        doc: "Called for definition description elements.",
440        extra: &[ExtraParam {
441            cs_name: "text",
442            cs_type: "string",
443            pinvoke_types: &["IntPtr"],
444            decode: "Marshal.PtrToStringUTF8(rawText0)!",
445        }],
446        has_is_header: false,
447    },
448    CallbackSpec {
449        c_field: "visit_definition_list_end",
450        cs_method: "VisitDefinitionListEnd",
451        doc: "Called after a definition list.",
452        extra: &[ExtraParam {
453            cs_name: "output",
454            cs_type: "string",
455            pinvoke_types: &["IntPtr"],
456            decode: "Marshal.PtrToStringUTF8(rawOutput0)!",
457        }],
458        has_is_header: false,
459    },
460    CallbackSpec {
461        c_field: "visit_form",
462        cs_method: "VisitForm",
463        doc: "Called for form elements. action and method may be null.",
464        extra: &[
465            ExtraParam {
466                cs_name: "action",
467                cs_type: "string?",
468                pinvoke_types: &["IntPtr"],
469                decode: "rawAction0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawAction0)",
470            },
471            ExtraParam {
472                cs_name: "method",
473                cs_type: "string?",
474                pinvoke_types: &["IntPtr"],
475                decode: "rawMethod0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawMethod0)",
476            },
477        ],
478        has_is_header: false,
479    },
480    CallbackSpec {
481        c_field: "visit_input",
482        cs_method: "VisitInput",
483        doc: "Called for input elements. name and value may be null.",
484        extra: &[
485            ExtraParam {
486                cs_name: "inputType",
487                cs_type: "string",
488                pinvoke_types: &["IntPtr"],
489                decode: "Marshal.PtrToStringUTF8(rawInputType0)!",
490            },
491            ExtraParam {
492                cs_name: "name",
493                cs_type: "string?",
494                pinvoke_types: &["IntPtr"],
495                decode: "rawName0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawName0)",
496            },
497            ExtraParam {
498                cs_name: "value",
499                cs_type: "string?",
500                pinvoke_types: &["IntPtr"],
501                decode: "rawValue0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawValue0)",
502            },
503        ],
504        has_is_header: false,
505    },
506    CallbackSpec {
507        c_field: "visit_button",
508        cs_method: "VisitButton",
509        doc: "Called for button elements.",
510        extra: &[ExtraParam {
511            cs_name: "text",
512            cs_type: "string",
513            pinvoke_types: &["IntPtr"],
514            decode: "Marshal.PtrToStringUTF8(rawText0)!",
515        }],
516        has_is_header: false,
517    },
518    CallbackSpec {
519        c_field: "visit_audio",
520        cs_method: "VisitAudio",
521        doc: "Called for audio elements. src may be null.",
522        extra: &[ExtraParam {
523            cs_name: "src",
524            cs_type: "string?",
525            pinvoke_types: &["IntPtr"],
526            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawSrc0)",
527        }],
528        has_is_header: false,
529    },
530    CallbackSpec {
531        c_field: "visit_video",
532        cs_method: "VisitVideo",
533        doc: "Called for video elements. src may be null.",
534        extra: &[ExtraParam {
535            cs_name: "src",
536            cs_type: "string?",
537            pinvoke_types: &["IntPtr"],
538            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawSrc0)",
539        }],
540        has_is_header: false,
541    },
542    CallbackSpec {
543        c_field: "visit_iframe",
544        cs_method: "VisitIframe",
545        doc: "Called for iframe elements. src may be null.",
546        extra: &[ExtraParam {
547            cs_name: "src",
548            cs_type: "string?",
549            pinvoke_types: &["IntPtr"],
550            decode: "rawSrc0 == IntPtr.Zero ? null : Marshal.PtrToStringUTF8(rawSrc0)",
551        }],
552        has_is_header: false,
553    },
554    CallbackSpec {
555        c_field: "visit_details",
556        cs_method: "VisitDetails",
557        doc: "Called for details elements.",
558        extra: &[ExtraParam {
559            cs_name: "open",
560            cs_type: "bool",
561            pinvoke_types: &["int"],
562            decode: "rawOpen0 != 0",
563        }],
564        has_is_header: false,
565    },
566    CallbackSpec {
567        c_field: "visit_summary",
568        cs_method: "VisitSummary",
569        doc: "Called for summary elements.",
570        extra: &[ExtraParam {
571            cs_name: "text",
572            cs_type: "string",
573            pinvoke_types: &["IntPtr"],
574            decode: "Marshal.PtrToStringUTF8(rawText0)!",
575        }],
576        has_is_header: false,
577    },
578    CallbackSpec {
579        c_field: "visit_figure_start",
580        cs_method: "VisitFigureStart",
581        doc: "Called before a figure element.",
582        extra: &[],
583        has_is_header: false,
584    },
585    CallbackSpec {
586        c_field: "visit_figcaption",
587        cs_method: "VisitFigcaption",
588        doc: "Called for figcaption elements.",
589        extra: &[ExtraParam {
590            cs_name: "text",
591            cs_type: "string",
592            pinvoke_types: &["IntPtr"],
593            decode: "Marshal.PtrToStringUTF8(rawText0)!",
594        }],
595        has_is_header: false,
596    },
597    CallbackSpec {
598        c_field: "visit_figure_end",
599        cs_method: "VisitFigureEnd",
600        doc: "Called after a figure element.",
601        extra: &[ExtraParam {
602            cs_name: "output",
603            cs_type: "string",
604            pinvoke_types: &["IntPtr"],
605            decode: "Marshal.PtrToStringUTF8(rawOutput0)!",
606        }],
607        has_is_header: false,
608    },
609];
610
611// ---------------------------------------------------------------------------
612// Public API
613// ---------------------------------------------------------------------------
614
615/// Returns `(filename, content)` pairs for all visitor-related C# files.
616///
617/// IVisitor.cs and VisitorCallbacks.cs are superseded by IVisitor and VisitorCallbacks
618/// in TraitBridges.cs which use the HtmlVisitorBridge approach. They are intentionally
619/// excluded here; stale committed copies are removed by delete_superseded_visitor_files.
620pub fn gen_visitor_files(namespace: &str) -> Vec<(String, String)> {
621    vec![
622        ("NodeContext.cs".to_string(), gen_node_context(namespace)),
623        ("VisitResult.cs".to_string(), gen_visit_result(namespace)),
624    ]
625}
626
627/// Generate the P/Invoke declarations needed in NativeMethods.cs for visitor FFI.
628///
629/// Parameters:
630/// - `namespace`: C# namespace (unused, kept for compatibility)
631/// - `lib_name`: Native library name (unused, kept for compatibility)
632/// - `prefix`: C FFI function name prefix (e.g., "htm")
633/// - `trait_name`: Name of the visitor trait (e.g., "HtmlVisitor") for bridge function names
634/// - `options_field`: Field name in options to set visitor on (e.g., "visitor")
635pub fn gen_native_methods_visitor(
636    namespace: &str,
637    lib_name: &str,
638    prefix: &str,
639    trait_name: &str,
640    options_field: &str,
641) -> String {
642    let mut out = String::with_capacity(512);
643    writeln!(out).ok();
644    writeln!(out, "    // Visitor FFI (HtmlVisitorBridge)").ok();
645
646    // Generate function names:
647    // htm_htm_html_visitor_bridge_new, htm_htm_html_visitor_bridge_free, htm_options_set_visitor
648    let trait_snake = trait_name.to_snake_case();
649    let bridge_snake = format!("{prefix}_{trait_snake}_bridge");
650    let fn_bridge_new = format!("{prefix}_{bridge_snake}_new");
651    let fn_bridge_free = format!("{prefix}_{bridge_snake}_free");
652    let fn_options_set = format!("{prefix}_options_set_{options_field}");
653
654    writeln!(
655        out,
656        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{fn_bridge_new}\")]"
657    )
658    .ok();
659    writeln!(
660        out,
661        "    internal static extern IntPtr HtmlVisitorBridgeNew(IntPtr vtable, IntPtr userData);"
662    )
663    .ok();
664    writeln!(out).ok();
665
666    writeln!(
667        out,
668        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{fn_bridge_free}\")]"
669    )
670    .ok();
671    writeln!(
672        out,
673        "    internal static extern void HtmlVisitorBridgeFree(IntPtr bridge);"
674    )
675    .ok();
676    writeln!(out).ok();
677
678    writeln!(
679        out,
680        "    [DllImport(LibName, CallingConvention = CallingConvention.Cdecl, EntryPoint = \"{fn_options_set}\")]"
681    )
682    .ok();
683    writeln!(
684        out,
685        "    internal static extern int ConversionOptionsSetVisitor(IntPtr options, IntPtr visitor);"
686    )
687    .ok();
688
689    let _ = namespace;
690    let _ = lib_name;
691    out
692}
693
694/// DEPRECATED: gen_convert_with_visitor_method is no longer used.
695/// The visitor logic is now integrated into the main Convert() method in gen_wrapper_function,
696/// which creates the HtmlVisitorBridge and uses htm_options_set_visitor instead.
697#[allow(dead_code)]
698pub fn gen_convert_with_visitor_method(exception_name: &str, prefix: &str) -> String {
699    let _ = exception_name;
700    let _ = prefix;
701    String::new()
702}
703
704// ---------------------------------------------------------------------------
705// Individual file generators
706// ---------------------------------------------------------------------------
707
708fn gen_node_context(namespace: &str) -> String {
709    let mut out = String::with_capacity(1024);
710    out.push_str(&hash::header(CommentStyle::DoubleSlash));
711    writeln!(out, "#nullable enable").ok();
712    writeln!(out).ok();
713    writeln!(out, "using System;").ok();
714    writeln!(out).ok();
715    writeln!(out, "namespace {namespace};").ok();
716    writeln!(out).ok();
717    writeln!(out, "/// <summary>Context passed to every visitor callback.</summary>").ok();
718    writeln!(out, "public record NodeContext(").ok();
719    writeln!(out, "    /// <summary>Coarse-grained node type tag.</summary>").ok();
720    writeln!(out, "    NodeType NodeType,").ok();
721    writeln!(out, "    /// <summary>HTML element tag name (e.g. \"div\").</summary>").ok();
722    writeln!(out, "    string TagName,").ok();
723    writeln!(out, "    /// <summary>DOM depth (0 = root).</summary>").ok();
724    writeln!(out, "    ulong Depth,").ok();
725    writeln!(out, "    /// <summary>0-based sibling index.</summary>").ok();
726    writeln!(out, "    ulong IndexInParent,").ok();
727    writeln!(
728        out,
729        "    /// <summary>Parent element tag name, or null at the root.</summary>"
730    )
731    .ok();
732    writeln!(out, "    string? ParentTag,").ok();
733    writeln!(
734        out,
735        "    /// <summary>True when this element is treated as inline.</summary>"
736    )
737    .ok();
738    writeln!(out, "    bool IsInline").ok();
739    writeln!(out, ");").ok();
740    out
741}
742
743fn gen_visit_result(namespace: &str) -> String {
744    let mut out = String::with_capacity(2048);
745    out.push_str(&hash::header(CommentStyle::DoubleSlash));
746    writeln!(out, "#nullable enable").ok();
747    writeln!(out).ok();
748    writeln!(out, "using System;").ok();
749    writeln!(out).ok();
750    writeln!(out, "namespace {namespace};").ok();
751    writeln!(out).ok();
752    writeln!(
753        out,
754        "/// <summary>Controls how the visitor affects the conversion pipeline.</summary>"
755    )
756    .ok();
757    writeln!(out, "public abstract record VisitResult").ok();
758    writeln!(out, "{{").ok();
759    writeln!(out, "    private VisitResult() {{}}").ok();
760    writeln!(out).ok();
761    writeln!(out, "    /// <summary>Proceed with default conversion.</summary>").ok();
762    writeln!(out, "    public sealed record Continue : VisitResult;").ok();
763    writeln!(out).ok();
764    writeln!(
765        out,
766        "    /// <summary>Omit this element from output entirely.</summary>"
767    )
768    .ok();
769    writeln!(out, "    public sealed record Skip : VisitResult;").ok();
770    writeln!(out).ok();
771    writeln!(out, "    /// <summary>Keep original HTML verbatim.</summary>").ok();
772    writeln!(out, "    public sealed record PreserveHtml : VisitResult;").ok();
773    writeln!(out).ok();
774    writeln!(out, "    /// <summary>Replace with custom Markdown.</summary>").ok();
775    writeln!(out, "    public sealed record Custom(string Markdown) : VisitResult;").ok();
776    writeln!(out).ok();
777    writeln!(
778        out,
779        "    /// <summary>Abort conversion with an error message.</summary>"
780    )
781    .ok();
782    writeln!(out, "    public sealed record Error(string Message) : VisitResult;").ok();
783    writeln!(out).ok();
784    writeln!(out, "    internal string ToFfiJson() => this switch {{").ok();
785    writeln!(out, "        VisitResult.Continue => \"\\\"Continue\\\"\",").ok();
786    writeln!(out, "        VisitResult.Skip => \"\\\"Skip\\\"\",").ok();
787    writeln!(out, "        VisitResult.PreserveHtml => \"\\\"PreserveHtml\\\"\",").ok();
788    writeln!(out, "        VisitResult.Custom c => \"{{\\\"Custom\\\":\" + System.Text.Json.JsonSerializer.Serialize(c.Markdown) + \"}}\",").ok();
789    writeln!(out, "        VisitResult.Error e => \"{{\\\"Error\\\":\" + System.Text.Json.JsonSerializer.Serialize(e.Message) + \"}}\",").ok();
790    writeln!(out, "        _ => \"\\\"Continue\\\"\"").ok();
791    writeln!(out, "    }};").ok();
792    writeln!(out, "}}").ok();
793    out
794}
795
796// gen_ivisitor and gen_visitor_callbacks were removed: IVisitor and VisitorCallbacks
797// are now handwritten in TraitBridges.cs (HtmlVisitorBridge pattern). Generating them
798// here produced dead code that conflicted with the handwritten implementations.