Skip to main content

alef_backend_java/
gen_visitor.rs

1/// Generate Java visitor support: interface, NodeContext record, VisitResult sealed interface,
2/// VisitorBridge (upcall stubs), and convertWithVisitor method.
3///
4/// # Panama FFM upcall strategy
5///
6/// Java cannot expose method references as raw C function pointers. The generated
7/// code uses Java 21+ Foreign Function & Memory API (Panama) upcall stubs:
8///
9/// - `NodeContext`: a `record` carrying the fields from `HTMHtmNodeContext`.
10/// - `VisitResult`: a `sealed interface` with `Continue`, `Skip`, `PreserveHtml`,
11///   `Custom`, and `Error` implementations.
12/// - `Visitor`: an `interface` with default no-op methods for all 40 callbacks.
13/// - `VisitorBridge`: a package-private class that allocates one `MemorySegment`
14///   upcall stub per callback inside a `Arena.ofConfined()` scope, then writes
15///   all stubs into a flat `MemorySegment` matching `HTMHtmVisitorCallbacks`.
16/// - `convertWithVisitor`: static method on the wrapper class that drives the full
17///   lifecycle: marshal options → create `VisitorBridge` → `htm_visitor_create` →
18///   `htm_convert_with_visitor` → deserialise JSON result → `htm_visitor_free`.
19use alef_core::hash::{self, CommentStyle};
20use std::fmt::Write;
21
22// ---------------------------------------------------------------------------
23// Callback specification table (mirrors crates/alef-backend-go/src/gen_visitor.rs)
24// ---------------------------------------------------------------------------
25
26pub struct CallbackSpec {
27    /// Field name in `HTMHtmVisitorCallbacks` (snake_case). Used for documentation.
28    pub c_field: &'static str,
29    /// Java interface method name (camelCase).
30    pub java_method: &'static str,
31    /// Javadoc line.
32    pub doc: &'static str,
33    /// Extra parameters beyond `NodeContext` in the Java interface.
34    pub extra: &'static [ExtraParam],
35    /// If true, add `boolean isHeader` (only visit_table_row).
36    pub has_is_header: bool,
37}
38
39pub struct ExtraParam {
40    /// Java parameter name in the interface.
41    pub java_name: &'static str,
42    /// Java type in the interface method signature.
43    pub java_type: &'static str,
44    /// Panama `ValueLayout` constants for each C-level argument that maps to this Java param.
45    /// One Java param can correspond to multiple C args (e.g. cells = ptr + count).
46    pub c_layouts: &'static [&'static str],
47    /// Java expression to build the interface-typed value from the raw C parameters.
48    /// Raw variables are named `raw_<java_name>_<idx>` where idx counts within c_layouts.
49    pub decode: &'static str,
50}
51
52pub const CALLBACKS: &[CallbackSpec] = &[
53    CallbackSpec {
54        c_field: "visit_text",
55        java_method: "visitText",
56        doc: "Called for text nodes.",
57        extra: &[ExtraParam {
58            java_name: "text",
59            java_type: "String",
60            c_layouts: &["ValueLayout.ADDRESS"],
61            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
62        }],
63        has_is_header: false,
64    },
65    CallbackSpec {
66        c_field: "visit_element_start",
67        java_method: "visitElementStart",
68        doc: "Called before entering any element.",
69        extra: &[],
70        has_is_header: false,
71    },
72    CallbackSpec {
73        c_field: "visit_element_end",
74        java_method: "visitElementEnd",
75        doc: "Called after exiting any element; receives the default markdown output.",
76        extra: &[ExtraParam {
77            java_name: "output",
78            java_type: "String",
79            c_layouts: &["ValueLayout.ADDRESS"],
80            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
81        }],
82        has_is_header: false,
83    },
84    CallbackSpec {
85        c_field: "visit_link",
86        java_method: "visitLink",
87        doc: "Called for anchor links. title is null when the attribute is absent.",
88        extra: &[
89            ExtraParam {
90                java_name: "href",
91                java_type: "String",
92                c_layouts: &["ValueLayout.ADDRESS"],
93                decode: "raw_href_0.reinterpret(Long.MAX_VALUE).getString(0)",
94            },
95            ExtraParam {
96                java_name: "text",
97                java_type: "String",
98                c_layouts: &["ValueLayout.ADDRESS"],
99                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
100            },
101            ExtraParam {
102                java_name: "title",
103                java_type: "String",
104                c_layouts: &["ValueLayout.ADDRESS"],
105                decode: "raw_title_0.equals(MemorySegment.NULL) ? null : raw_title_0.reinterpret(Long.MAX_VALUE).getString(0)",
106            },
107        ],
108        has_is_header: false,
109    },
110    CallbackSpec {
111        c_field: "visit_image",
112        java_method: "visitImage",
113        doc: "Called for images. title is null when absent.",
114        extra: &[
115            ExtraParam {
116                java_name: "src",
117                java_type: "String",
118                c_layouts: &["ValueLayout.ADDRESS"],
119                decode: "raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
120            },
121            ExtraParam {
122                java_name: "alt",
123                java_type: "String",
124                c_layouts: &["ValueLayout.ADDRESS"],
125                decode: "raw_alt_0.reinterpret(Long.MAX_VALUE).getString(0)",
126            },
127            ExtraParam {
128                java_name: "title",
129                java_type: "String",
130                c_layouts: &["ValueLayout.ADDRESS"],
131                decode: "raw_title_0.equals(MemorySegment.NULL) ? null : raw_title_0.reinterpret(Long.MAX_VALUE).getString(0)",
132            },
133        ],
134        has_is_header: false,
135    },
136    CallbackSpec {
137        c_field: "visit_heading",
138        java_method: "visitHeading",
139        doc: "Called for heading elements h1-h6. id is null when absent.",
140        extra: &[
141            ExtraParam {
142                java_name: "level",
143                java_type: "int",
144                c_layouts: &["ValueLayout.JAVA_INT"],
145                decode: "(int) raw_level_0",
146            },
147            ExtraParam {
148                java_name: "text",
149                java_type: "String",
150                c_layouts: &["ValueLayout.ADDRESS"],
151                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
152            },
153            ExtraParam {
154                java_name: "id",
155                java_type: "String",
156                c_layouts: &["ValueLayout.ADDRESS"],
157                decode: "raw_id_0.equals(MemorySegment.NULL) ? null : raw_id_0.reinterpret(Long.MAX_VALUE).getString(0)",
158            },
159        ],
160        has_is_header: false,
161    },
162    CallbackSpec {
163        c_field: "visit_code_block",
164        java_method: "visitCodeBlock",
165        doc: "Called for code blocks. lang is null when absent.",
166        extra: &[
167            ExtraParam {
168                java_name: "lang",
169                java_type: "String",
170                c_layouts: &["ValueLayout.ADDRESS"],
171                decode: "raw_lang_0.equals(MemorySegment.NULL) ? null : raw_lang_0.reinterpret(Long.MAX_VALUE).getString(0)",
172            },
173            ExtraParam {
174                java_name: "code",
175                java_type: "String",
176                c_layouts: &["ValueLayout.ADDRESS"],
177                decode: "raw_code_0.reinterpret(Long.MAX_VALUE).getString(0)",
178            },
179        ],
180        has_is_header: false,
181    },
182    CallbackSpec {
183        c_field: "visit_code_inline",
184        java_method: "visitCodeInline",
185        doc: "Called for inline code elements.",
186        extra: &[ExtraParam {
187            java_name: "code",
188            java_type: "String",
189            c_layouts: &["ValueLayout.ADDRESS"],
190            decode: "raw_code_0.reinterpret(Long.MAX_VALUE).getString(0)",
191        }],
192        has_is_header: false,
193    },
194    CallbackSpec {
195        c_field: "visit_list_item",
196        java_method: "visitListItem",
197        doc: "Called for list items.",
198        extra: &[
199            ExtraParam {
200                java_name: "ordered",
201                java_type: "boolean",
202                c_layouts: &["ValueLayout.JAVA_INT"],
203                decode: "((int) raw_ordered_0) != 0",
204            },
205            ExtraParam {
206                java_name: "marker",
207                java_type: "String",
208                c_layouts: &["ValueLayout.ADDRESS"],
209                decode: "raw_marker_0.reinterpret(Long.MAX_VALUE).getString(0)",
210            },
211            ExtraParam {
212                java_name: "text",
213                java_type: "String",
214                c_layouts: &["ValueLayout.ADDRESS"],
215                decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
216            },
217        ],
218        has_is_header: false,
219    },
220    CallbackSpec {
221        c_field: "visit_list_start",
222        java_method: "visitListStart",
223        doc: "Called before processing a list.",
224        extra: &[ExtraParam {
225            java_name: "ordered",
226            java_type: "boolean",
227            c_layouts: &["ValueLayout.JAVA_INT"],
228            decode: "((int) raw_ordered_0) != 0",
229        }],
230        has_is_header: false,
231    },
232    CallbackSpec {
233        c_field: "visit_list_end",
234        java_method: "visitListEnd",
235        doc: "Called after processing a list.",
236        extra: &[
237            ExtraParam {
238                java_name: "ordered",
239                java_type: "boolean",
240                c_layouts: &["ValueLayout.JAVA_INT"],
241                decode: "((int) raw_ordered_0) != 0",
242            },
243            ExtraParam {
244                java_name: "output",
245                java_type: "String",
246                c_layouts: &["ValueLayout.ADDRESS"],
247                decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
248            },
249        ],
250        has_is_header: false,
251    },
252    CallbackSpec {
253        c_field: "visit_table_start",
254        java_method: "visitTableStart",
255        doc: "Called before processing a table.",
256        extra: &[],
257        has_is_header: false,
258    },
259    CallbackSpec {
260        c_field: "visit_table_row",
261        java_method: "visitTableRow",
262        doc: "Called for table rows. cells contains the cell text values.",
263        extra: &[ExtraParam {
264            java_name: "cells",
265            java_type: "java.util.List<String>",
266            c_layouts: &["ValueLayout.ADDRESS", "ValueLayout.JAVA_LONG"],
267            decode: "decodeCells(raw_cells_0, (long) raw_cells_1)",
268        }],
269        has_is_header: true,
270    },
271    CallbackSpec {
272        c_field: "visit_table_end",
273        java_method: "visitTableEnd",
274        doc: "Called after processing a table.",
275        extra: &[ExtraParam {
276            java_name: "output",
277            java_type: "String",
278            c_layouts: &["ValueLayout.ADDRESS"],
279            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
280        }],
281        has_is_header: false,
282    },
283    CallbackSpec {
284        c_field: "visit_blockquote",
285        java_method: "visitBlockquote",
286        doc: "Called for blockquote elements.",
287        extra: &[
288            ExtraParam {
289                java_name: "content",
290                java_type: "String",
291                c_layouts: &["ValueLayout.ADDRESS"],
292                decode: "raw_content_0.reinterpret(Long.MAX_VALUE).getString(0)",
293            },
294            ExtraParam {
295                java_name: "depth",
296                java_type: "long",
297                c_layouts: &["ValueLayout.JAVA_LONG"],
298                decode: "(long) raw_depth_0",
299            },
300        ],
301        has_is_header: false,
302    },
303    CallbackSpec {
304        c_field: "visit_strong",
305        java_method: "visitStrong",
306        doc: "Called for strong/bold elements.",
307        extra: &[ExtraParam {
308            java_name: "text",
309            java_type: "String",
310            c_layouts: &["ValueLayout.ADDRESS"],
311            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
312        }],
313        has_is_header: false,
314    },
315    CallbackSpec {
316        c_field: "visit_emphasis",
317        java_method: "visitEmphasis",
318        doc: "Called for emphasis/italic elements.",
319        extra: &[ExtraParam {
320            java_name: "text",
321            java_type: "String",
322            c_layouts: &["ValueLayout.ADDRESS"],
323            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
324        }],
325        has_is_header: false,
326    },
327    CallbackSpec {
328        c_field: "visit_strikethrough",
329        java_method: "visitStrikethrough",
330        doc: "Called for strikethrough elements.",
331        extra: &[ExtraParam {
332            java_name: "text",
333            java_type: "String",
334            c_layouts: &["ValueLayout.ADDRESS"],
335            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
336        }],
337        has_is_header: false,
338    },
339    CallbackSpec {
340        c_field: "visit_underline",
341        java_method: "visitUnderline",
342        doc: "Called for underline elements.",
343        extra: &[ExtraParam {
344            java_name: "text",
345            java_type: "String",
346            c_layouts: &["ValueLayout.ADDRESS"],
347            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
348        }],
349        has_is_header: false,
350    },
351    CallbackSpec {
352        c_field: "visit_subscript",
353        java_method: "visitSubscript",
354        doc: "Called for subscript elements.",
355        extra: &[ExtraParam {
356            java_name: "text",
357            java_type: "String",
358            c_layouts: &["ValueLayout.ADDRESS"],
359            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
360        }],
361        has_is_header: false,
362    },
363    CallbackSpec {
364        c_field: "visit_superscript",
365        java_method: "visitSuperscript",
366        doc: "Called for superscript elements.",
367        extra: &[ExtraParam {
368            java_name: "text",
369            java_type: "String",
370            c_layouts: &["ValueLayout.ADDRESS"],
371            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
372        }],
373        has_is_header: false,
374    },
375    CallbackSpec {
376        c_field: "visit_mark",
377        java_method: "visitMark",
378        doc: "Called for mark/highlight elements.",
379        extra: &[ExtraParam {
380            java_name: "text",
381            java_type: "String",
382            c_layouts: &["ValueLayout.ADDRESS"],
383            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
384        }],
385        has_is_header: false,
386    },
387    CallbackSpec {
388        c_field: "visit_line_break",
389        java_method: "visitLineBreak",
390        doc: "Called for line break elements.",
391        extra: &[],
392        has_is_header: false,
393    },
394    CallbackSpec {
395        c_field: "visit_horizontal_rule",
396        java_method: "visitHorizontalRule",
397        doc: "Called for horizontal rule elements.",
398        extra: &[],
399        has_is_header: false,
400    },
401    CallbackSpec {
402        c_field: "visit_custom_element",
403        java_method: "visitCustomElement",
404        doc: "Called for custom or unknown elements.",
405        extra: &[
406            ExtraParam {
407                java_name: "tagName",
408                java_type: "String",
409                c_layouts: &["ValueLayout.ADDRESS"],
410                decode: "raw_tagName_0.reinterpret(Long.MAX_VALUE).getString(0)",
411            },
412            ExtraParam {
413                java_name: "html",
414                java_type: "String",
415                c_layouts: &["ValueLayout.ADDRESS"],
416                decode: "raw_html_0.reinterpret(Long.MAX_VALUE).getString(0)",
417            },
418        ],
419        has_is_header: false,
420    },
421    CallbackSpec {
422        c_field: "visit_definition_list_start",
423        java_method: "visitDefinitionListStart",
424        doc: "Called before a definition list.",
425        extra: &[],
426        has_is_header: false,
427    },
428    CallbackSpec {
429        c_field: "visit_definition_term",
430        java_method: "visitDefinitionTerm",
431        doc: "Called for definition term elements.",
432        extra: &[ExtraParam {
433            java_name: "text",
434            java_type: "String",
435            c_layouts: &["ValueLayout.ADDRESS"],
436            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
437        }],
438        has_is_header: false,
439    },
440    CallbackSpec {
441        c_field: "visit_definition_description",
442        java_method: "visitDefinitionDescription",
443        doc: "Called for definition description elements.",
444        extra: &[ExtraParam {
445            java_name: "text",
446            java_type: "String",
447            c_layouts: &["ValueLayout.ADDRESS"],
448            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
449        }],
450        has_is_header: false,
451    },
452    CallbackSpec {
453        c_field: "visit_definition_list_end",
454        java_method: "visitDefinitionListEnd",
455        doc: "Called after a definition list.",
456        extra: &[ExtraParam {
457            java_name: "output",
458            java_type: "String",
459            c_layouts: &["ValueLayout.ADDRESS"],
460            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
461        }],
462        has_is_header: false,
463    },
464    CallbackSpec {
465        c_field: "visit_form",
466        java_method: "visitForm",
467        doc: "Called for form elements. action and method may be null.",
468        extra: &[
469            ExtraParam {
470                java_name: "action",
471                java_type: "String",
472                c_layouts: &["ValueLayout.ADDRESS"],
473                decode: "raw_action_0.equals(MemorySegment.NULL) ? null : raw_action_0.reinterpret(Long.MAX_VALUE).getString(0)",
474            },
475            ExtraParam {
476                java_name: "method",
477                java_type: "String",
478                c_layouts: &["ValueLayout.ADDRESS"],
479                decode: "raw_method_0.equals(MemorySegment.NULL) ? null : raw_method_0.reinterpret(Long.MAX_VALUE).getString(0)",
480            },
481        ],
482        has_is_header: false,
483    },
484    CallbackSpec {
485        c_field: "visit_input",
486        java_method: "visitInput",
487        doc: "Called for input elements. name and value may be null.",
488        extra: &[
489            ExtraParam {
490                java_name: "inputType",
491                java_type: "String",
492                c_layouts: &["ValueLayout.ADDRESS"],
493                decode: "raw_inputType_0.reinterpret(Long.MAX_VALUE).getString(0)",
494            },
495            ExtraParam {
496                java_name: "name",
497                java_type: "String",
498                c_layouts: &["ValueLayout.ADDRESS"],
499                decode: "raw_name_0.equals(MemorySegment.NULL) ? null : raw_name_0.reinterpret(Long.MAX_VALUE).getString(0)",
500            },
501            ExtraParam {
502                java_name: "value",
503                java_type: "String",
504                c_layouts: &["ValueLayout.ADDRESS"],
505                decode: "raw_value_0.equals(MemorySegment.NULL) ? null : raw_value_0.reinterpret(Long.MAX_VALUE).getString(0)",
506            },
507        ],
508        has_is_header: false,
509    },
510    CallbackSpec {
511        c_field: "visit_button",
512        java_method: "visitButton",
513        doc: "Called for button elements.",
514        extra: &[ExtraParam {
515            java_name: "text",
516            java_type: "String",
517            c_layouts: &["ValueLayout.ADDRESS"],
518            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
519        }],
520        has_is_header: false,
521    },
522    CallbackSpec {
523        c_field: "visit_audio",
524        java_method: "visitAudio",
525        doc: "Called for audio elements. src may be null.",
526        extra: &[ExtraParam {
527            java_name: "src",
528            java_type: "String",
529            c_layouts: &["ValueLayout.ADDRESS"],
530            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
531        }],
532        has_is_header: false,
533    },
534    CallbackSpec {
535        c_field: "visit_video",
536        java_method: "visitVideo",
537        doc: "Called for video elements. src may be null.",
538        extra: &[ExtraParam {
539            java_name: "src",
540            java_type: "String",
541            c_layouts: &["ValueLayout.ADDRESS"],
542            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
543        }],
544        has_is_header: false,
545    },
546    CallbackSpec {
547        c_field: "visit_iframe",
548        java_method: "visitIframe",
549        doc: "Called for iframe elements. src may be null.",
550        extra: &[ExtraParam {
551            java_name: "src",
552            java_type: "String",
553            c_layouts: &["ValueLayout.ADDRESS"],
554            decode: "raw_src_0.equals(MemorySegment.NULL) ? null : raw_src_0.reinterpret(Long.MAX_VALUE).getString(0)",
555        }],
556        has_is_header: false,
557    },
558    CallbackSpec {
559        c_field: "visit_details",
560        java_method: "visitDetails",
561        doc: "Called for details elements.",
562        extra: &[ExtraParam {
563            java_name: "open",
564            java_type: "boolean",
565            c_layouts: &["ValueLayout.JAVA_INT"],
566            decode: "((int) raw_open_0) != 0",
567        }],
568        has_is_header: false,
569    },
570    CallbackSpec {
571        c_field: "visit_summary",
572        java_method: "visitSummary",
573        doc: "Called for summary elements.",
574        extra: &[ExtraParam {
575            java_name: "text",
576            java_type: "String",
577            c_layouts: &["ValueLayout.ADDRESS"],
578            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
579        }],
580        has_is_header: false,
581    },
582    CallbackSpec {
583        c_field: "visit_figure_start",
584        java_method: "visitFigureStart",
585        doc: "Called before a figure element.",
586        extra: &[],
587        has_is_header: false,
588    },
589    CallbackSpec {
590        c_field: "visit_figcaption",
591        java_method: "visitFigcaption",
592        doc: "Called for figcaption elements.",
593        extra: &[ExtraParam {
594            java_name: "text",
595            java_type: "String",
596            c_layouts: &["ValueLayout.ADDRESS"],
597            decode: "raw_text_0.reinterpret(Long.MAX_VALUE).getString(0)",
598        }],
599        has_is_header: false,
600    },
601    CallbackSpec {
602        c_field: "visit_figure_end",
603        java_method: "visitFigureEnd",
604        doc: "Called after a figure element.",
605        extra: &[ExtraParam {
606            java_name: "output",
607            java_type: "String",
608            c_layouts: &["ValueLayout.ADDRESS"],
609            decode: "raw_output_0.reinterpret(Long.MAX_VALUE).getString(0)",
610        }],
611        has_is_header: false,
612    },
613];
614
615// ---------------------------------------------------------------------------
616// Public API: generate visitor-related Java source files
617// ---------------------------------------------------------------------------
618
619/// Returns `(filename, content)` pairs for all visitor-related Java files.
620///
621/// Callers push these into the `files` vector in `generate_bindings`.
622pub fn gen_visitor_files(package: &str, class_name: &str) -> Vec<(String, String)> {
623    vec![
624        ("NodeContext.java".to_string(), gen_node_context(package)),
625        ("VisitContext.java".to_string(), gen_visit_context(package)),
626        ("VisitResult.java".to_string(), gen_visit_result(package)),
627        ("Visitor.java".to_string(), gen_visitor_interface(package, class_name)),
628        (
629            "VisitorBridge.java".to_string(),
630            gen_visitor_bridge(package, class_name),
631        ),
632        ("TestVisitor.java".to_string(), gen_test_visitor_interface(package)),
633        ("TestVisitorAdapter.java".to_string(), gen_test_visitor_adapter(package)),
634    ]
635}
636
637/// Generate NativeLib method handle declarations for visitor FFI functions.
638///
639/// These lines are injected into the `NativeLib` class body after the normal handles.
640pub fn gen_native_lib_visitor_handles(prefix: &str) -> String {
641    let mut out = String::with_capacity(512);
642    let pu = prefix.to_uppercase();
643
644    writeln!(out).ok();
645    writeln!(out, "    // Visitor FFI handles").ok();
646    writeln!(
647        out,
648        "    static final MethodHandle {pu}_VISITOR_CREATE = LINKER.downcallHandle("
649    )
650    .ok();
651    writeln!(out, "        LIB.find(\"{prefix}_visitor_create\").orElseThrow(),").ok();
652    writeln!(
653        out,
654        "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
655    )
656    .ok();
657    writeln!(out, "    );").ok();
658    writeln!(out).ok();
659    writeln!(
660        out,
661        "    static final MethodHandle {pu}_VISITOR_FREE = LINKER.downcallHandle("
662    )
663    .ok();
664    writeln!(out, "        LIB.find(\"{prefix}_visitor_free\").orElseThrow(),").ok();
665    writeln!(out, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
666    writeln!(out, "    );").ok();
667    writeln!(out).ok();
668    writeln!(
669        out,
670        "    static final MethodHandle {pu}_CONVERT_WITH_VISITOR = LINKER.downcallHandle("
671    )
672    .ok();
673    writeln!(
674        out,
675        "        LIB.find(\"{prefix}_convert_with_visitor\").orElseThrow(),"
676    )
677    .ok();
678    writeln!(
679        out,
680        "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
681    )
682    .ok();
683    writeln!(out, "    );").ok();
684
685    out
686}
687
688/// Generate the `convertWithVisitor` method body to inject into the main wrapper class.
689///
690/// Returns the method source as a string (without surrounding class braces).
691pub fn gen_convert_with_visitor_method(class_name: &str, prefix: &str) -> String {
692    let mut out = String::with_capacity(2048);
693    let pu = prefix.to_uppercase();
694    let exc = format!("{class_name}Exception");
695
696    writeln!(
697        out,
698        "    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor) throws {exc} {{"
699    )
700    .ok();
701    writeln!(out, "        try (var arena = Arena.ofConfined();").ok();
702    writeln!(out, "             var bridge = new VisitorBridge(visitor)) {{").ok();
703    writeln!(out, "            var cHtml = arena.allocateFrom(html);").ok();
704    writeln!(out).ok();
705    writeln!(out, "            MemorySegment optionsPtr = MemorySegment.NULL;").ok();
706    writeln!(out, "            if (options != null) {{").ok();
707    writeln!(
708        out,
709        "                var optJson = arena.allocateFrom(createObjectMapper().writeValueAsString(options));"
710    )
711    .ok();
712    writeln!(
713        out,
714        "                optionsPtr = (MemorySegment) NativeLib.{pu}_CONVERSION_OPTIONS_FROM_JSON.invoke(optJson);"
715    )
716    .ok();
717    writeln!(out, "            }}").ok();
718    writeln!(out).ok();
719    writeln!(
720        out,
721        "            var visitorHandle = (MemorySegment) NativeLib.{pu}_VISITOR_CREATE.invoke(bridge.callbacksStruct());"
722    )
723    .ok();
724    writeln!(out, "            if (visitorHandle.equals(MemorySegment.NULL)) {{").ok();
725    writeln!(
726        out,
727        "                throw new {exc}(\"Failed to create visitor handle\", null);"
728    )
729    .ok();
730    writeln!(out, "            }}").ok();
731    writeln!(out).ok();
732    writeln!(out, "            try {{").ok();
733    writeln!(
734        out,
735        "                var resultPtr = (MemorySegment) NativeLib.{pu}_CONVERT_WITH_VISITOR.invoke(cHtml, optionsPtr, visitorHandle);"
736    )
737    .ok();
738    writeln!(out, "                if (!optionsPtr.equals(MemorySegment.NULL)) {{").ok();
739    writeln!(
740        out,
741        "                    NativeLib.{pu}_CONVERSION_OPTIONS_FREE.invoke(optionsPtr);"
742    )
743    .ok();
744    writeln!(out, "                }}").ok();
745    writeln!(out, "                if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
746    writeln!(out, "                    checkLastError();").ok();
747    writeln!(out, "                    return null;").ok();
748    writeln!(out, "                }}").ok();
749    writeln!(
750        out,
751        "                var markdown = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
752    )
753    .ok();
754    writeln!(out, "                NativeLib.{pu}_FREE_STRING.invoke(resultPtr);").ok();
755    writeln!(
756        out,
757        "                return new ConversionResult(java.util.Optional.of(markdown), java.util.Optional.empty(), null, null, null, null);"
758    )
759    .ok();
760    writeln!(out, "            }} catch (Throwable e) {{").ok();
761    writeln!(out, "                throw new {exc}(\"FFI call failed\", e);").ok();
762    writeln!(out, "            }} finally {{").ok();
763    writeln!(
764        out,
765        "                NativeLib.{pu}_VISITOR_FREE.invoke(visitorHandle);"
766    )
767    .ok();
768    writeln!(out, "            }}").ok();
769    writeln!(out, "        }} catch ({exc} e) {{").ok();
770    writeln!(out, "            throw e;").ok();
771    writeln!(out, "        }} catch (Throwable e) {{").ok();
772    writeln!(out, "            throw new {exc}(\"FFI call failed\", e);").ok();
773    writeln!(out, "        }}").ok();
774    writeln!(out, "    }}").ok();
775
776    out
777}
778
779/// Generate the `convert(html, options, TestVisitor)` overload injected into the public facade.
780pub fn gen_convert_with_test_visitor_method(raw_class: &str) -> String {
781    let mut out = String::with_capacity(512);
782    let exc = format!("{raw_class}Exception");
783    writeln!(
784        out,
785        "    public static ConversionResult convert(final String html, final ConversionOptions options,"
786    )
787    .ok();
788    writeln!(out, "            final TestVisitor visitor) throws {exc} {{").ok();
789    writeln!(
790        out,
791        "        return {raw_class}.convertWithVisitor(html, options, new TestVisitorAdapter(visitor));"
792    )
793    .ok();
794    writeln!(out, "    }}").ok();
795    out
796}
797
798// ---------------------------------------------------------------------------
799// Individual file generators
800// ---------------------------------------------------------------------------
801
802fn gen_node_context(package: &str) -> String {
803    let mut out = String::with_capacity(1024);
804    out.push_str(&hash::header(CommentStyle::DoubleSlash));
805    writeln!(out, "package {package};").ok();
806    writeln!(out).ok();
807    writeln!(out, "/** Context passed to every visitor callback. */").ok();
808    writeln!(out, "public record NodeContext(").ok();
809    writeln!(out, "        /** Coarse-grained node type tag. */").ok();
810    writeln!(out, "        int nodeType,").ok();
811    writeln!(out, "        /** HTML element tag name (e.g. \"div\"). */").ok();
812    writeln!(out, "        String tagName,").ok();
813    writeln!(out, "        /** DOM depth (0 = root). */").ok();
814    writeln!(out, "        long depth,").ok();
815    writeln!(out, "        /** 0-based sibling index. */").ok();
816    writeln!(out, "        long indexInParent,").ok();
817    writeln!(out, "        /** Parent element tag name, or null at the root. */").ok();
818    writeln!(out, "        String parentTag,").ok();
819    writeln!(out, "        /** True when this element is treated as inline. */").ok();
820    writeln!(out, "        boolean isInline").ok();
821    writeln!(out, ") {{}}").ok();
822    out
823}
824
825/// Generate `VisitContext.java` — the context type passed to `TestVisitor` callbacks.
826fn gen_visit_context(package: &str) -> String {
827    let mut out = String::with_capacity(1024);
828    out.push_str(&hash::header(CommentStyle::DoubleSlash));
829    writeln!(out, "package {package};").ok();
830    writeln!(out).ok();
831    writeln!(out, "/** Context passed to every visitor callback. */").ok();
832    writeln!(out, "public record VisitContext(").ok();
833    writeln!(out, "        /** Coarse-grained node type tag. */").ok();
834    writeln!(out, "        int nodeType,").ok();
835    writeln!(out, "        /** HTML element tag name (e.g. \"div\"). */").ok();
836    writeln!(out, "        String tagName,").ok();
837    writeln!(out, "        /** DOM depth (0 = root). */").ok();
838    writeln!(out, "        long depth,").ok();
839    writeln!(out, "        /** 0-based sibling index. */").ok();
840    writeln!(out, "        long indexInParent,").ok();
841    writeln!(out, "        /** Parent element tag name, or null at the root. */").ok();
842    writeln!(out, "        String parentTag,").ok();
843    writeln!(out, "        /** True when this element is treated as inline. */").ok();
844    writeln!(out, "        boolean isInline").ok();
845    writeln!(out, ") {{}}").ok();
846    out
847}
848
849fn gen_visit_result(package: &str) -> String {
850    let mut out = String::with_capacity(2048);
851    out.push_str(&hash::header(CommentStyle::DoubleSlash));
852    writeln!(out, "package {package};").ok();
853    writeln!(out).ok();
854    writeln!(out, "/** Controls how the visitor affects the conversion pipeline. */").ok();
855    writeln!(out, "public sealed interface VisitResult").ok();
856    writeln!(
857        out,
858        "        permits VisitResult.Continue, VisitResult.Skip, VisitResult.PreserveHtml,"
859    )
860    .ok();
861    writeln!(out, "                VisitResult.Custom, VisitResult.Error {{").ok();
862    writeln!(out).ok();
863    writeln!(out, "    /** Proceed with default conversion. */").ok();
864    writeln!(out, "    record Continue() implements VisitResult {{}}").ok();
865    writeln!(out).ok();
866    writeln!(out, "    /** Omit this element from output entirely. */").ok();
867    writeln!(out, "    record Skip() implements VisitResult {{}}").ok();
868    writeln!(out).ok();
869    writeln!(out, "    /** Keep original HTML verbatim. */").ok();
870    writeln!(out, "    record PreserveHtml() implements VisitResult {{}}").ok();
871    writeln!(out).ok();
872    writeln!(out, "    /** Replace with custom Markdown. */").ok();
873    writeln!(out, "    record Custom(String markdown) implements VisitResult {{}}").ok();
874    writeln!(out).ok();
875    writeln!(out, "    /** Abort conversion with an error message. */").ok();
876    writeln!(out, "    record Error(String message) implements VisitResult {{}}").ok();
877    writeln!(out).ok();
878    writeln!(out, "    /** Convenience: continue with default conversion. */").ok();
879    writeln!(
880        out,
881        "    static VisitResult continueDefault() {{ return new Continue(); }}"
882    )
883    .ok();
884    writeln!(out).ok();
885    writeln!(out, "    /** Convenience: skip this element. */").ok();
886    writeln!(out, "    static VisitResult skip() {{ return new Skip(); }}").ok();
887    writeln!(out).ok();
888    writeln!(out, "    /** Convenience: preserve original HTML. */").ok();
889    writeln!(
890        out,
891        "    static VisitResult preserveHtml() {{ return new PreserveHtml(); }}"
892    )
893    .ok();
894    writeln!(out).ok();
895    writeln!(out, "    /** Convenience: emit custom Markdown. */").ok();
896    writeln!(
897        out,
898        "    static VisitResult custom(String markdown) {{ return new Custom(markdown); }}"
899    )
900    .ok();
901    writeln!(out).ok();
902    writeln!(out, "    /** Convenience: abort with error. */").ok();
903    writeln!(
904        out,
905        "    static VisitResult error(String message) {{ return new Error(message); }}"
906    )
907    .ok();
908    writeln!(out).ok();
909    writeln!(out, "    /** Alias for {{@link #continueDefault()}}. */").ok();
910    writeln!(out, "    static VisitResult continue_() {{ return new Continue(); }}").ok();
911    writeln!(out, "}}").ok();
912    out
913}
914
915fn gen_visitor_interface(package: &str, _class_name: &str) -> String {
916    let mut out = String::with_capacity(4096);
917    out.push_str(&hash::header(CommentStyle::DoubleSlash));
918    writeln!(out, "package {package};").ok();
919    writeln!(out).ok();
920    writeln!(
921        out,
922        "/** Visitor interface for the HTML-to-Markdown conversion pipeline. */"
923    )
924    .ok();
925    writeln!(out, "public interface Visitor {{").ok();
926    for spec in CALLBACKS {
927        let params = iface_param_str(spec);
928        writeln!(out, "    /** {} */", spec.doc).ok();
929        writeln!(
930            out,
931            "    default VisitResult {}({}) {{ return VisitResult.continueDefault(); }}",
932            spec.java_method, params
933        )
934        .ok();
935    }
936    writeln!(out, "}}").ok();
937    out
938}
939
940/// Generate `TestVisitor.java` — visitor interface using `VisitContext` (test-friendly).
941///
942/// Same shape as `Visitor` but uses `VisitContext` instead of `NodeContext` so
943/// e2e tests do not need to import the raw FFI type.
944fn gen_test_visitor_interface(package: &str) -> String {
945    let mut out = String::with_capacity(4096);
946    out.push_str(&hash::header(CommentStyle::DoubleSlash));
947    writeln!(out, "package {package};").ok();
948    writeln!(out).ok();
949    writeln!(
950        out,
951        "/** Test-friendly visitor interface using VisitContext instead of NodeContext. */"
952    )
953    .ok();
954    writeln!(out, "public interface TestVisitor {{").ok();
955    for spec in CALLBACKS {
956        let params = test_iface_param_str(spec);
957        writeln!(out, "    /** {} */", spec.doc).ok();
958        writeln!(
959            out,
960            "    default VisitResult {}({}) {{ return VisitResult.continueDefault(); }}",
961            spec.java_method, params
962        )
963        .ok();
964    }
965    writeln!(out, "}}").ok();
966    out
967}
968
969/// Generate `TestVisitorAdapter.java` — adapts `TestVisitor` to `Visitor` by converting
970/// `NodeContext` → `VisitContext` before dispatching to the wrapped `TestVisitor`.
971fn gen_test_visitor_adapter(package: &str) -> String {
972    let mut out = String::with_capacity(4096);
973    out.push_str(&hash::header(CommentStyle::DoubleSlash));
974    writeln!(out, "package {package};").ok();
975    writeln!(out).ok();
976    writeln!(
977        out,
978        "/** Adapts a {{@link TestVisitor}} to the {{@link Visitor}} interface. */"
979    )
980    .ok();
981    writeln!(out, "final class TestVisitorAdapter implements Visitor {{").ok();
982    writeln!(out, "    private final TestVisitor delegate;").ok();
983    writeln!(out).ok();
984    writeln!(out, "    TestVisitorAdapter(final TestVisitor delegate) {{").ok();
985    writeln!(
986        out,
987        "        java.util.Objects.requireNonNull(delegate, \"delegate must not be null\");"
988    )
989    .ok();
990    writeln!(out, "        this.delegate = delegate;").ok();
991    writeln!(out, "    }}").ok();
992    writeln!(out).ok();
993    writeln!(
994        out,
995        "    private static VisitContext toVisitContext(final NodeContext ctx) {{"
996    )
997    .ok();
998    writeln!(
999        out,
1000        "        return new VisitContext(ctx.nodeType(), ctx.tagName(), ctx.depth(), ctx.indexInParent(), ctx.parentTag(), ctx.isInline());"
1001    )
1002    .ok();
1003    writeln!(out, "    }}").ok();
1004    writeln!(out).ok();
1005    for spec in CALLBACKS {
1006        let node_params = iface_param_str(spec);
1007        let visit_params = test_iface_param_str(spec);
1008        // Build delegation call args — same as call_args but use 'visitCtx' for context
1009        let mut call_args = vec!["visitCtx".to_string()];
1010        for ep in spec.extra {
1011            call_args.push(ep.java_name.to_string());
1012        }
1013        if spec.has_is_header {
1014            call_args.push("isHeader".to_string());
1015        }
1016        writeln!(out, "    @Override").ok();
1017        // Check if method sig is short enough for one line
1018        let single = format!("    public VisitResult {}({}) {{", spec.java_method, node_params);
1019        if single.len() <= 100 {
1020            writeln!(out, "{}", single).ok();
1021        } else {
1022            writeln!(out, "    public VisitResult {}(", spec.java_method).ok();
1023            writeln!(out, "            {}) {{", node_params).ok();
1024        }
1025        writeln!(out, "        var visitCtx = toVisitContext(context);").ok();
1026        // Drop 'final NodeContext context' from params — same args but replace context with visitCtx
1027        // Build the TestVisitor method call with VisitContext params
1028        let _ = visit_params; // used for documentation only
1029        writeln!(
1030            out,
1031            "        return delegate.{}({});",
1032            spec.java_method,
1033            call_args.join(", ")
1034        )
1035        .ok();
1036        writeln!(out, "    }}").ok();
1037        writeln!(out).ok();
1038    }
1039    writeln!(out, "}}").ok();
1040    out
1041}
1042
1043/// Build param string for TestVisitor using VisitContext instead of NodeContext.
1044fn test_iface_param_str(spec: &CallbackSpec) -> String {
1045    let mut params = vec!["final VisitContext ctx".to_string()];
1046    for ep in spec.extra {
1047        params.push(format!("final {} {}", ep.java_type, ep.java_name));
1048    }
1049    if spec.has_is_header {
1050        params.push("final boolean isHeader".to_string());
1051    }
1052    params.join(", ")
1053}
1054
1055/// Generate `VisitorBridge.java` — builds Panama upcall stubs for all 40 callbacks
1056/// and exposes a `MemorySegment callbacksStruct()` pointing to the C struct.
1057fn gen_visitor_bridge(package: &str, _class_name: &str) -> String {
1058    let mut out = String::with_capacity(32_768);
1059    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1060    writeln!(out, "package {package};").ok();
1061    writeln!(out).ok();
1062    writeln!(out, "import java.lang.foreign.Arena;").ok();
1063    writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
1064    writeln!(out, "import java.lang.foreign.Linker;").ok();
1065    writeln!(out, "import java.lang.foreign.MemoryLayout;").ok();
1066    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1067    writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
1068    writeln!(out, "import java.lang.invoke.MethodHandles;").ok();
1069    writeln!(out, "import java.lang.invoke.MethodType;").ok();
1070    writeln!(out, "import java.util.ArrayList;").ok();
1071    writeln!(out, "import java.util.List;").ok();
1072    writeln!(out).ok();
1073
1074    writeln!(out, "/**").ok();
1075    writeln!(out, " * Allocates Panama FFM upcall stubs for a Visitor and assembles").ok();
1076    writeln!(out, " * the C HTMHtmVisitorCallbacks struct in native memory.").ok();
1077    writeln!(out, " */").ok();
1078    writeln!(out, "final class VisitorBridge implements AutoCloseable {{").ok();
1079    writeln!(out, "    private static final Linker LINKER = Linker.nativeLinker();").ok();
1080    writeln!(out, "    private static final MethodHandles.Lookup LOOKUP =").ok();
1081    writeln!(out, "        MethodHandles.lookup();").ok();
1082    writeln!(out).ok();
1083    // Named constants for VisitResult discriminant values
1084    writeln!(out, "    // VisitResult discriminant codes returned to C").ok();
1085    writeln!(out, "    private static final int VISIT_RESULT_CONTINUE = 0;").ok();
1086    writeln!(out, "    private static final int VISIT_RESULT_SKIP = 1;").ok();
1087    writeln!(out, "    private static final int VISIT_RESULT_PRESERVE_HTML = 2;").ok();
1088    writeln!(out, "    private static final int VISIT_RESULT_CUSTOM = 3;").ok();
1089    writeln!(out, "    private static final int VISIT_RESULT_ERROR = 4;").ok();
1090    writeln!(out).ok();
1091
1092    // The struct has user_data (pointer) + 40 function pointer fields.
1093    let num_fields = CALLBACKS.len() + 1; // +1 for user_data
1094    writeln!(
1095        out,
1096        "    // HTMHtmVisitorCallbacks: user_data + {n} callbacks",
1097        n = CALLBACKS.len(),
1098    )
1099    .ok();
1100    writeln!(out, "    // = {total} pointer-sized slots", total = num_fields,).ok();
1101    writeln!(out, "    private static final long CALLBACKS_STRUCT_SIZE =").ok();
1102    writeln!(out, "        (long) ValueLayout.ADDRESS.byteSize() * {num_fields}L;").ok();
1103    writeln!(out).ok();
1104    // Named offset constants for HTMHtmNodeContext struct fields (avoids magic numbers)
1105    writeln!(out, "    // HTMHtmNodeContext field offsets").ok();
1106    writeln!(out, "    private static final long CTX_OFFSET_TAG_NAME = 8L;").ok();
1107    writeln!(out, "    private static final long CTX_OFFSET_DEPTH = 16L;").ok();
1108    writeln!(out, "    private static final long CTX_OFFSET_INDEX_IN_PARENT = 24L;").ok();
1109    writeln!(out, "    private static final long CTX_OFFSET_PARENT_TAG = 32L;").ok();
1110    writeln!(out, "    private static final long CTX_OFFSET_IS_INLINE = 40L;").ok();
1111    writeln!(out).ok();
1112    writeln!(out, "    private final Arena arena;").ok();
1113    writeln!(out, "    private final MemorySegment struct;").ok();
1114    writeln!(out, "    private final Visitor visitor;").ok();
1115    writeln!(out).ok();
1116    writeln!(out, "    VisitorBridge(Visitor visitor) {{").ok();
1117    writeln!(out, "        this.visitor = visitor;").ok();
1118    writeln!(out, "        this.arena = Arena.ofConfined();").ok();
1119    writeln!(out, "        this.struct = arena.allocate(CALLBACKS_STRUCT_SIZE);").ok();
1120    writeln!(out, "        // Slot 0: user_data = NULL").ok();
1121    writeln!(out, "        // (visitor captured via lambda closure)").ok();
1122    writeln!(out, "        struct.set(ValueLayout.ADDRESS, 0L, MemorySegment.NULL);").ok();
1123    writeln!(out, "        try {{").ok();
1124    writeln!(out, "            long offset = ValueLayout.ADDRESS.byteSize();").ok();
1125    // Compute number of registerStubs sub-methods needed
1126    let num_chunks = CALLBACKS.chunks(10).count();
1127    for i in 1..=num_chunks {
1128        if i < num_chunks {
1129            writeln!(out, "            offset = registerStubs{}(offset);", i).ok();
1130        } else {
1131            writeln!(out, "            registerStubs{}(offset);", i).ok();
1132        }
1133    }
1134    writeln!(out, "        }} catch (ReflectiveOperationException e) {{").ok();
1135    writeln!(out, "            arena.close();").ok();
1136    writeln!(out, "            throw new RuntimeException(").ok();
1137    writeln!(out, "                \"Failed to create visitor upcall stubs\", e);").ok();
1138    writeln!(out, "        }}").ok();
1139    writeln!(out, "    }}").ok();
1140    writeln!(out).ok();
1141
1142    // Split callbacks into chunks of 10 each; each sub-method returns the updated offset.
1143    const CHUNK_SIZE: usize = 10;
1144    for (chunk_idx, chunk) in CALLBACKS.chunks(CHUNK_SIZE).enumerate() {
1145        let method_num = chunk_idx + 1;
1146        writeln!(
1147            out,
1148            "    private long registerStubs{}(\n            final long offset)\n            throws ReflectiveOperationException {{",
1149            method_num
1150        )
1151        .ok();
1152        writeln!(out, "        long off = offset;").ok();
1153        for spec in chunk {
1154            let descriptor = callback_descriptor(spec);
1155            let method_type = callback_method_type(spec);
1156            let stub_var = stub_var_name(spec.java_method);
1157            writeln!(out, "        // {}", spec.c_field).ok();
1158            writeln!(out, "        var {} = LINKER.upcallStub(", stub_var).ok();
1159            writeln!(out, "                LOOKUP.bind(",).ok();
1160            writeln!(
1161                out,
1162                "                    this, \"{}\",",
1163                handle_method_name(spec.java_method),
1164            )
1165            .ok();
1166            writeln!(out, "                    {}),", method_type).ok();
1167            writeln!(out, "                {},", descriptor).ok();
1168            writeln!(out, "                arena);").ok();
1169            writeln!(out, "        struct.set(ValueLayout.ADDRESS, off, {});", stub_var).ok();
1170            writeln!(out, "        off += ValueLayout.ADDRESS.byteSize();").ok();
1171        }
1172        writeln!(out, "        return off;").ok();
1173        writeln!(out, "    }}").ok();
1174        writeln!(out).ok();
1175    }
1176    writeln!(out).ok();
1177    writeln!(
1178        out,
1179        "    /** Returns the native HTMHtmVisitorCallbacks struct pointer. */"
1180    )
1181    .ok();
1182    writeln!(out, "    MemorySegment callbacksStruct() {{").ok();
1183    writeln!(out, "        return struct;").ok();
1184    writeln!(out, "    }}").ok();
1185    writeln!(out).ok();
1186
1187    // Generate one handle_* method per callback
1188    for spec in CALLBACKS {
1189        gen_handle_method(&mut out, spec);
1190    }
1191
1192    // decodeNodeContext helper
1193    writeln!(
1194        out,
1195        "    // HTMHtmNodeContext: int32 node_type, char* tag_name, uintptr depth,"
1196    )
1197    .ok();
1198    writeln!(out, "    //   uintptr index_in_parent, char* parent_tag,").ok();
1199    writeln!(out, "    //   int32 is_inline").ok();
1200    writeln!(out, "    private static final MemoryLayout CTX_LAYOUT =").ok();
1201    writeln!(out, "        MemoryLayout.structLayout(").ok();
1202    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"node_type\"),").ok();
1203    writeln!(out, "            MemoryLayout.paddingLayout(4),").ok();
1204    writeln!(out, "            ValueLayout.ADDRESS.withName(\"tag_name\"),").ok();
1205    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"depth\"),").ok();
1206    writeln!(out, "            ValueLayout.JAVA_LONG.withName(\"index_in_parent\"),").ok();
1207    writeln!(out, "            ValueLayout.ADDRESS.withName(\"parent_tag\"),").ok();
1208    writeln!(out, "            ValueLayout.JAVA_INT.withName(\"is_inline\"),").ok();
1209    writeln!(out, "            MemoryLayout.paddingLayout(4)").ok();
1210    writeln!(out, "    );").ok();
1211    writeln!(out).ok();
1212    writeln!(out, "    private static NodeContext decodeNodeContext(").ok();
1213    writeln!(out, "            final MemorySegment ctxPtr) {{").ok();
1214    writeln!(out, "        var ctx = ctxPtr.reinterpret(").ok();
1215    writeln!(out, "            CTX_LAYOUT.byteSize());").ok();
1216    writeln!(out, "        int nodeType = ctx.get(").ok();
1217    writeln!(out, "            ValueLayout.JAVA_INT, 0L);").ok();
1218    writeln!(out, "        var tagNamePtr = ctx.get(").ok();
1219    writeln!(out, "            ValueLayout.ADDRESS, CTX_OFFSET_TAG_NAME);").ok();
1220    writeln!(out, "        String tagName = tagNamePtr").ok();
1221    writeln!(out, "            .reinterpret(Long.MAX_VALUE).getString(0);").ok();
1222    writeln!(
1223        out,
1224        "        long depth = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_DEPTH);"
1225    )
1226    .ok();
1227    writeln!(
1228        out,
1229        "        long indexInParent = ctx.get(ValueLayout.JAVA_LONG, CTX_OFFSET_INDEX_IN_PARENT);"
1230    )
1231    .ok();
1232    writeln!(
1233        out,
1234        "        var parentTagPtr = ctx.get(ValueLayout.ADDRESS, CTX_OFFSET_PARENT_TAG);"
1235    )
1236    .ok();
1237    writeln!(
1238        out,
1239        "        String parentTag = parentTagPtr.equals(MemorySegment.NULL) ? null"
1240    )
1241    .ok();
1242    writeln!(
1243        out,
1244        "                : parentTagPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1245    )
1246    .ok();
1247    writeln!(
1248        out,
1249        "        int isInlineRaw = ctx.get(ValueLayout.JAVA_INT, CTX_OFFSET_IS_INLINE);"
1250    )
1251    .ok();
1252    writeln!(
1253        out,
1254        "        return new NodeContext(nodeType, tagName, depth, indexInParent, parentTag, isInlineRaw != 0);"
1255    )
1256    .ok();
1257    writeln!(out, "    }}").ok();
1258    writeln!(out).ok();
1259
1260    // decodeCells helper
1261    writeln!(
1262        out,
1263        "    private static List<String> decodeCells(MemorySegment cellsPtr, long count) {{"
1264    )
1265    .ok();
1266    writeln!(out, "        var result = new ArrayList<String>((int) count);").ok();
1267    writeln!(out, "        for (long i = 0; i < count; i++) {{").ok();
1268    writeln!(
1269        out,
1270        "            var ptr = cellsPtr.get(ValueLayout.ADDRESS, i * ValueLayout.ADDRESS.byteSize());"
1271    )
1272    .ok();
1273    writeln!(
1274        out,
1275        "            result.add(ptr.reinterpret(Long.MAX_VALUE).getString(0));"
1276    )
1277    .ok();
1278    writeln!(out, "        }}").ok();
1279    writeln!(out, "        return result;").ok();
1280    writeln!(out, "    }}").ok();
1281    writeln!(out).ok();
1282
1283    // encodeVisitResult helper
1284    // outCustom and outLen arrive as 0-byte upcall MemorySegments; we must
1285    // reinterpret them to the correct size before writing (Panama FFM requirement).
1286    // We use Arena.global() so the allocated buffer outlives this callback frame —
1287    // Rust calls free() on the pointer AFTER the callback returns.
1288    writeln!(
1289        out,
1290        "    private static int encodeVisitResult(VisitResult result, MemorySegment outCustom, MemorySegment outLen) {{"
1291    )
1292    .ok();
1293    writeln!(out, "        return switch (result) {{").ok();
1294    writeln!(
1295        out,
1296        "            case VisitResult.Continue ignored -> VISIT_RESULT_CONTINUE;"
1297    )
1298    .ok();
1299    writeln!(out, "            case VisitResult.Skip ignored -> VISIT_RESULT_SKIP;").ok();
1300    writeln!(
1301        out,
1302        "            case VisitResult.PreserveHtml ignored -> VISIT_RESULT_PRESERVE_HTML;"
1303    )
1304    .ok();
1305    writeln!(out, "            case VisitResult.Custom c -> {{").ok();
1306    writeln!(
1307        out,
1308        "                var buf = Arena.global().allocateFrom(c.markdown());"
1309    )
1310    .ok();
1311    writeln!(
1312        out,
1313        "                outCustom.reinterpret(ValueLayout.ADDRESS.byteSize()).set(ValueLayout.ADDRESS, 0L, buf);"
1314    )
1315    .ok();
1316    writeln!(
1317        out,
1318        "                outLen.reinterpret(ValueLayout.JAVA_LONG.byteSize()).set(ValueLayout.JAVA_LONG, 0L, (long) c.markdown().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1319    )
1320    .ok();
1321    writeln!(out, "                yield VISIT_RESULT_CUSTOM;").ok();
1322    writeln!(out, "            }}").ok();
1323    writeln!(out, "            case VisitResult.Error e -> {{").ok();
1324    writeln!(
1325        out,
1326        "                var buf = Arena.global().allocateFrom(e.message());"
1327    )
1328    .ok();
1329    writeln!(
1330        out,
1331        "                outCustom.reinterpret(ValueLayout.ADDRESS.byteSize()).set(ValueLayout.ADDRESS, 0L, buf);"
1332    )
1333    .ok();
1334    writeln!(
1335        out,
1336        "                outLen.reinterpret(ValueLayout.JAVA_LONG.byteSize()).set(ValueLayout.JAVA_LONG, 0L, (long) e.message().getBytes(java.nio.charset.StandardCharsets.UTF_8).length);"
1337    )
1338    .ok();
1339    writeln!(out, "                yield VISIT_RESULT_ERROR;").ok();
1340    writeln!(out, "            }}").ok();
1341    writeln!(out, "        }};").ok();
1342    writeln!(out, "    }}").ok();
1343    writeln!(out).ok();
1344
1345    writeln!(out, "    @Override").ok();
1346    writeln!(out, "    public void close() {{").ok();
1347    writeln!(out, "        arena.close();").ok();
1348    writeln!(out, "    }}").ok();
1349    writeln!(out, "}}").ok();
1350    out
1351}
1352
1353// ---------------------------------------------------------------------------
1354// Internal helpers
1355// ---------------------------------------------------------------------------
1356
1357/// Generate camelCase stub variable name: stub + capitalize(java_method).
1358/// e.g. visitText -> stubVisitText
1359fn stub_var_name(java_method: &str) -> String {
1360    let mut name = String::with_capacity(5 + java_method.len());
1361    name.push_str("stub");
1362    let mut chars = java_method.chars();
1363    if let Some(first) = chars.next() {
1364        for c in first.to_uppercase() {
1365            name.push(c);
1366        }
1367        name.push_str(chars.as_str());
1368    }
1369    name
1370}
1371
1372fn handle_method_name(java_method: &str) -> String {
1373    // camelCase: "handle" + capitalize first letter of java_method
1374    let mut name = String::with_capacity(7 + java_method.len());
1375    name.push_str("handle");
1376    let mut chars = java_method.chars();
1377    if let Some(first) = chars.next() {
1378        for c in first.to_uppercase() {
1379            name.push(c);
1380        }
1381        name.push_str(chars.as_str());
1382    }
1383    name
1384}
1385
1386fn iface_param_str(spec: &CallbackSpec) -> String {
1387    let mut params = vec!["final NodeContext context".to_string()];
1388    for ep in spec.extra {
1389        params.push(format!("final {} {}", ep.java_type, ep.java_name));
1390    }
1391    if spec.has_is_header {
1392        params.push("final boolean isHeader".to_string());
1393    }
1394    params.join(", ")
1395}
1396
1397/// Build the `FunctionDescriptor` for one callback's upcall stub.
1398/// All callbacks: (ADDRESS ctx, ADDRESS userData, ..extra.., ADDRESS outCustom, ADDRESS outLen) -> JAVA_INT
1399/// Returns a multi-line string with 20-space continuation indent so no line exceeds 80 chars.
1400fn callback_descriptor(spec: &CallbackSpec) -> String {
1401    let mut layouts = vec![
1402        "ValueLayout.ADDRESS".to_string(), // ctx
1403        "ValueLayout.ADDRESS".to_string(), // user_data
1404    ];
1405    for ep in spec.extra {
1406        for layout in ep.c_layouts {
1407            layouts.push((*layout).to_string());
1408        }
1409    }
1410    if spec.has_is_header {
1411        layouts.push("ValueLayout.JAVA_INT".to_string());
1412    }
1413    layouts.push("ValueLayout.ADDRESS".to_string()); // out_custom
1414    layouts.push("ValueLayout.ADDRESS".to_string()); // out_len
1415    let indent = "                    ";
1416    let args = layouts.join(&format!(",\n{indent}"));
1417    format!("FunctionDescriptor.of(\n{indent}ValueLayout.JAVA_INT,\n{indent}{args})")
1418}
1419
1420/// Build the `MethodType` for `LOOKUP.bind(this, name, type)`.
1421/// Returns a multi-line string with 20-space continuation indent so no line exceeds 80 chars.
1422fn callback_method_type(spec: &CallbackSpec) -> String {
1423    let mut types = vec![
1424        "MemorySegment.class".to_string(), // ctx
1425        "MemorySegment.class".to_string(), // user_data
1426    ];
1427    for ep in spec.extra {
1428        for layout in ep.c_layouts {
1429            types.push(layout_to_java_class(layout).to_string());
1430        }
1431    }
1432    if spec.has_is_header {
1433        types.push("int.class".to_string());
1434    }
1435    types.push("MemorySegment.class".to_string()); // out_custom
1436    types.push("MemorySegment.class".to_string()); // out_len
1437    let indent = "                    ";
1438    let args = types.join(&format!(",\n{indent}"));
1439    format!("MethodType.methodType(\n{indent}int.class,\n{indent}{args})")
1440}
1441
1442fn layout_to_java_class(layout: &str) -> &'static str {
1443    match layout {
1444        "ValueLayout.ADDRESS" => "MemorySegment.class",
1445        "ValueLayout.JAVA_INT" => "int.class",
1446        "ValueLayout.JAVA_LONG" => "long.class",
1447        _ => "long.class",
1448    }
1449}
1450
1451/// Generate one `handle_*` instance method inside `VisitorBridge`.
1452fn gen_handle_method(out: &mut String, spec: &CallbackSpec) {
1453    // Build method signature matching the MethodType passed to upcallStub.
1454    let mut params = vec![
1455        "final MemorySegment ctx".to_string(),
1456        "final MemorySegment userData".to_string(),
1457    ];
1458    for ep in spec.extra {
1459        for (c_idx, layout) in ep.c_layouts.iter().enumerate() {
1460            let java_ptype = match *layout {
1461                "ValueLayout.JAVA_INT" => "int",
1462                "ValueLayout.JAVA_LONG" => "long",
1463                _ => "MemorySegment",
1464            };
1465            params.push(format!("final {java_ptype} {}", raw_var_name(ep.java_name, c_idx)));
1466        }
1467    }
1468    if spec.has_is_header {
1469        params.push("final int isHeader".to_string());
1470    }
1471    params.push("final MemorySegment outCustom".to_string());
1472    params.push("final MemorySegment outLen".to_string());
1473
1474    let method_name = handle_method_name(spec.java_method);
1475    let single_line = format!("    int {}({}) {{", method_name, params.join(", "));
1476    if single_line.len() <= 80 {
1477        writeln!(out, "{}", single_line).ok();
1478    } else {
1479        let indent = "            ";
1480        writeln!(
1481            out,
1482            "    int {}(\n{}{}) {{",
1483            method_name,
1484            indent,
1485            params.join(&format!(",\n{indent}"))
1486        )
1487        .ok();
1488    }
1489    writeln!(out, "        try {{").ok();
1490    writeln!(out, "            var context = decodeNodeContext(ctx);").ok();
1491
1492    // Decode each extra param
1493    for ep in spec.extra {
1494        let mut decode = ep.decode.to_string();
1495        for (c_idx, _) in ep.c_layouts.iter().enumerate() {
1496            let placeholder = format!("raw_{}_{}", ep.java_name, c_idx);
1497            let var = raw_var_name(ep.java_name, c_idx);
1498            decode = decode.replace(&placeholder, &var);
1499        }
1500        writeln!(out, "            var {} = {};", ep.java_name, decode).ok();
1501    }
1502    if spec.has_is_header {
1503        writeln!(out, "            var goIsHeader = isHeader != 0;").ok();
1504    }
1505
1506    // Build call args
1507    let mut call_args = vec!["context".to_string()];
1508    for ep in spec.extra {
1509        call_args.push(ep.java_name.to_string());
1510    }
1511    if spec.has_is_header {
1512        call_args.push("goIsHeader".to_string());
1513    }
1514
1515    writeln!(
1516        out,
1517        "            var result = visitor.{}({});",
1518        spec.java_method,
1519        call_args.join(", ")
1520    )
1521    .ok();
1522    writeln!(out, "            return encodeVisitResult(result, outCustom, outLen);").ok();
1523    writeln!(out, "        }} catch (Throwable ignored) {{").ok();
1524    writeln!(out, "            return 0;").ok();
1525    writeln!(out, "        }}").ok();
1526    writeln!(out, "    }}").ok();
1527    writeln!(out).ok();
1528}
1529
1530fn raw_var_name(java_name: &str, c_idx: usize) -> String {
1531    // camelCase: "raw" + capitalize first letter of java_name + "_" + index
1532    // e.g. raw_text_0 -> rawText0, raw_cells_1 -> rawCells1
1533    let mut name = String::with_capacity(4 + java_name.len() + 2);
1534    name.push_str("raw");
1535    let mut chars = java_name.chars();
1536    if let Some(first) = chars.next() {
1537        for c in first.to_uppercase() {
1538            name.push(c);
1539        }
1540        name.push_str(chars.as_str());
1541    }
1542    name.push_str(&c_idx.to_string());
1543    name
1544}