Skip to main content

alef_docs/
lib.rs

1//! API reference documentation generator for alef polyglot bindings.
2//!
3//! Generates per-language `api-{lang}.md` files plus shared `configuration.md`
4//! and `errors.md` files from the alef IR (`ApiSurface`).
5
6use 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
14// ---------------------------------------------------------------------------
15// Public API
16// ---------------------------------------------------------------------------
17
18/// Generate API reference documentation for the given languages.
19///
20/// Produces one `api-{lang}.md` per language, plus shared `configuration.md`
21/// and `errors.md` files written into `output_dir`.
22pub 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
40// ---------------------------------------------------------------------------
41// Per-language doc page
42// ---------------------------------------------------------------------------
43
44fn 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    // Front matter
57    out.push_str(&format!("---\ntitle: \"{lang_display} API Reference\"\n---\n\n"));
58
59    // Title
60    out.push_str(&format!(
61        "# {lang_display} API Reference <span class=\"version-badge\">v{version}</span>\n\n"
62    ));
63
64    // --- Functions section ---
65    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    // --- Types section ---
75    // Order: ConversionOptions, ConversionResult, then rest alphabetical
76    // Skip opaque types and *Update types in main section
77    let mut types_to_doc: Vec<&TypeDef> = api.types.iter().filter(|t| !is_update_type(&t.name)).collect();
78
79    // Sort: ConversionOptions first, ConversionResult second, rest alphabetical
80    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    // --- Enums section ---
91    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    // --- Errors section ---
100    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
117// ---------------------------------------------------------------------------
118// Function rendering
119// ---------------------------------------------------------------------------
120
121fn 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    // Extract parameter descriptions from the RAW doc string BEFORE cleaning
128    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    // Signature
137    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    // Parameters table
143    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(&param.name, lang);
149            let pty = doc_type_with_optional(&param.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                    // Clean Rust syntax from param descriptions
156                    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    // Return type
166    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    // Errors
172    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; // api is available for future use in function rendering
178    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
377// ---------------------------------------------------------------------------
378// Type rendering
379// ---------------------------------------------------------------------------
380
381fn 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    // Fields table (only for non-opaque types or opaque types with documented fields)
395    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    // Methods (called "Functions" in Elixir)
409    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            // Go methods: func (receiver *TypeName) MethodName(params) ReturnType
494            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            // Java: avoid `default` reserved keyword
515            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
591// ---------------------------------------------------------------------------
592// Enum rendering
593// ---------------------------------------------------------------------------
594
595fn 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
620// ---------------------------------------------------------------------------
621// Error rendering
622// ---------------------------------------------------------------------------
623
624fn 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
655// ---------------------------------------------------------------------------
656// Configuration page
657// ---------------------------------------------------------------------------
658
659fn 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    // Collect config-like types (ConversionOptions, PreprocessingOptions, etc.)
671    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
708// ---------------------------------------------------------------------------
709// Errors page
710// ---------------------------------------------------------------------------
711
712fn 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
747// ---------------------------------------------------------------------------
748// Type mapping
749// ---------------------------------------------------------------------------
750
751/// Map an IR TypeRef to the idiomatic type string for a given language.
752pub 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                    // Java generics can't use primitives — box them
798                    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                // Java generics require boxed types
824                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
982/// Return the boxed (object) type for Java generics.
983///
984/// Java generics cannot use primitive types (`int`, `long`, etc.); they require
985/// the corresponding wrapper classes (`Integer`, `Long`, etc.).
986fn 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        // Non-primitive types are already reference types in Java
998        _ => doc_type(ty, Language::Java),
999    }
1000}
1001
1002// ---------------------------------------------------------------------------
1003// Naming conventions
1004// ---------------------------------------------------------------------------
1005
1006/// Get the display name for a language.
1007fn 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
1023/// Get the slug used in file names (e.g. `typescript` for `Node`).
1024fn 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
1040/// Get the code fence language identifier.
1041fn 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
1056/// Convert a Rust type name to the idiomatic name for the target language.
1057fn type_name(name: &str, lang: Language) -> String {
1058    // Strip module path prefix if present
1059    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            // C: prefix with HTM and PascalCase
1073            format!("HTM{}", short.to_pascal_case())
1074        }
1075    }
1076}
1077
1078/// Convert a Rust function name to the idiomatic name for the target language.
1079fn 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    // Handle reserved keywords
1087    match (lang, base.as_str()) {
1088        (Language::Java, "default") => "defaultOptions".to_string(),
1089        (Language::Csharp, "Default") => "CreateDefault".to_string(),
1090        _ => base,
1091    }
1092}
1093
1094/// Convert a Rust field name to the idiomatic name for the target language.
1095fn 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        // Go and C# exported fields/properties are PascalCase
1099        Language::Go | Language::Csharp => name.to_pascal_case(),
1100        Language::Node | Language::Wasm | Language::Java | Language::Php => to_camel_case(name),
1101    }
1102}
1103
1104/// Convert a Rust enum variant name to the idiomatic name for the target language.
1105fn enum_variant_name(name: &str, lang: Language) -> String {
1106    match lang {
1107        Language::Python => {
1108            // Python: UPPER_SNAKE_CASE
1109            name.to_snake_case().to_uppercase()
1110        }
1111        Language::Ruby | Language::Elixir => {
1112            // Ruby/Elixir: :snake_atom style
1113            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
1123/// Convert snake_case or PascalCase to camelCase.
1124fn 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
1133// ---------------------------------------------------------------------------
1134// Default value formatting
1135// ---------------------------------------------------------------------------
1136
1137fn 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            // v is something like "HeadingStyle::Atx" or just "Atx"
1174            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                // Bare variant name — resolve the enum type from the field type
1181                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 the field type is a Named enum, resolve to its default (or first) variant
1203            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            // Non-enum Empty: depends on field type
1218            // Unwrap Optional wrapper to get inner type for collection/map detection
1219            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            // Non-collection Empty: use language-specific null/default
1264            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
1292/// Format an enum variant reference: `TypeName.VARIANT` or `:atom` style per language.
1293fn 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
1308/// Format the error/exception phrase for a function that can fail.
1309fn 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
1343/// Like `doc_type` but wraps in the nullable form when `optional` is true.
1344fn doc_type_with_optional(ty: &TypeRef, lang: Language, optional: bool) -> String {
1345    // If the type is already Optional<T>, don't double-wrap
1346    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
1364// ---------------------------------------------------------------------------
1365// Doc string utilities
1366// ---------------------------------------------------------------------------
1367
1368/// Rust doc section headers that should be stripped for all non-Rust output.
1369const RUST_ONLY_SECTIONS: &[&str] = &["example", "examples", "arguments", "fields"];
1370
1371/// Clean up Rust doc strings for Markdown output.
1372///
1373/// - Strips `# Example`, `# Arguments`, `# Fields` sections (Rust-specific)
1374/// - Strips code blocks containing Rust-specific syntax
1375/// - Converts `` [`Foo`](Self::bar) `` → `` `Foo` ``
1376/// - Converts bare `` [`Foo`] `` → `` `Foo` ``
1377/// - Converts `# Errors` / `# Returns` headings to bold inline text
1378/// - Converts `Foo::bar()` Rust path syntax to `Foo.bar()` in prose
1379fn clean_doc(doc: &str, lang: Language) -> String {
1380    if doc.is_empty() {
1381        return String::new();
1382    }
1383
1384    // Strip Rust-specific sections and their code blocks
1385    let doc = strip_rust_sections(doc);
1386
1387    // Convert Rust-style links
1388    let doc = rust_links_to_plain(&doc);
1389
1390    // Convert `# Errors` / `# Returns` headings to bold inline text
1391    // These are Rust doc conventions that render as H1 headings, which is wrong
1392    let doc = convert_doc_headings_to_bold(&doc);
1393
1394    // Convert Rust path syntax `Foo::bar()` → `Foo.bar()` (or `Foo::bar()` for PHP) in prose
1395    let doc = rust_paths_to_dot_notation(&doc, lang);
1396
1397    // Replace Rust-centric terminology
1398    let doc = replace_rust_terminology(&doc, lang);
1399
1400    doc.trim().to_string()
1401}
1402
1403/// Convert `# Errors` and `# Returns` section headings to bold inline text.
1404fn 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
1434/// Replace Rust-centric terminology with language-neutral equivalents.
1435fn 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    // Replace `None` backtick references with the language-idiomatic null
1446    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`", // keep as-is for Python
1450        Language::R | Language::Ffi => "`NULL`",
1451    };
1452    let doc = doc.replace("`None`", none_replacement);
1453
1454    // For Python, normalise boolean literals in prose: `true` → `True`, `false` → `False`
1455    if lang == Language::Python {
1456        let doc = doc.replace("`true`", "`True`").replace("`false`", "`False`");
1457        return doc;
1458    }
1459
1460    doc
1461}
1462
1463/// Replace Rust `Foo::bar()` path notation with `Foo.bar()` in prose (outside code blocks).
1464///
1465/// For PHP, static method calls use `::` so we keep that separator.
1466fn rust_paths_to_dot_notation(doc: &str, lang: Language) -> String {
1467    // PHP uses `::` for static method calls; other languages use `.`
1468    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        // Replace `Foo::bar` patterns in prose
1484        // Common Rust-isms: Default::default(), ConversionOptions::default(), ConversionOptions::builder()
1485        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
1494/// Inline version that also strips newlines for use in table cells.
1495fn clean_doc_inline(doc: &str) -> String {
1496    if doc.is_empty() {
1497        return String::new();
1498    }
1499    // Use Python as a representative non-Rust language for inline cleaning
1500    let cleaned = clean_doc(doc, Language::Python);
1501    // Collapse to single line for table cells
1502    cleaned
1503        .lines()
1504        .map(str::trim)
1505        .filter(|l| !l.is_empty())
1506        .collect::<Vec<_>>()
1507        .join(" ")
1508        // Escape pipe characters in table cells
1509        .replace('|', "\\|")
1510}
1511
1512/// Strip Rust-specific doc sections (`# Example`, `# Arguments`, `# Fields`).
1513///
1514/// Also strips fenced code blocks that contain Rust-specific syntax
1515/// (use statements, unwrap(), assert!, etc.) regardless of which section they appear in.
1516fn 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        // Track code block boundaries
1524        if line.trim_start().starts_with("```") {
1525            if in_code_block {
1526                // End of code block — decide whether to emit it
1527                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        // Outside code block: check for section headers
1554        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                // Any other section header ends the skip
1561                skip_section = false;
1562            }
1563        }
1564
1565        if skip_section {
1566            // Blank lines and list items are part of the section — keep skipping
1567            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("  ") // indented continuation
1573                || trimmed.starts_with('\t');
1574            if is_section_content {
1575                continue;
1576            }
1577            // Non-list, non-blank line ends the skip
1578            skip_section = false;
1579        }
1580
1581        // Skip lines that are clearly Rust-specific (unfenced imports/assertions)
1582        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
1593/// Returns true if a code block's content contains Rust-specific patterns.
1594fn is_rust_code_block(content: &str) -> bool {
1595    // Opening fence line may declare "rust" or "no_run" etc.
1596    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        // Check if content looks like Rust
1600        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
1619/// Returns true if a plain (non-fenced) line is Rust-specific and should be removed.
1620fn 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
1625/// Extract parameter descriptions from a `# Arguments` section in a doc string.
1626///
1627/// Parses lines like `* name - description` or `* name: description`.
1628/// Returns a map of parameter name → description.
1629fn 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            // Match "* `param_name` - description" or "* param_name - description"
1651            // or "* param_name: description"
1652            let trimmed = line.trim_start_matches(['*', '-', ' ']);
1653            // Try " - " separator first (3 chars), then ": " (2 chars)
1654            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                // Strip surrounding backticks if present (e.g. `` `html` `` → `html`)
1661                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
1673/// Convert `` [`text`](path) `` and bare `` [`text`] `` patterns to `` `text` ``.
1674fn rust_links_to_plain(doc: &str) -> String {
1675    // Pattern 1: [`text`](anything) → `text`
1676    // Pattern 2: [`text`] → `text`  (bare doc links)
1677    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        // Look for [`
1682        if i + 1 < chars.len() && chars[i] == '[' && chars[i + 1] == '`' {
1683            // Find closing `]`
1684            let start = i + 1; // position of opening `
1685            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                // Check if followed by `(` (linked form) or not (bare form)
1692                if j + 1 < chars.len() && chars[j + 1] == '(' {
1693                    // Linked form: find closing `)`
1694                    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                    // Bare form: [`text`] — emit just the text
1705                    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
1717// ---------------------------------------------------------------------------
1718// Ordering helpers
1719// ---------------------------------------------------------------------------
1720
1721fn 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// ---------------------------------------------------------------------------
1734// Tests
1735// ---------------------------------------------------------------------------
1736
1737#[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        // 1 lang + configuration.md + errors.md
1939        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}