Skip to main content

intent_render/
markdown.rs

1//! Render a parsed intent specification as Markdown.
2//!
3//! Produces clean, readable documentation suitable for review
4//! by non-engineers (PMs, designers, stakeholders).
5
6use intent_parser::ast;
7
8use crate::format_type;
9
10/// Render an AST [`File`] to a Markdown string.
11pub fn render(file: &ast::File) -> String {
12    let mut out = String::new();
13    out.push_str(&format!("# {}\n\n", file.module.name));
14
15    if let Some(doc) = &file.doc {
16        for line in &doc.lines {
17            out.push_str(line);
18            out.push('\n');
19        }
20        out.push('\n');
21    }
22
23    if !file.imports.is_empty() {
24        out.push_str("**Imports:**\n\n");
25        for use_decl in &file.imports {
26            if let Some(item) = &use_decl.item {
27                out.push_str(&format!("- `{}.{}`\n", use_decl.module_name, item));
28            } else {
29                out.push_str(&format!("- `{}`\n", use_decl.module_name));
30            }
31        }
32        out.push('\n');
33    }
34
35    for item in &file.items {
36        match item {
37            ast::TopLevelItem::Entity(e) => render_entity(&mut out, e),
38            ast::TopLevelItem::Action(a) => render_action(&mut out, a),
39            ast::TopLevelItem::Invariant(i) => render_invariant(&mut out, i),
40            ast::TopLevelItem::EdgeCases(ec) => render_edge_cases(&mut out, ec),
41            ast::TopLevelItem::Test(_) => {} // Tests are not rendered
42        }
43    }
44
45    out
46}
47
48fn render_entity(out: &mut String, entity: &ast::EntityDecl) {
49    out.push_str(&format!("## Entity: {}\n\n", entity.name));
50    if let Some(doc) = &entity.doc {
51        for line in &doc.lines {
52            out.push_str(line);
53            out.push('\n');
54        }
55        out.push('\n');
56    }
57    out.push_str("| Field | Type |\n|-------|------|\n");
58    for field in &entity.fields {
59        out.push_str(&format!(
60            "| `{}` | `{}` |\n",
61            field.name,
62            format_type(&field.ty)
63        ));
64    }
65    out.push('\n');
66}
67
68fn render_action(out: &mut String, action: &ast::ActionDecl) {
69    out.push_str(&format!("## Action: {}\n\n", action.name));
70    if let Some(doc) = &action.doc {
71        for line in &doc.lines {
72            out.push_str(line);
73            out.push('\n');
74        }
75        out.push('\n');
76    }
77    if !action.params.is_empty() {
78        out.push_str("**Parameters:**\n\n");
79        for p in &action.params {
80            out.push_str(&format!("- `{}`: `{}`\n", p.name, format_type(&p.ty)));
81        }
82        out.push('\n');
83    }
84}
85
86fn render_invariant(out: &mut String, inv: &ast::InvariantDecl) {
87    out.push_str(&format!("## Invariant: {}\n\n", inv.name));
88    if let Some(doc) = &inv.doc {
89        for line in &doc.lines {
90            out.push_str(line);
91            out.push('\n');
92        }
93        out.push('\n');
94    }
95}
96
97fn render_edge_cases(out: &mut String, _ec: &ast::EdgeCasesDecl) {
98    out.push_str("## Edge Cases\n\n");
99}