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