1use alef_core::hash::{self, CommentStyle};
17use heck::ToSnakeCase;
18use std::fmt::Write;
19
20pub struct CallbackSpec {
25 pub c_field: &'static str,
27 pub cs_method: &'static str,
29 pub doc: &'static str,
31 pub extra: &'static [ExtraParam],
33 pub has_is_header: bool,
35}
36
37pub struct ExtraParam {
38 pub cs_name: &'static str,
40 pub cs_type: &'static str,
42 pub pinvoke_types: &'static [&'static str],
44 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
611pub 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
627pub 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 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#[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
704fn 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