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