1use alef_core::backend::GeneratedFile;
7use alef_core::config::{AlefConfig, Language};
8use alef_core::ir::{
9 ApiSurface, DefaultValue, EnumDef, ErrorDef, FieldDef, FunctionDef, MethodDef, PrimitiveType, TypeDef, TypeRef,
10};
11use heck::{ToPascalCase, ToShoutySnakeCase, ToSnakeCase, ToUpperCamelCase};
12use std::path::PathBuf;
13
14pub fn generate_docs(
23 api: &ApiSurface,
24 config: &AlefConfig,
25 languages: &[Language],
26 output_dir: &str,
27) -> anyhow::Result<Vec<GeneratedFile>> {
28 let mut files = Vec::new();
29
30 for &lang in languages {
31 files.push(generate_lang_doc(api, config, lang, output_dir)?);
32 }
33
34 files.push(generate_configuration_doc(api, config, output_dir)?);
35 files.push(generate_errors_doc(api, output_dir)?);
36
37 Ok(files)
38}
39
40fn generate_lang_doc(
45 api: &ApiSurface,
46 config: &AlefConfig,
47 lang: Language,
48 output_dir: &str,
49) -> anyhow::Result<GeneratedFile> {
50 let lang_display = lang_display_name(lang);
51 let version = &api.version;
52 let lang_slug = lang_slug(lang);
53
54 let mut out = String::new();
55
56 out.push_str(&format!("---\ntitle: \"{lang_display} API Reference\"\n---\n\n"));
58
59 out.push_str(&format!(
61 "# {lang_display} API Reference <span class=\"version-badge\">v{version}</span>\n\n"
62 ));
63
64 let public_fns: Vec<&FunctionDef> = api.functions.iter().collect();
66 if !public_fns.is_empty() {
67 out.push_str("## Functions\n\n");
68 for func in &public_fns {
69 out.push_str(&render_function(func, lang, config, api));
70 out.push_str("\n---\n\n");
71 }
72 }
73
74 let mut types_to_doc: Vec<&TypeDef> = api.types.iter().filter(|t| !is_update_type(&t.name)).collect();
78
79 types_to_doc.sort_by(|a, b| type_sort_key(&a.name).cmp(&type_sort_key(&b.name)));
81
82 if !types_to_doc.is_empty() {
83 out.push_str("## Types\n\n");
84 for ty in &types_to_doc {
85 out.push_str(&render_type(ty, lang, api));
86 out.push_str("\n---\n\n");
87 }
88 }
89
90 if !api.enums.is_empty() {
92 out.push_str("## Enums\n\n");
93 for en in &api.enums {
94 out.push_str(&render_enum(en, lang));
95 out.push_str("\n---\n\n");
96 }
97 }
98
99 if !api.errors.is_empty() {
101 out.push_str("## Errors\n\n");
102 for err in &api.errors {
103 out.push_str(&render_error(err, lang));
104 out.push_str("\n---\n\n");
105 }
106 }
107
108 let path = PathBuf::from(format!("{output_dir}/api-{lang_slug}.md"));
109
110 Ok(GeneratedFile {
111 path,
112 content: out,
113 generated_header: false,
114 })
115}
116
117fn render_function(func: &FunctionDef, lang: Language, _config: &AlefConfig, api: &ApiSurface) -> String {
122 let mut out = String::new();
123 let fn_name = func_name(&func.name, lang);
124
125 out.push_str(&format!("### {fn_name}()\n\n"));
126
127 let param_docs = extract_param_docs(&func.doc);
129
130 if !func.doc.is_empty() {
131 out.push_str(&clean_doc(&func.doc, lang));
132 out.push('\n');
133 out.push('\n');
134 }
135
136 out.push_str("**Signature:**\n\n");
138 let lang_code = lang_code_fence(lang);
139 let sig = render_function_signature(func, lang);
140 out.push_str(&format!("```{lang_code}\n{sig}\n```\n\n"));
141
142 if !func.params.is_empty() {
144 out.push_str("**Parameters:**\n\n");
145 out.push_str("| Name | Type | Required | Description |\n");
146 out.push_str("|------|------|----------|-------------|\n");
147 for param in &func.params {
148 let pname = field_name(¶m.name, lang);
149 let pty = doc_type_with_optional(¶m.ty, lang, param.optional);
150 let required = if param.optional { "No" } else { "Yes" };
151 let pdoc = param_docs
152 .get(param.name.as_str())
153 .map(|s| {
154 let s = s.replace('|', "\\|");
155 let s = s.replace("::", ".");
157 s.replace("ConversionOptions.default()", "default options")
158 })
159 .unwrap_or_default();
160 out.push_str(&format!("| `{pname}` | `{pty}` | {required} | {pdoc} |\n"));
161 }
162 out.push('\n');
163 }
164
165 let ret_ty = doc_type(&func.return_type, lang);
167 out.push_str(&format!("**Returns:** `{ret_ty}`"));
168 out.push('\n');
169 out.push('\n');
170
171 if let Some(err) = &func.error_type {
173 let error_phrase = format_error_phrase(err, lang);
174 out.push_str(&format!("**Errors:** {error_phrase}\n\n"));
175 }
176
177 let _ = api; out
179}
180
181fn render_function_signature(func: &FunctionDef, lang: Language) -> String {
182 match lang {
183 Language::Python => render_python_fn_sig(func),
184 Language::Node | Language::Wasm => render_typescript_fn_sig(func),
185 Language::Go => render_go_fn_sig(func),
186 Language::Java => render_java_fn_sig(func),
187 Language::Ruby => render_ruby_fn_sig(func),
188 Language::Ffi => render_c_fn_sig(func),
189 Language::Php => render_php_fn_sig(func),
190 Language::Elixir => render_elixir_fn_sig(func),
191 Language::R => render_r_fn_sig(func),
192 Language::Csharp => render_csharp_fn_sig(func),
193 }
194}
195
196fn render_python_fn_sig(func: &FunctionDef) -> String {
197 let name = func.name.to_snake_case();
198 let params: Vec<String> = func
199 .params
200 .iter()
201 .map(|p| {
202 let pname = p.name.to_snake_case();
203 let pty = doc_type(&p.ty, Language::Python);
204 if p.optional {
205 format!("{pname}: {pty} = None")
206 } else {
207 format!("{pname}: {pty}")
208 }
209 })
210 .collect();
211 let ret = doc_type(&func.return_type, Language::Python);
212 if func.is_async {
213 format!("async def {}({}) -> {}", name, params.join(", "), ret)
214 } else {
215 format!("def {}({}) -> {}", name, params.join(", "), ret)
216 }
217}
218
219fn render_typescript_fn_sig(func: &FunctionDef) -> String {
220 let name = to_camel_case(&func.name);
221 let params: Vec<String> = func
222 .params
223 .iter()
224 .map(|p| {
225 let pname = to_camel_case(&p.name);
226 let pty = doc_type(&p.ty, Language::Node);
227 if p.optional {
228 format!("{pname}?: {pty}")
229 } else {
230 format!("{pname}: {pty}")
231 }
232 })
233 .collect();
234 let ret = doc_type(&func.return_type, Language::Node);
235 if func.is_async {
236 format!("function {}({}): Promise<{}>", name, params.join(", "), ret)
237 } else {
238 format!("function {}({}): {}", name, params.join(", "), ret)
239 }
240}
241
242fn render_go_fn_sig(func: &FunctionDef) -> String {
243 let name = func.name.to_pascal_case();
244 let params: Vec<String> = func
245 .params
246 .iter()
247 .map(|p| {
248 let pname = to_camel_case(&p.name);
249 let pty = doc_type(&p.ty, Language::Go);
250 format!("{pname} {pty}")
251 })
252 .collect();
253 let ret = doc_type(&func.return_type, Language::Go);
254 if func.error_type.is_some() {
255 format!("func {}({}) ({}, error)", name, params.join(", "), ret)
256 } else {
257 format!("func {}({}) {}", name, params.join(", "), ret)
258 }
259}
260
261fn render_java_fn_sig(func: &FunctionDef) -> String {
262 let name = to_camel_case(&func.name);
263 let ret = doc_type(&func.return_type, Language::Java);
264 let params: Vec<String> = func
265 .params
266 .iter()
267 .map(|p| {
268 let pname = to_camel_case(&p.name);
269 let pty = doc_type(&p.ty, Language::Java);
270 format!("{pty} {pname}")
271 })
272 .collect();
273 let throws = func
274 .error_type
275 .as_ref()
276 .map(|e| format!(" throws {}", type_name(e, Language::Java)))
277 .unwrap_or_default();
278 format!("public static {} {}({}){}", ret, name, params.join(", "), throws)
279}
280
281fn render_ruby_fn_sig(func: &FunctionDef) -> String {
282 let name = func.name.to_snake_case();
283 let params: Vec<String> = func
284 .params
285 .iter()
286 .map(|p| {
287 let pname = p.name.to_snake_case();
288 if p.optional { format!("{pname}: nil") } else { pname }
289 })
290 .collect();
291 format!("def self.{}({})", name, params.join(", "))
292}
293
294fn render_c_fn_sig(func: &FunctionDef) -> String {
295 let prefix = "htm";
296 let name = format!("{}_{}", prefix, func.name.to_snake_case());
297 let ret = doc_type(&func.return_type, Language::Ffi);
298 let params: Vec<String> = func
299 .params
300 .iter()
301 .map(|p| {
302 let pname = p.name.to_snake_case();
303 let pty = doc_type(&p.ty, Language::Ffi);
304 format!("{pty} {pname}")
305 })
306 .collect();
307 format!("{}* {}({});", ret, name, params.join(", "))
308}
309
310fn render_php_fn_sig(func: &FunctionDef) -> String {
311 let name = to_camel_case(&func.name);
312 let params: Vec<String> = func
313 .params
314 .iter()
315 .map(|p| {
316 let pname = format!("${}", p.name.to_snake_case());
317 let pty = doc_type(&p.ty, Language::Php);
318 if p.optional {
319 format!("?{pty} {pname} = null")
320 } else {
321 format!("{pty} {pname}")
322 }
323 })
324 .collect();
325 let ret = doc_type(&func.return_type, Language::Php);
326 format!("public static function {}({}): {}", name, params.join(", "), ret)
327}
328
329fn render_elixir_fn_sig(func: &FunctionDef) -> String {
330 let name = func.name.to_snake_case();
331 let params: Vec<String> = func.params.iter().map(|p| p.name.to_snake_case()).collect();
332 format!(
333 "@spec {}({}) :: {{:ok, term()}} | {{:error, term()}}\ndef {}({})",
334 name,
335 params.join(", "),
336 name,
337 params.join(", ")
338 )
339}
340
341fn render_r_fn_sig(func: &FunctionDef) -> String {
342 let name = func.name.to_snake_case();
343 let params: Vec<String> = func
344 .params
345 .iter()
346 .map(|p| {
347 let pname = p.name.to_snake_case();
348 if p.optional { format!("{pname} = NULL") } else { pname }
349 })
350 .collect();
351 format!("{}({})", name, params.join(", "))
352}
353
354fn render_csharp_fn_sig(func: &FunctionDef) -> String {
355 let name = func.name.to_pascal_case();
356 let ret = doc_type(&func.return_type, Language::Csharp);
357 let params: Vec<String> = func
358 .params
359 .iter()
360 .map(|p| {
361 let pname = to_camel_case(&p.name);
362 let pty = doc_type(&p.ty, Language::Csharp);
363 if p.optional {
364 format!("{pty}? {pname} = null")
365 } else {
366 format!("{pty} {pname}")
367 }
368 })
369 .collect();
370 if func.is_async {
371 format!("public static async Task<{}> {}Async({})", ret, name, params.join(", "))
372 } else {
373 format!("public static {} {}({})", ret, name, params.join(", "))
374 }
375}
376
377fn render_type(ty: &TypeDef, lang: Language, api: &ApiSurface) -> String {
382 let mut out = String::new();
383 let tname = type_name(&ty.name, lang);
384
385 out.push_str(&format!("### {tname}\n\n"));
386
387 let doc = clean_doc(&ty.doc, lang);
388 if !doc.is_empty() {
389 out.push_str(&doc);
390 out.push('\n');
391 out.push('\n');
392 }
393
394 if !ty.is_opaque && !ty.fields.is_empty() {
396 out.push_str("| Field | Type | Default | Description |\n");
397 out.push_str("|-------|------|---------|-------------|\n");
398 for field in &ty.fields {
399 let fname = field_name(&field.name, lang);
400 let fty = doc_type_with_optional(&field.ty, lang, field.optional);
401 let fdefault = format_field_default(field, lang, api);
402 let fdoc = clean_doc_inline(&field.doc);
403 out.push_str(&format!("| `{fname}` | `{fty}` | {fdefault} | {fdoc} |\n"));
404 }
405 out.push('\n');
406 }
407
408 if !ty.methods.is_empty() {
410 let methods_heading = if lang == Language::Elixir {
411 "Functions"
412 } else {
413 "Methods"
414 };
415 out.push_str(&format!("#### {methods_heading}\n\n"));
416 for method in &ty.methods {
417 out.push_str(&render_method(method, &ty.name, lang));
418 }
419 }
420
421 out
422}
423
424fn render_method(method: &MethodDef, type_name_str: &str, lang: Language) -> String {
425 let mut out = String::new();
426 let mname = func_name(&method.name, lang);
427
428 out.push_str(&format!("##### {mname}()\n\n"));
429
430 let doc = clean_doc(&method.doc, lang);
431 if !doc.is_empty() {
432 out.push_str(&doc);
433 out.push('\n');
434 out.push('\n');
435 }
436
437 let lang_code = lang_code_fence(lang);
438 let sig = render_method_signature(method, type_name_str, lang);
439 out.push_str("**Signature:**\n\n");
440 out.push_str(&format!("```{lang_code}\n{sig}\n```\n\n"));
441
442 out
443}
444
445fn render_method_signature(method: &MethodDef, type_name_str: &str, lang: Language) -> String {
446 let name = func_name(&method.name, lang);
447 let ret = doc_type(&method.return_type, lang);
448
449 match lang {
450 Language::Python => {
451 let params: Vec<String> = method
452 .params
453 .iter()
454 .map(|p| {
455 let pname = field_name(&p.name, lang);
456 let pty = doc_type(&p.ty, lang);
457 format!("{pname}: {pty}")
458 })
459 .collect();
460 if method.is_static {
461 format!("@staticmethod\ndef {}({}) -> {}", name, params.join(", "), ret)
462 } else {
463 let mut all_params = vec!["self".to_string()];
464 all_params.extend(params);
465 format!("def {}({}) -> {}", name, all_params.join(", "), ret)
466 }
467 }
468 Language::Node | Language::Wasm => {
469 let params: Vec<String> = method
470 .params
471 .iter()
472 .map(|p| {
473 let pname = field_name(&p.name, lang);
474 let pty = doc_type(&p.ty, lang);
475 format!("{pname}: {pty}")
476 })
477 .collect();
478 if method.is_static {
479 format!("static {}({}): {}", name, params.join(", "), ret)
480 } else {
481 format!("{}({}): {}", name, params.join(", "), ret)
482 }
483 }
484 Language::Ruby => {
485 let params: Vec<String> = method.params.iter().map(|p| p.name.to_snake_case()).collect();
486 if method.is_static {
487 format!("def self.{}({})", name, params.join(", "))
488 } else {
489 format!("def {}({})", name, params.join(", "))
490 }
491 }
492 Language::Go => {
493 let go_receiver_type = type_name(type_name_str, Language::Go);
495 let receiver = format!("o *{go_receiver_type}");
496 let params: Vec<String> = method
497 .params
498 .iter()
499 .map(|p| {
500 let pname = to_camel_case(&p.name);
501 let pty = doc_type(&p.ty, lang);
502 format!("{pname} {pty}")
503 })
504 .collect();
505 if method.error_type.is_some() {
506 format!("func ({receiver}) {}({}) ({}, error)", name, params.join(", "), ret)
507 } else if ret.is_empty() {
508 format!("func ({receiver}) {}({})", name, params.join(", "))
509 } else {
510 format!("func ({receiver}) {}({}) {}", name, params.join(", "), ret)
511 }
512 }
513 Language::Java => {
514 let java_name = if name == "default" {
516 "defaultOptions".to_string()
517 } else {
518 name.clone()
519 };
520 let params: Vec<String> = method
521 .params
522 .iter()
523 .map(|p| {
524 let pname = to_camel_case(&p.name);
525 let pty = doc_type(&p.ty, lang);
526 format!("{pty} {pname}")
527 })
528 .collect();
529 let throws = method
530 .error_type
531 .as_ref()
532 .map(|e| format!(" throws {}", type_name(e, lang)))
533 .unwrap_or_default();
534 if method.is_static {
535 format!("public static {} {}({}){}", ret, java_name, params.join(", "), throws)
536 } else {
537 format!("public {} {}({}){}", ret, java_name, params.join(", "), throws)
538 }
539 }
540 Language::Csharp => {
541 let params: Vec<String> = method
542 .params
543 .iter()
544 .map(|p| {
545 let pname = to_camel_case(&p.name);
546 let pty = doc_type(&p.ty, lang);
547 format!("{pty} {pname}")
548 })
549 .collect();
550 format!("public {} {}({})", ret, name, params.join(", "))
551 }
552 Language::Php => {
553 let params: Vec<String> = method
554 .params
555 .iter()
556 .map(|p| {
557 let pname = format!("${}", p.name.to_snake_case());
558 let pty = doc_type(&p.ty, lang);
559 format!("{pty} {pname}")
560 })
561 .collect();
562 if method.is_static {
563 format!("public static function {}({}): {}", name, params.join(", "), ret)
564 } else {
565 format!("public function {}({}): {}", name, params.join(", "), ret)
566 }
567 }
568 Language::Elixir => {
569 let params: Vec<String> = method.params.iter().map(|p| p.name.to_snake_case()).collect();
570 format!("def {}({})", name, params.join(", "))
571 }
572 Language::R => {
573 let params: Vec<String> = method.params.iter().map(|p| p.name.to_snake_case()).collect();
574 format!("{}({})", name, params.join(", "))
575 }
576 Language::Ffi => {
577 let params: Vec<String> = method
578 .params
579 .iter()
580 .map(|p| {
581 let pname = p.name.to_snake_case();
582 let pty = doc_type(&p.ty, lang);
583 format!("{pty} {pname}")
584 })
585 .collect();
586 format!("{} {}({});", ret, name, params.join(", "))
587 }
588 }
589}
590
591fn render_enum(en: &EnumDef, lang: Language) -> String {
596 let mut out = String::new();
597 let ename = type_name(&en.name, lang);
598
599 out.push_str(&format!("### {ename}\n\n"));
600
601 let doc = clean_doc(&en.doc, lang);
602 if !doc.is_empty() {
603 out.push_str(&doc);
604 out.push('\n');
605 out.push('\n');
606 }
607
608 out.push_str("| Value | Description |\n");
609 out.push_str("|-------|-------------|\n");
610 for variant in &en.variants {
611 let vname = enum_variant_name(&variant.name, lang);
612 let vdoc = clean_doc_inline(&variant.doc);
613 out.push_str(&format!("| `{vname}` | {vdoc} |\n"));
614 }
615 out.push('\n');
616
617 out
618}
619
620fn render_error(err: &ErrorDef, lang: Language) -> String {
625 let mut out = String::new();
626 let ename = type_name(&err.name, lang);
627
628 out.push_str(&format!("### {ename}\n\n"));
629
630 let doc = clean_doc(&err.doc, lang);
631 if !doc.is_empty() {
632 out.push_str(&doc);
633 out.push('\n');
634 out.push('\n');
635 }
636
637 out.push_str("| Variant | Description |\n");
638 out.push_str("|---------|-------------|\n");
639 for variant in &err.variants {
640 let vname = enum_variant_name(&variant.name, lang);
641 let vdoc = if !variant.doc.is_empty() {
642 clean_doc_inline(&variant.doc)
643 } else if let Some(tmpl) = &variant.message_template {
644 clean_doc_inline(tmpl)
645 } else {
646 String::new()
647 };
648 out.push_str(&format!("| `{vname}` | {vdoc} |\n"));
649 }
650 out.push('\n');
651
652 out
653}
654
655fn generate_configuration_doc(
660 api: &ApiSurface,
661 _config: &AlefConfig,
662 output_dir: &str,
663) -> anyhow::Result<GeneratedFile> {
664 let mut out = String::new();
665
666 out.push_str("---\ntitle: \"Configuration Reference\"\n---\n\n");
667 out.push_str("# Configuration Reference\n\n");
668 out.push_str("This page documents all configuration types and their defaults across all languages.\n\n");
669
670 let config_types: Vec<&TypeDef> = api
672 .types
673 .iter()
674 .filter(|t| t.name.ends_with("Options") && !t.is_opaque && !is_update_type(&t.name))
675 .collect();
676
677 for ty in config_types {
678 out.push_str(&format!("## {}\n\n", ty.name));
679 let doc = clean_doc(&ty.doc, Language::Python);
680 if !doc.is_empty() {
681 out.push_str(&doc);
682 out.push('\n');
683 out.push('\n');
684 }
685
686 if !ty.fields.is_empty() {
687 out.push_str("| Field | Type | Default | Description |\n");
688 out.push_str("|-------|------|---------|-------------|\n");
689 for field in &ty.fields {
690 let fty = doc_type_with_optional(&field.ty, Language::Python, field.optional);
691 let fdefault = format_field_default(field, Language::Python, api);
692 let fdoc = clean_doc_inline(&field.doc);
693 out.push_str(&format!("| `{}` | `{}` | {} | {} |\n", field.name, fty, fdefault, fdoc));
694 }
695 out.push('\n');
696 }
697
698 out.push_str("---\n\n");
699 }
700
701 Ok(GeneratedFile {
702 path: PathBuf::from(format!("{output_dir}/configuration.md")),
703 content: out,
704 generated_header: false,
705 })
706}
707
708fn generate_errors_doc(api: &ApiSurface, output_dir: &str) -> anyhow::Result<GeneratedFile> {
713 let mut out = String::new();
714
715 out.push_str("---\ntitle: \"Error Reference\"\n---\n\n");
716 out.push_str("# Error Reference\n\n");
717 out.push_str("All error types thrown by the library across all languages.\n\n");
718
719 for err in &api.errors {
720 out.push_str(&format!("## {}\n\n", err.name));
721
722 let doc = clean_doc(&err.doc, Language::Python);
723 if !doc.is_empty() {
724 out.push_str(&doc);
725 out.push('\n');
726 out.push('\n');
727 }
728
729 out.push_str("| Variant | Message | Description |\n");
730 out.push_str("|---------|---------|-------------|\n");
731 for variant in &err.variants {
732 let tmpl = variant.message_template.as_deref().unwrap_or("").replace('|', "\\|");
733 let vdoc = clean_doc_inline(&variant.doc);
734 out.push_str(&format!("| `{}` | {} | {} |\n", variant.name, tmpl, vdoc));
735 }
736 out.push('\n');
737 out.push_str("---\n\n");
738 }
739
740 Ok(GeneratedFile {
741 path: PathBuf::from(format!("{output_dir}/errors.md")),
742 content: out,
743 generated_header: false,
744 })
745}
746
747pub fn doc_type(ty: &TypeRef, lang: Language) -> String {
753 match ty {
754 TypeRef::String | TypeRef::Char => match lang {
755 Language::Python => "str".to_string(),
756 Language::Node | Language::Wasm => "string".to_string(),
757 Language::Go => "string".to_string(),
758 Language::Java => "String".to_string(),
759 Language::Csharp => "string".to_string(),
760 Language::Ruby => "String".to_string(),
761 Language::Php => "string".to_string(),
762 Language::Elixir => "String.t()".to_string(),
763 Language::R => "character".to_string(),
764 Language::Ffi => "const char*".to_string(),
765 },
766 TypeRef::Bytes => match lang {
767 Language::Python => "bytes".to_string(),
768 Language::Node | Language::Wasm => "Buffer".to_string(),
769 Language::Go => "[]byte".to_string(),
770 Language::Java => "byte[]".to_string(),
771 Language::Csharp => "byte[]".to_string(),
772 Language::Ruby => "String".to_string(),
773 Language::Php => "string".to_string(),
774 Language::Elixir => "binary()".to_string(),
775 Language::R => "raw".to_string(),
776 Language::Ffi => "const uint8_t*".to_string(),
777 },
778 TypeRef::Primitive(p) => doc_primitive(p, lang),
779 TypeRef::Optional(inner) => {
780 let inner_ty = doc_type(inner, lang);
781 match lang {
782 Language::Python => format!("{inner_ty} | None"),
783 Language::Node | Language::Wasm => format!("{inner_ty} | null"),
784 Language::Go => format!("*{inner_ty}"),
785 Language::Java => format!("Optional<{inner_ty}>"),
786 Language::Csharp => format!("{inner_ty}?"),
787 Language::Ruby => format!("{inner_ty}?"),
788 Language::Php => format!("?{inner_ty}"),
789 Language::Elixir => format!("{inner_ty} | nil"),
790 Language::R => format!("{inner_ty} or NULL"),
791 Language::Ffi => format!("{inner_ty}*"),
792 }
793 }
794 TypeRef::Vec(inner) => {
795 match lang {
796 Language::Java => {
797 let inner_ty = java_boxed_type(inner);
799 format!("List<{inner_ty}>")
800 }
801 Language::Csharp => {
802 let inner_ty = doc_type(inner, lang);
803 format!("List<{inner_ty}>")
804 }
805 _ => {
806 let inner_ty = doc_type(inner, lang);
807 match lang {
808 Language::Python => format!("list[{inner_ty}]"),
809 Language::Node | Language::Wasm => format!("Array<{inner_ty}>"),
810 Language::Go => format!("[]{inner_ty}"),
811 Language::Ruby => format!("Array<{inner_ty}>"),
812 Language::Php => format!("array<{inner_ty}>"),
813 Language::Elixir => format!("list({inner_ty})"),
814 Language::R => "list".to_string(),
815 Language::Ffi => format!("{inner_ty}*"),
816 Language::Java | Language::Csharp => unreachable!(),
817 }
818 }
819 }
820 }
821 TypeRef::Map(k, v) => {
822 if lang == Language::Java {
823 let kty = java_boxed_type(k);
825 let vty = java_boxed_type(v);
826 return format!("Map<{kty}, {vty}>");
827 }
828 let kty = doc_type(k, lang);
829 let vty = doc_type(v, lang);
830 match lang {
831 Language::Python => format!("dict[{kty}, {vty}]"),
832 Language::Node | Language::Wasm => format!("Record<{kty}, {vty}>"),
833 Language::Go => format!("map[{kty}]{vty}"),
834 Language::Java => format!("Map<{kty}, {vty}>"),
835 Language::Csharp => format!("Dictionary<{kty}, {vty}>"),
836 Language::Ruby => format!("Hash{{{kty}=>{vty}}}"),
837 Language::Php => format!("array<{kty}, {vty}>"),
838 Language::Elixir => "map()".to_string(),
839 Language::R => "list".to_string(),
840 Language::Ffi => "void*".to_string(),
841 }
842 }
843 TypeRef::Named(name) => type_name(name, lang),
844 TypeRef::Path => match lang {
845 Language::Python => "str".to_string(),
846 Language::Node | Language::Wasm => "string".to_string(),
847 Language::Go => "string".to_string(),
848 Language::Java => "String".to_string(),
849 Language::Csharp => "string".to_string(),
850 Language::Ruby => "String".to_string(),
851 Language::Php => "string".to_string(),
852 Language::Elixir => "String.t()".to_string(),
853 Language::R => "character".to_string(),
854 Language::Ffi => "const char*".to_string(),
855 },
856 TypeRef::Unit => match lang {
857 Language::Python => "None".to_string(),
858 Language::Node | Language::Wasm => "void".to_string(),
859 Language::Go => "".to_string(),
860 Language::Java => "void".to_string(),
861 Language::Csharp => "void".to_string(),
862 Language::Ruby => "nil".to_string(),
863 Language::Php => "void".to_string(),
864 Language::Elixir => ":ok".to_string(),
865 Language::R => "NULL".to_string(),
866 Language::Ffi => "void".to_string(),
867 },
868 TypeRef::Json => match lang {
869 Language::Python => "Any".to_string(),
870 Language::Node | Language::Wasm => "unknown".to_string(),
871 Language::Go => "interface{}".to_string(),
872 Language::Java => "Object".to_string(),
873 Language::Csharp => "object".to_string(),
874 Language::Ruby => "Object".to_string(),
875 Language::Php => "mixed".to_string(),
876 Language::Elixir => "term()".to_string(),
877 Language::R => "list".to_string(),
878 Language::Ffi => "void*".to_string(),
879 },
880 TypeRef::Duration => match lang {
881 Language::Python => "float".to_string(),
882 Language::Node | Language::Wasm => "number".to_string(),
883 Language::Go => "time.Duration".to_string(),
884 Language::Java => "Duration".to_string(),
885 Language::Csharp => "TimeSpan".to_string(),
886 Language::Ruby => "Float".to_string(),
887 Language::Php => "float".to_string(),
888 Language::Elixir => "integer()".to_string(),
889 Language::R => "numeric".to_string(),
890 Language::Ffi => "uint64_t".to_string(),
891 },
892 }
893}
894
895fn doc_primitive(p: &PrimitiveType, lang: Language) -> String {
896 match lang {
897 Language::Python => match p {
898 PrimitiveType::Bool => "bool".to_string(),
899 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
900 _ => "int".to_string(),
901 },
902 Language::Node | Language::Wasm => match p {
903 PrimitiveType::Bool => "boolean".to_string(),
904 _ => "number".to_string(),
905 },
906 Language::Go => match p {
907 PrimitiveType::Bool => "bool".to_string(),
908 PrimitiveType::U8 => "uint8".to_string(),
909 PrimitiveType::U16 => "uint16".to_string(),
910 PrimitiveType::U32 => "uint32".to_string(),
911 PrimitiveType::U64 => "uint64".to_string(),
912 PrimitiveType::I8 => "int8".to_string(),
913 PrimitiveType::I16 => "int16".to_string(),
914 PrimitiveType::I32 => "int32".to_string(),
915 PrimitiveType::I64 => "int64".to_string(),
916 PrimitiveType::F32 => "float32".to_string(),
917 PrimitiveType::F64 => "float64".to_string(),
918 PrimitiveType::Usize | PrimitiveType::Isize => "int".to_string(),
919 },
920 Language::Java => match p {
921 PrimitiveType::Bool => "boolean".to_string(),
922 PrimitiveType::U8 | PrimitiveType::I8 => "byte".to_string(),
923 PrimitiveType::U16 | PrimitiveType::I16 => "short".to_string(),
924 PrimitiveType::U32 | PrimitiveType::I32 => "int".to_string(),
925 PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "long".to_string(),
926 PrimitiveType::F32 => "float".to_string(),
927 PrimitiveType::F64 => "double".to_string(),
928 },
929 Language::Csharp => match p {
930 PrimitiveType::Bool => "bool".to_string(),
931 PrimitiveType::U8 => "byte".to_string(),
932 PrimitiveType::U16 => "ushort".to_string(),
933 PrimitiveType::U32 => "uint".to_string(),
934 PrimitiveType::U64 => "ulong".to_string(),
935 PrimitiveType::I8 => "sbyte".to_string(),
936 PrimitiveType::I16 => "short".to_string(),
937 PrimitiveType::I32 => "int".to_string(),
938 PrimitiveType::I64 => "long".to_string(),
939 PrimitiveType::Usize => "nuint".to_string(),
940 PrimitiveType::Isize => "nint".to_string(),
941 PrimitiveType::F32 => "float".to_string(),
942 PrimitiveType::F64 => "double".to_string(),
943 },
944 Language::Ruby => match p {
945 PrimitiveType::Bool => "Boolean".to_string(),
946 PrimitiveType::F32 | PrimitiveType::F64 => "Float".to_string(),
947 _ => "Integer".to_string(),
948 },
949 Language::Php => match p {
950 PrimitiveType::Bool => "bool".to_string(),
951 PrimitiveType::F32 | PrimitiveType::F64 => "float".to_string(),
952 _ => "int".to_string(),
953 },
954 Language::Elixir => match p {
955 PrimitiveType::Bool => "boolean()".to_string(),
956 PrimitiveType::F32 | PrimitiveType::F64 => "float()".to_string(),
957 _ => "integer()".to_string(),
958 },
959 Language::R => match p {
960 PrimitiveType::Bool => "logical".to_string(),
961 PrimitiveType::F32 | PrimitiveType::F64 => "numeric".to_string(),
962 _ => "integer".to_string(),
963 },
964 Language::Ffi => match p {
965 PrimitiveType::Bool => "bool".to_string(),
966 PrimitiveType::U8 => "uint8_t".to_string(),
967 PrimitiveType::U16 => "uint16_t".to_string(),
968 PrimitiveType::U32 => "uint32_t".to_string(),
969 PrimitiveType::U64 => "uint64_t".to_string(),
970 PrimitiveType::I8 => "int8_t".to_string(),
971 PrimitiveType::I16 => "int16_t".to_string(),
972 PrimitiveType::I32 => "int32_t".to_string(),
973 PrimitiveType::I64 => "int64_t".to_string(),
974 PrimitiveType::Usize => "uintptr_t".to_string(),
975 PrimitiveType::Isize => "intptr_t".to_string(),
976 PrimitiveType::F32 => "float".to_string(),
977 PrimitiveType::F64 => "double".to_string(),
978 },
979 }
980}
981
982fn java_boxed_type(ty: &TypeRef) -> String {
987 match ty {
988 TypeRef::Primitive(p) => match p {
989 PrimitiveType::Bool => "Boolean".to_string(),
990 PrimitiveType::U8 | PrimitiveType::I8 => "Byte".to_string(),
991 PrimitiveType::U16 | PrimitiveType::I16 => "Short".to_string(),
992 PrimitiveType::U32 | PrimitiveType::I32 => "Integer".to_string(),
993 PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "Long".to_string(),
994 PrimitiveType::F32 => "Float".to_string(),
995 PrimitiveType::F64 => "Double".to_string(),
996 },
997 _ => doc_type(ty, Language::Java),
999 }
1000}
1001
1002fn lang_display_name(lang: Language) -> &'static str {
1008 match lang {
1009 Language::Python => "Python",
1010 Language::Node => "TypeScript",
1011 Language::Ruby => "Ruby",
1012 Language::Php => "PHP",
1013 Language::Elixir => "Elixir",
1014 Language::Go => "Go",
1015 Language::Java => "Java",
1016 Language::Csharp => "C#",
1017 Language::Ffi => "C",
1018 Language::Wasm => "WebAssembly",
1019 Language::R => "R",
1020 }
1021}
1022
1023fn lang_slug(lang: Language) -> &'static str {
1025 match lang {
1026 Language::Python => "python",
1027 Language::Node => "typescript",
1028 Language::Ruby => "ruby",
1029 Language::Php => "php",
1030 Language::Elixir => "elixir",
1031 Language::Go => "go",
1032 Language::Java => "java",
1033 Language::Csharp => "csharp",
1034 Language::Ffi => "c",
1035 Language::Wasm => "wasm",
1036 Language::R => "r",
1037 }
1038}
1039
1040fn lang_code_fence(lang: Language) -> &'static str {
1042 match lang {
1043 Language::Python => "python",
1044 Language::Node | Language::Wasm => "typescript",
1045 Language::Ruby => "ruby",
1046 Language::Php => "php",
1047 Language::Elixir => "elixir",
1048 Language::Go => "go",
1049 Language::Java => "java",
1050 Language::Csharp => "csharp",
1051 Language::Ffi => "c",
1052 Language::R => "r",
1053 }
1054}
1055
1056fn type_name(name: &str, lang: Language) -> String {
1058 let short = name.rsplit("::").next().unwrap_or(name);
1060 match lang {
1061 Language::Python
1062 | Language::Node
1063 | Language::Wasm
1064 | Language::Ruby
1065 | Language::Go
1066 | Language::Java
1067 | Language::Csharp
1068 | Language::Php
1069 | Language::Elixir
1070 | Language::R => short.to_pascal_case(),
1071 Language::Ffi => {
1072 format!("HTM{}", short.to_pascal_case())
1074 }
1075 }
1076}
1077
1078fn func_name(name: &str, lang: Language) -> String {
1080 let base = match lang {
1081 Language::Python | Language::Ruby | Language::Elixir | Language::R => name.to_snake_case(),
1082 Language::Node | Language::Wasm | Language::Java | Language::Php => to_camel_case(name),
1083 Language::Csharp | Language::Go => name.to_pascal_case(),
1084 Language::Ffi => format!("htm_{}", name.to_snake_case()),
1085 };
1086 match (lang, base.as_str()) {
1088 (Language::Java, "default") => "defaultOptions".to_string(),
1089 (Language::Csharp, "Default") => "CreateDefault".to_string(),
1090 _ => base,
1091 }
1092}
1093
1094fn field_name(name: &str, lang: Language) -> String {
1096 match lang {
1097 Language::Python | Language::Ruby | Language::Elixir | Language::R | Language::Ffi => name.to_snake_case(),
1098 Language::Go | Language::Csharp => name.to_pascal_case(),
1100 Language::Node | Language::Wasm | Language::Java | Language::Php => to_camel_case(name),
1101 }
1102}
1103
1104fn enum_variant_name(name: &str, lang: Language) -> String {
1106 match lang {
1107 Language::Python => {
1108 name.to_snake_case().to_uppercase()
1110 }
1111 Language::Ruby | Language::Elixir => {
1112 name.to_snake_case()
1114 }
1115 Language::Go | Language::Node | Language::Wasm | Language::Java | Language::Csharp | Language::Php => {
1116 name.to_pascal_case()
1117 }
1118 Language::R => name.to_snake_case(),
1119 Language::Ffi => format!("HTM_{}", name.to_snake_case().to_uppercase()),
1120 }
1121}
1122
1123fn to_camel_case(s: &str) -> String {
1125 let pascal = s.to_upper_camel_case();
1126 let mut chars = pascal.chars();
1127 match chars.next() {
1128 None => String::new(),
1129 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
1130 }
1131}
1132
1133fn format_field_default(field: &FieldDef, lang: Language, api: &ApiSurface) -> String {
1138 if let Some(typed) = &field.typed_default {
1139 return format_typed_default(typed, &field.ty, lang, api);
1140 }
1141 if let Some(raw) = &field.default {
1142 if !raw.is_empty() {
1143 return format!("`{raw}`");
1144 }
1145 }
1146 if field.optional {
1147 return match lang {
1148 Language::Python => "`None`".to_string(),
1149 Language::Node | Language::Wasm => "`null`".to_string(),
1150 Language::Go => "`nil`".to_string(),
1151 Language::Java => "`null`".to_string(),
1152 Language::Csharp => "`null`".to_string(),
1153 Language::Ruby => "`nil`".to_string(),
1154 Language::Php => "`null`".to_string(),
1155 Language::Elixir => "`nil`".to_string(),
1156 Language::R => "`NULL`".to_string(),
1157 Language::Ffi => "`NULL`".to_string(),
1158 };
1159 }
1160 "—".to_string()
1161}
1162
1163fn format_typed_default(val: &DefaultValue, field_ty: &TypeRef, lang: Language, api: &ApiSurface) -> String {
1164 match val {
1165 DefaultValue::BoolLiteral(b) => match lang {
1166 Language::Python => format!("`{}`", if *b { "True" } else { "False" }),
1167 _ => format!("`{b}`"),
1168 },
1169 DefaultValue::StringLiteral(s) => format!("`\"{s}\"`"),
1170 DefaultValue::IntLiteral(n) => format!("`{n}`"),
1171 DefaultValue::FloatLiteral(f) => format!("`{f}`"),
1172 DefaultValue::EnumVariant(v) => {
1173 let parts: Vec<&str> = v.splitn(2, "::").collect();
1175 if parts.len() == 2 {
1176 let enum_type = type_name(parts[0], lang);
1177 let variant = enum_variant_name(parts[1], lang);
1178 format!("`{}`", format_enum_variant_ref(&enum_type, &variant, lang))
1179 } else {
1180 let enum_type_name_str = match field_ty {
1182 TypeRef::Named(n) => Some(n.as_str()),
1183 TypeRef::Optional(inner) => {
1184 if let TypeRef::Named(n) = inner.as_ref() {
1185 Some(n.as_str())
1186 } else {
1187 None
1188 }
1189 }
1190 _ => None,
1191 };
1192 if let Some(type_str) = enum_type_name_str {
1193 let etype = type_name(type_str, lang);
1194 let variant = enum_variant_name(v, lang);
1195 format!("`{}`", format_enum_variant_ref(&etype, &variant, lang))
1196 } else {
1197 format!("`{v}`")
1198 }
1199 }
1200 }
1201 DefaultValue::Empty => {
1202 if let TypeRef::Named(type_name_str) = field_ty {
1204 if let Some(enum_def) = api.enums.iter().find(|e| &e.name == type_name_str) {
1205 let variant = enum_def
1206 .variants
1207 .iter()
1208 .find(|v| v.is_default)
1209 .or_else(|| enum_def.variants.first());
1210 if let Some(v) = variant {
1211 let etype = type_name(type_name_str, lang);
1212 let vname = enum_variant_name(&v.name, lang);
1213 return format!("`{}`", format_enum_variant_ref(&etype, &vname, lang));
1214 }
1215 }
1216 }
1217 let inner_ty = match field_ty {
1220 TypeRef::Optional(inner) => inner.as_ref(),
1221 other => other,
1222 };
1223 if matches!(inner_ty, TypeRef::Vec(_)) {
1224 return match lang {
1225 Language::Python => "`[]`".to_string(),
1226 Language::Node | Language::Wasm => "`[]`".to_string(),
1227 Language::Go => "`nil`".to_string(),
1228 Language::Java => "`Collections.emptyList()`".to_string(),
1229 Language::Csharp => {
1230 let elem_ty = if let TypeRef::Vec(elem) = inner_ty {
1231 doc_type(elem, lang)
1232 } else {
1233 String::new()
1234 };
1235 format!("`new List<{elem_ty}>()`")
1236 }
1237 Language::Ruby | Language::Elixir => "`[]`".to_string(),
1238 Language::Php => "`[]`".to_string(),
1239 Language::Ffi => "`NULL`".to_string(),
1240 Language::R => "`list()`".to_string(),
1241 };
1242 }
1243 if matches!(inner_ty, TypeRef::Map(_, _)) {
1244 return match lang {
1245 Language::Python | Language::Ruby | Language::Php => "`{}`".to_string(),
1246 Language::Node | Language::Wasm => "`{}`".to_string(),
1247 Language::Go => "`nil`".to_string(),
1248 Language::Elixir => "`%{}`".to_string(),
1249 Language::Java => "`Collections.emptyMap()`".to_string(),
1250 Language::Csharp => {
1251 if let TypeRef::Map(k, v) = inner_ty {
1252 let kty = doc_type(k, lang);
1253 let vty = doc_type(v, lang);
1254 format!("`new Dictionary<{kty}, {vty}>()`")
1255 } else {
1256 "`new Dictionary<>()`".to_string()
1257 }
1258 }
1259 Language::Ffi => "`NULL`".to_string(),
1260 Language::R => "`list()`".to_string(),
1261 };
1262 }
1263 match lang {
1265 Language::Python => "`None`".to_string(),
1266 Language::Node | Language::Wasm => "`null`".to_string(),
1267 Language::Go => "`nil`".to_string(),
1268 Language::Java => "`null`".to_string(),
1269 Language::Csharp => "`null`".to_string(),
1270 Language::Ruby => "`nil`".to_string(),
1271 Language::Php => "`null`".to_string(),
1272 Language::Elixir => "`nil`".to_string(),
1273 Language::R => "`NULL`".to_string(),
1274 Language::Ffi => "`NULL`".to_string(),
1275 }
1276 }
1277 DefaultValue::None => match lang {
1278 Language::Python => "`None`".to_string(),
1279 Language::Node | Language::Wasm => "`null`".to_string(),
1280 Language::Go => "`nil`".to_string(),
1281 Language::Java => "`null`".to_string(),
1282 Language::Csharp => "`null`".to_string(),
1283 Language::Ruby => "`nil`".to_string(),
1284 Language::Php => "`null`".to_string(),
1285 Language::Elixir => "`nil`".to_string(),
1286 Language::R => "`NULL`".to_string(),
1287 Language::Ffi => "`NULL`".to_string(),
1288 },
1289 }
1290}
1291
1292fn format_enum_variant_ref(enum_type: &str, variant: &str, lang: Language) -> String {
1294 match lang {
1295 Language::Python => format!("{enum_type}.{variant}"),
1296 Language::Node | Language::Wasm => format!("{enum_type}.{variant}"),
1297 Language::Go => format!("{enum_type}.{variant}"),
1298 Language::Java => format!("{enum_type}.{variant}"),
1299 Language::Csharp => format!("{enum_type}.{variant}"),
1300 Language::Ruby => format!(":{variant}"),
1301 Language::Php => format!("{enum_type}::{variant}"),
1302 Language::Elixir => format!(":{variant}"),
1303 Language::R => format!("\"{variant}\""),
1304 Language::Ffi => format!("HTM_{}", variant.to_shouty_snake_case()),
1305 }
1306}
1307
1308fn format_error_phrase(error_type: &str, lang: Language) -> String {
1310 let short = error_type.rsplit("::").next().unwrap_or(error_type);
1311 match lang {
1312 Language::Python => {
1313 let ename = short.to_pascal_case();
1314 format!("Raises `{ename}`.")
1315 }
1316 Language::Go => "Returns `error`.".to_string(),
1317 Language::Java => {
1318 let ename = short.to_pascal_case();
1319 format!("Throws `{ename}`.")
1320 }
1321 Language::Node | Language::Wasm => {
1322 let ename = short.to_pascal_case();
1323 format!("Throws `{ename}`.")
1324 }
1325 Language::Ruby => {
1326 let ename = short.to_pascal_case();
1327 format!("Raises `{ename}`.")
1328 }
1329 Language::Csharp => {
1330 let ename = short.to_pascal_case();
1331 format!("Throws `{ename}`.")
1332 }
1333 Language::Elixir => "Returns `{:error, reason}`".to_string(),
1334 Language::Php => {
1335 let ename = short.to_pascal_case();
1336 format!("Throws `{ename}`.")
1337 }
1338 Language::Ffi => "Returns `NULL` on error.".to_string(),
1339 Language::R => "Stops with error message.".to_string(),
1340 }
1341}
1342
1343fn doc_type_with_optional(ty: &TypeRef, lang: Language, optional: bool) -> String {
1345 if optional && !matches!(ty, TypeRef::Optional(_)) {
1347 let inner = doc_type(ty, lang);
1348 return match lang {
1349 Language::Python => format!("{inner} | None"),
1350 Language::Node | Language::Wasm => format!("{inner} | null"),
1351 Language::Go => format!("*{inner}"),
1352 Language::Java => format!("Optional<{inner}>"),
1353 Language::Csharp => format!("{inner}?"),
1354 Language::Ruby => format!("{inner}?"),
1355 Language::Php => format!("?{inner}"),
1356 Language::Elixir => format!("{inner} | nil"),
1357 Language::R => format!("{inner} or NULL"),
1358 Language::Ffi => format!("{inner}*"),
1359 };
1360 }
1361 doc_type(ty, lang)
1362}
1363
1364const RUST_ONLY_SECTIONS: &[&str] = &["example", "examples", "arguments", "fields"];
1370
1371fn clean_doc(doc: &str, lang: Language) -> String {
1380 if doc.is_empty() {
1381 return String::new();
1382 }
1383
1384 let doc = strip_rust_sections(doc);
1386
1387 let doc = rust_links_to_plain(&doc);
1389
1390 let doc = convert_doc_headings_to_bold(&doc);
1393
1394 let doc = rust_paths_to_dot_notation(&doc, lang);
1396
1397 let doc = replace_rust_terminology(&doc, lang);
1399
1400 doc.trim().to_string()
1401}
1402
1403fn convert_doc_headings_to_bold(doc: &str) -> String {
1405 let mut out = String::new();
1406 let mut in_code_block = false;
1407 for line in doc.lines() {
1408 if line.trim_start().starts_with("```") {
1409 in_code_block = !in_code_block;
1410 out.push_str(line);
1411 out.push('\n');
1412 continue;
1413 }
1414 if !in_code_block && line.starts_with('#') {
1415 let heading_text = line.trim_start_matches('#').trim();
1416 let lower = heading_text.to_lowercase();
1417 if lower == "errors"
1418 || lower == "returns"
1419 || lower == "panics"
1420 || lower == "safety"
1421 || lower == "notes"
1422 || lower == "note"
1423 {
1424 out.push_str(&format!("**{heading_text}:**\n"));
1425 continue;
1426 }
1427 }
1428 out.push_str(line);
1429 out.push('\n');
1430 }
1431 out
1432}
1433
1434fn replace_rust_terminology(doc: &str, lang: Language) -> String {
1436 let doc = doc
1437 .replace("this crate", "this library")
1438 .replace("in this crate", "in this library")
1439 .replace("for this crate", "for this library")
1440 .replace(
1441 "Panic caught during conversion to prevent unwinding across FFI boundaries",
1442 "Internal error caught during conversion",
1443 );
1444
1445 let none_replacement = match lang {
1447 Language::Go | Language::Ruby | Language::Elixir => "`nil`",
1448 Language::Java | Language::Node | Language::Wasm | Language::Csharp | Language::Php => "`null`",
1449 Language::Python => "`None`", Language::R | Language::Ffi => "`NULL`",
1451 };
1452 let doc = doc.replace("`None`", none_replacement);
1453
1454 if lang == Language::Python {
1456 let doc = doc.replace("`true`", "`True`").replace("`false`", "`False`");
1457 return doc;
1458 }
1459
1460 doc
1461}
1462
1463fn rust_paths_to_dot_notation(doc: &str, lang: Language) -> String {
1467 let sep = if lang == Language::Php { "::" } else { "." };
1469 let mut out = String::new();
1470 let mut in_code_block = false;
1471 for line in doc.lines() {
1472 if line.trim_start().starts_with("```") {
1473 in_code_block = !in_code_block;
1474 out.push_str(line);
1475 out.push('\n');
1476 continue;
1477 }
1478 if in_code_block {
1479 out.push_str(line);
1480 out.push('\n');
1481 continue;
1482 }
1483 let line = line
1486 .replace("Default::default()", "the default constructor")
1487 .replace("::", sep);
1488 out.push_str(&line);
1489 out.push('\n');
1490 }
1491 out
1492}
1493
1494fn clean_doc_inline(doc: &str) -> String {
1496 if doc.is_empty() {
1497 return String::new();
1498 }
1499 let cleaned = clean_doc(doc, Language::Python);
1501 cleaned
1503 .lines()
1504 .map(str::trim)
1505 .filter(|l| !l.is_empty())
1506 .collect::<Vec<_>>()
1507 .join(" ")
1508 .replace('|', "\\|")
1510}
1511
1512fn strip_rust_sections(doc: &str) -> String {
1517 let mut out = String::new();
1518 let mut skip_section = false;
1519 let mut in_code_block = false;
1520 let mut code_block_buf = String::new();
1521
1522 for line in doc.lines() {
1523 if line.trim_start().starts_with("```") {
1525 if in_code_block {
1526 in_code_block = false;
1528 if !skip_section && !is_rust_code_block(&code_block_buf) {
1529 out.push_str(&code_block_buf);
1530 out.push_str(line);
1531 out.push('\n');
1532 }
1533 code_block_buf.clear();
1534 continue;
1535 } else {
1536 in_code_block = true;
1537 if !skip_section {
1538 code_block_buf.push_str(line);
1539 code_block_buf.push('\n');
1540 }
1541 continue;
1542 }
1543 }
1544
1545 if in_code_block {
1546 if !skip_section {
1547 code_block_buf.push_str(line);
1548 code_block_buf.push('\n');
1549 }
1550 continue;
1551 }
1552
1553 if line.starts_with('#') {
1555 let header_text = line.trim_start_matches('#').trim().to_lowercase();
1556 if RUST_ONLY_SECTIONS.contains(&header_text.as_str()) {
1557 skip_section = true;
1558 continue;
1559 } else {
1560 skip_section = false;
1562 }
1563 }
1564
1565 if skip_section {
1566 let trimmed = line.trim();
1568 let is_section_content = trimmed.is_empty()
1569 || trimmed.starts_with('*')
1570 || trimmed.starts_with('-')
1571 || trimmed.starts_with('+')
1572 || trimmed.starts_with(" ") || trimmed.starts_with('\t');
1574 if is_section_content {
1575 continue;
1576 }
1577 skip_section = false;
1579 }
1580
1581 if is_rust_specific_line(line) {
1583 continue;
1584 }
1585
1586 out.push_str(line);
1587 out.push('\n');
1588 }
1589
1590 out
1591}
1592
1593fn is_rust_code_block(content: &str) -> bool {
1595 let first_line = content.lines().next().unwrap_or("");
1597 let fence_lang = first_line.trim_start_matches('`').trim().to_lowercase();
1598 if matches!(fence_lang.as_str(), "rust" | "rust,no_run" | "rust,ignore" | "") {
1599 for line in content.lines().skip(1) {
1601 if line.starts_with("use ")
1602 || line.contains("unwrap()")
1603 || line.contains("assert!")
1604 || line.contains("assert_eq!")
1605 || line.contains("Vec::new()")
1606 || line.contains("Default::default()")
1607 || line.contains("::new(")
1608 || line.contains(".to_string()")
1609 || line.contains("html_to_markdown")
1610 || line.contains("r#\"")
1611 {
1612 return true;
1613 }
1614 }
1615 }
1616 false
1617}
1618
1619fn is_rust_specific_line(line: &str) -> bool {
1621 let trimmed = line.trim();
1622 trimmed.starts_with("# use ") || trimmed.starts_with("use ") && trimmed.ends_with(';')
1623}
1624
1625fn extract_param_docs(doc: &str) -> std::collections::HashMap<String, String> {
1630 let mut map = std::collections::HashMap::new();
1631 let mut in_args = false;
1632 let mut in_code_block = false;
1633
1634 for line in doc.lines() {
1635 if line.trim_start().starts_with("```") {
1636 in_code_block = !in_code_block;
1637 continue;
1638 }
1639 if in_code_block {
1640 continue;
1641 }
1642
1643 if line.starts_with('#') {
1644 let header = line.trim_start_matches('#').trim().to_lowercase();
1645 in_args = matches!(header.as_str(), "arguments" | "args" | "parameters" | "params");
1646 continue;
1647 }
1648
1649 if in_args {
1650 let trimmed = line.trim_start_matches(['*', '-', ' ']);
1653 let parsed = trimmed
1655 .find(" - ")
1656 .map(|pos| (pos, 3))
1657 .or_else(|| trimmed.find(": ").map(|pos| (pos, 2)));
1658 if let Some((sep_pos, sep_len)) = parsed {
1659 let raw_name = trimmed[..sep_pos].trim();
1660 let param_name = raw_name.trim_matches('`');
1662 let desc = trimmed[sep_pos + sep_len..].trim();
1663 if !param_name.is_empty() && !desc.is_empty() {
1664 map.insert(param_name.to_string(), desc.to_string());
1665 }
1666 }
1667 }
1668 }
1669
1670 map
1671}
1672
1673fn rust_links_to_plain(doc: &str) -> String {
1675 let mut result = String::with_capacity(doc.len());
1678 let chars: Vec<char> = doc.chars().collect();
1679 let mut i = 0;
1680 while i < chars.len() {
1681 if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '`' {
1683 let start = i + 1; let mut j = start;
1686 while j < chars.len() && chars[j] != ']' {
1687 j += 1;
1688 }
1689 if j < chars.len() {
1690 let text: String = chars[start..j].iter().collect();
1691 if j + 1 < chars.len() && chars[j + 1] == '(' {
1693 let mut k = j + 2;
1695 while k < chars.len() && chars[k] != ')' {
1696 k += 1;
1697 }
1698 if k < chars.len() {
1699 result.push_str(&text);
1700 i = k + 1;
1701 continue;
1702 }
1703 } else {
1704 result.push_str(&text);
1706 i = j + 1;
1707 continue;
1708 }
1709 }
1710 }
1711 result.push(chars[i]);
1712 i += 1;
1713 }
1714 result
1715}
1716
1717fn type_sort_key(name: &str) -> (u8, &str) {
1722 match name {
1723 "ConversionOptions" => (0, name),
1724 "ConversionResult" => (1, name),
1725 _ => (2, name),
1726 }
1727}
1728
1729fn is_update_type(name: &str) -> bool {
1730 name.ends_with("Update")
1731}
1732
1733#[cfg(test)]
1738mod tests {
1739 use super::*;
1740 use alef_core::ir::PrimitiveType;
1741
1742 #[test]
1743 fn test_doc_type_string() {
1744 assert_eq!(doc_type(&TypeRef::String, Language::Python), "str");
1745 assert_eq!(doc_type(&TypeRef::String, Language::Node), "string");
1746 assert_eq!(doc_type(&TypeRef::String, Language::Java), "String");
1747 assert_eq!(doc_type(&TypeRef::String, Language::Ffi), "const char*");
1748 }
1749
1750 #[test]
1751 fn test_doc_type_optional() {
1752 let ty = TypeRef::Optional(Box::new(TypeRef::String));
1753 assert_eq!(doc_type(&ty, Language::Python), "str | None");
1754 assert_eq!(doc_type(&ty, Language::Node), "string | null");
1755 assert_eq!(doc_type(&ty, Language::Go), "*string");
1756 assert_eq!(doc_type(&ty, Language::Csharp), "string?");
1757 }
1758
1759 #[test]
1760 fn test_doc_type_vec() {
1761 let ty = TypeRef::Vec(Box::new(TypeRef::String));
1762 assert_eq!(doc_type(&ty, Language::Python), "list[str]");
1763 assert_eq!(doc_type(&ty, Language::Node), "Array<string>");
1764 assert_eq!(doc_type(&ty, Language::Go), "[]string");
1765 assert_eq!(doc_type(&ty, Language::Java), "List<String>");
1766 }
1767
1768 #[test]
1769 fn test_doc_type_primitives() {
1770 assert_eq!(
1771 doc_type(&TypeRef::Primitive(PrimitiveType::Bool), Language::Python),
1772 "bool"
1773 );
1774 assert_eq!(
1775 doc_type(&TypeRef::Primitive(PrimitiveType::Bool), Language::Node),
1776 "boolean"
1777 );
1778 assert_eq!(
1779 doc_type(&TypeRef::Primitive(PrimitiveType::U64), Language::Node),
1780 "number"
1781 );
1782 assert_eq!(
1783 doc_type(&TypeRef::Primitive(PrimitiveType::F64), Language::Python),
1784 "float"
1785 );
1786 assert_eq!(
1787 doc_type(&TypeRef::Primitive(PrimitiveType::U32), Language::Ffi),
1788 "uint32_t"
1789 );
1790 }
1791
1792 #[test]
1793 fn test_enum_variant_name_python() {
1794 assert_eq!(enum_variant_name("Atx", Language::Python), "ATX");
1795 assert_eq!(enum_variant_name("SnakeCase", Language::Python), "SNAKE_CASE");
1796 }
1797
1798 #[test]
1799 fn test_enum_variant_name_java() {
1800 assert_eq!(enum_variant_name("Atx", Language::Java), "Atx");
1801 }
1802
1803 #[test]
1804 fn test_enum_variant_name_ffi() {
1805 assert_eq!(enum_variant_name("Atx", Language::Ffi), "HTM_ATX");
1806 }
1807
1808 #[test]
1809 fn test_clean_doc_strips_examples() {
1810 let doc = "Does something.\n\n# Examples\n\n```rust\nfoo();\n```\n";
1811 let cleaned = clean_doc(doc, Language::Python);
1812 assert!(!cleaned.contains("Examples"));
1813 assert!(!cleaned.contains("foo()"));
1814 assert!(cleaned.contains("Does something"));
1815 }
1816
1817 #[test]
1818 fn test_clean_doc_strips_arguments() {
1819 let doc = "Does something.\n\n# Arguments\n\n* html - The HTML string\n\nMore text.";
1820 let cleaned = clean_doc(doc, Language::Python);
1821 assert!(!cleaned.contains("Arguments"));
1822 assert!(!cleaned.contains("html - The HTML string"));
1823 assert!(cleaned.contains("Does something"));
1824 assert!(cleaned.contains("More text"));
1825 }
1826
1827 #[test]
1828 fn test_clean_doc_rust_links() {
1829 let doc = "See [`field`](Self::field) for details.";
1830 let cleaned = clean_doc(doc, Language::Python);
1831 assert_eq!(cleaned, "See `field` for details.");
1832 }
1833
1834 #[test]
1835 fn test_clean_doc_bare_rust_links() {
1836 let doc = "See [`ConversionOptions`] for details.";
1837 let cleaned = clean_doc(doc, Language::Python);
1838 assert_eq!(cleaned, "See `ConversionOptions` for details.");
1839 }
1840
1841 #[test]
1842 fn test_extract_param_docs() {
1843 let doc = "Convert HTML to Markdown.\n\n# Arguments\n\n* html - The HTML string to convert\n* options - Conversion options\n";
1844 let params = extract_param_docs(doc);
1845 assert_eq!(
1846 params.get("html").map(String::as_str),
1847 Some("The HTML string to convert")
1848 );
1849 assert_eq!(params.get("options").map(String::as_str), Some("Conversion options"));
1850 }
1851
1852 #[test]
1853 fn test_field_name_go_pascal_case() {
1854 assert_eq!(field_name("heading_style", Language::Go), "HeadingStyle");
1855 assert_eq!(field_name("list_indent_type", Language::Go), "ListIndentType");
1856 }
1857
1858 #[test]
1859 fn test_is_update_type() {
1860 assert!(is_update_type("ConversionOptionsUpdate"));
1861 assert!(!is_update_type("ConversionOptions"));
1862 }
1863
1864 #[test]
1865 fn test_type_sort_key_ordering() {
1866 assert!(type_sort_key("ConversionOptions") < type_sort_key("ConversionResult"));
1867 assert!(type_sort_key("ConversionResult") < type_sort_key("SomeOtherType"));
1868 }
1869
1870 #[test]
1871 fn test_func_name_conventions() {
1872 assert_eq!(func_name("convert", Language::Python), "convert");
1873 assert_eq!(func_name("convert_html", Language::Node), "convertHtml");
1874 assert_eq!(func_name("convert_html", Language::Go), "ConvertHtml");
1875 assert_eq!(func_name("convert", Language::Ffi), "htm_convert");
1876 }
1877
1878 #[test]
1879 fn test_type_name_ffi_prefix() {
1880 assert_eq!(type_name("ConversionOptions", Language::Ffi), "HTMConversionOptions");
1881 assert_eq!(type_name("ConversionResult", Language::Ffi), "HTMConversionResult");
1882 }
1883
1884 #[test]
1885 fn test_generate_docs_empty_api() {
1886 let api = ApiSurface {
1887 crate_name: "test".to_string(),
1888 version: "0.1.0".to_string(),
1889 types: vec![],
1890 functions: vec![],
1891 enums: vec![],
1892 errors: vec![],
1893 };
1894 use alef_core::config::*;
1895 let config = AlefConfig {
1896 crate_config: CrateConfig {
1897 name: "test".to_string(),
1898 sources: vec![],
1899 version_from: "Cargo.toml".to_string(),
1900 core_import: None,
1901 workspace_root: None,
1902 skip_core_import: false,
1903 features: vec![],
1904 path_mappings: std::collections::HashMap::new(),
1905 },
1906 languages: vec![Language::Python],
1907 exclude: ExcludeConfig::default(),
1908 include: IncludeConfig::default(),
1909 output: OutputConfig::default(),
1910 python: None,
1911 node: None,
1912 ruby: None,
1913 php: None,
1914 elixir: None,
1915 wasm: None,
1916 ffi: None,
1917 go: None,
1918 java: None,
1919 csharp: None,
1920 r: None,
1921 scaffold: None,
1922 readme: None,
1923 lint: None,
1924 test: None,
1925 custom_files: None,
1926 adapters: vec![],
1927 custom_modules: CustomModulesConfig::default(),
1928 custom_registrations: CustomRegistrationsConfig::default(),
1929 opaque_types: std::collections::HashMap::new(),
1930 generate: GenerateConfig::default(),
1931 generate_overrides: std::collections::HashMap::new(),
1932 dto: Default::default(),
1933 sync: None,
1934 e2e: None,
1935 };
1936
1937 let files = generate_docs(&api, &config, &[Language::Python], "docs").unwrap();
1938 assert_eq!(files.len(), 3);
1940 let lang_file = files
1941 .iter()
1942 .find(|f| f.path.to_str().unwrap().contains("api-python"))
1943 .unwrap();
1944 assert!(lang_file.content.contains("Python API Reference"));
1945 assert!(lang_file.content.contains("v0.1.0"));
1946 }
1947}