Skip to main content

gruel_doc/
lib.rs

1//! ADR-0089: gruel's documentation surface.
2//!
3//! Inputs: a merged `Ast` + the `ThreadedRodeo` used to intern its
4//! identifiers. Outputs: a `DocSite` — one file per input file, each
5//! containing rendered per-item pages in either Markdown or HTML.
6
7use gruel_parser::ast::{Ast, Doc, EnumDecl, Item, LinkExternBlock, StructDecl};
8use lasso::ThreadedRodeo;
9
10pub mod markdown;
11pub use markdown::render_markdown;
12
13pub mod html;
14pub use html::render_html;
15
16pub mod links;
17pub use links::LinkTable;
18
19/// A single file's rendered documentation.
20#[derive(Debug, Clone)]
21pub struct DocFile {
22    /// Source-file stem used as the directory name in the output
23    /// (e.g. `"math"` for `std/math.gruel`).
24    pub stem: String,
25    /// Optional module-level docstring.
26    pub module_doc: Option<Doc>,
27    /// One entry per top-level item in declaration order.
28    pub items: Vec<DocItem>,
29}
30
31/// One renderable item with the URL stem we use to address it.
32#[derive(Debug, Clone)]
33pub struct DocItem {
34    /// File-name base for this item, e.g. `"fn.foo"`, `"struct.Bar"`.
35    pub slug: String,
36    /// Display name (`"foo"`, `"Bar"`, etc.).
37    pub name: String,
38    /// What kind of item this is (purely cosmetic in the rendered page).
39    pub kind: ItemKind,
40    /// The item's own docstring, if any.
41    pub doc: Option<Doc>,
42    /// Type-specific extra info rendered into the page body.
43    pub detail: ItemDetail,
44}
45
46/// Top-level item kind (used in the rendered headers).
47#[derive(Debug, Clone, Copy)]
48pub enum ItemKind {
49    Function,
50    Struct,
51    Enum,
52    Interface,
53    Derive,
54    Const,
55    LinkExtern,
56}
57
58impl ItemKind {
59    pub fn label(self) -> &'static str {
60        match self {
61            ItemKind::Function => "fn",
62            ItemKind::Struct => "struct",
63            ItemKind::Enum => "enum",
64            ItemKind::Interface => "interface",
65            ItemKind::Derive => "derive",
66            ItemKind::Const => "const",
67            ItemKind::LinkExtern => "link_extern",
68        }
69    }
70}
71
72/// Type-specific information used to render a richer page than just
73/// `# name + docstring`. Anything not modelled here falls back to
74/// the bare `name + doc` rendering.
75#[derive(Debug, Clone, Default)]
76pub struct ItemDetail {
77    /// Public fields (struct/enum struct-variants).
78    pub fields: Vec<NamedDoc>,
79    /// Enum variants.
80    pub variants: Vec<NamedDoc>,
81    /// Methods on a struct/enum/derive.
82    pub methods: Vec<NamedDoc>,
83    /// Extern fn declarations inside a `link_extern { }` block.
84    pub extern_fns: Vec<NamedDoc>,
85}
86
87/// A name + (optional) doc pair, used for nested items.
88#[derive(Debug, Clone)]
89pub struct NamedDoc {
90    pub name: String,
91    pub doc: Option<Doc>,
92}
93
94/// A renderable site composed of one entry per source file.
95#[derive(Debug, Clone, Default)]
96pub struct DocSite {
97    pub files: Vec<DocFile>,
98}
99
100impl DocSite {
101    /// Build a `DocSite` from a single `Ast` + interner pair.
102    ///
103    /// `stem` is the file's display name (typically the source path's
104    /// stem). Anonymous types and items lacking docs still appear in
105    /// the output — the renderer just shows their header.
106    pub fn from_ast(stem: impl Into<String>, ast: &Ast, interner: &ThreadedRodeo) -> DocFile {
107        let mut items = Vec::new();
108        for item in &ast.items {
109            if let Some(doc_item) = item_to_doc_item(item, interner) {
110                items.push(doc_item);
111            }
112        }
113        DocFile {
114            stem: stem.into(),
115            module_doc: ast.module_doc.clone(),
116            items,
117        }
118    }
119
120    /// Add a `DocFile` produced by `from_ast` to this site.
121    pub fn push(&mut self, file: DocFile) {
122        self.files.push(file);
123    }
124
125    /// ADR-0089 Phase 5: build a `LinkTable` covering every top-level
126    /// item in the site, with each slug prefixed by the containing file's
127    /// stem. Right for the site-level index page where every link is one
128    /// directory away.
129    pub fn link_table(&self) -> LinkTable {
130        let mut table = LinkTable::new();
131        for file in &self.files {
132            for item in &file.items {
133                let slug_with_dir = format!("{}/{}", file.stem, item.slug);
134                table.insert(&item.name, item.kind.label(), &slug_with_dir);
135            }
136        }
137        table
138    }
139}
140
141impl DocFile {
142    /// ADR-0089 Phase 5: build a `LinkTable` for cross-references from
143    /// inside this file. Items in the same file are siblings, so their
144    /// slugs are unprefixed.
145    pub fn link_table(&self) -> LinkTable {
146        let mut table = LinkTable::new();
147        for item in &self.items {
148            table.insert(&item.name, item.kind.label(), &item.slug);
149        }
150        table
151    }
152}
153
154fn item_to_doc_item(item: &Item, interner: &ThreadedRodeo) -> Option<DocItem> {
155    match item {
156        Item::Function(f) => Some(DocItem {
157            slug: format!("fn.{}", interner.resolve(&f.name.name)),
158            name: interner.resolve(&f.name.name).to_string(),
159            kind: ItemKind::Function,
160            doc: f.doc.clone(),
161            detail: ItemDetail::default(),
162        }),
163        Item::Struct(s) => Some(struct_doc_item(s, interner)),
164        Item::Enum(e) => Some(enum_doc_item(e, interner)),
165        Item::Interface(i) => Some(DocItem {
166            slug: format!("interface.{}", interner.resolve(&i.name.name)),
167            name: interner.resolve(&i.name.name).to_string(),
168            kind: ItemKind::Interface,
169            doc: i.doc.clone(),
170            detail: ItemDetail {
171                methods: i
172                    .methods
173                    .iter()
174                    .map(|m| NamedDoc {
175                        name: interner.resolve(&m.name.name).to_string(),
176                        doc: m.doc.clone(),
177                    })
178                    .collect(),
179                ..ItemDetail::default()
180            },
181        }),
182        Item::Derive(d) => Some(DocItem {
183            slug: format!("derive.{}", interner.resolve(&d.name.name)),
184            name: interner.resolve(&d.name.name).to_string(),
185            kind: ItemKind::Derive,
186            doc: d.doc.clone(),
187            detail: ItemDetail {
188                methods: d
189                    .methods
190                    .iter()
191                    .map(|m| NamedDoc {
192                        name: interner.resolve(&m.name.name).to_string(),
193                        doc: m.doc.clone(),
194                    })
195                    .collect(),
196                ..ItemDetail::default()
197            },
198        }),
199        Item::Const(c) => Some(DocItem {
200            slug: format!("const.{}", interner.resolve(&c.name.name)),
201            name: interner.resolve(&c.name.name).to_string(),
202            kind: ItemKind::Const,
203            doc: c.doc.clone(),
204            detail: ItemDetail::default(),
205        }),
206        Item::LinkExtern(b) => Some(link_extern_doc_item(b, interner)),
207        Item::Error(_) => None,
208    }
209}
210
211fn struct_doc_item(s: &StructDecl, interner: &ThreadedRodeo) -> DocItem {
212    DocItem {
213        slug: format!("struct.{}", interner.resolve(&s.name.name)),
214        name: interner.resolve(&s.name.name).to_string(),
215        kind: ItemKind::Struct,
216        doc: s.doc.clone(),
217        detail: ItemDetail {
218            fields: s
219                .fields
220                .iter()
221                .map(|f| NamedDoc {
222                    name: interner.resolve(&f.name.name).to_string(),
223                    doc: f.doc.clone(),
224                })
225                .collect(),
226            methods: s
227                .methods
228                .iter()
229                .map(|m| NamedDoc {
230                    name: interner.resolve(&m.name.name).to_string(),
231                    doc: m.doc.clone(),
232                })
233                .collect(),
234            ..ItemDetail::default()
235        },
236    }
237}
238
239fn enum_doc_item(e: &EnumDecl, interner: &ThreadedRodeo) -> DocItem {
240    DocItem {
241        slug: format!("enum.{}", interner.resolve(&e.name.name)),
242        name: interner.resolve(&e.name.name).to_string(),
243        kind: ItemKind::Enum,
244        doc: e.doc.clone(),
245        detail: ItemDetail {
246            variants: e
247                .variants
248                .iter()
249                .map(|v| NamedDoc {
250                    name: interner.resolve(&v.name.name).to_string(),
251                    doc: v.doc.clone(),
252                })
253                .collect(),
254            methods: e
255                .methods
256                .iter()
257                .map(|m| NamedDoc {
258                    name: interner.resolve(&m.name.name).to_string(),
259                    doc: m.doc.clone(),
260                })
261                .collect(),
262            ..ItemDetail::default()
263        },
264    }
265}
266
267fn link_extern_doc_item(b: &LinkExternBlock, interner: &ThreadedRodeo) -> DocItem {
268    let name = interner.resolve(&b.library.value).to_string();
269    DocItem {
270        slug: format!("link_extern.{}", &name),
271        name,
272        kind: ItemKind::LinkExtern,
273        doc: b.doc.clone(),
274        detail: ItemDetail {
275            extern_fns: b
276                .items
277                .iter()
278                .map(|f| NamedDoc {
279                    name: interner.resolve(&f.name.name).to_string(),
280                    doc: f.doc.clone(),
281                })
282                .collect(),
283            ..ItemDetail::default()
284        },
285    }
286}