1use std::fmt::Write;
4
5use crate::links::{LinkTable, rewrite};
6use crate::{DocFile, DocItem, ItemKind, NamedDoc};
7
8pub fn render_index(file: &DocFile) -> String {
11 render_index_with(file, &LinkTable::new())
12}
13
14pub fn render_index_with(file: &DocFile, table: &LinkTable) -> String {
17 let mut out = String::new();
18 write!(out, "# {}\n\n", file.stem).unwrap();
19 if let Some(module_doc) = &file.module_doc {
20 out.push_str(&rewrite(&module_doc.body, table, ".md"));
21 out.push_str("\n\n");
22 }
23 if file.items.is_empty() {
24 return out;
25 }
26 out.push_str("## Items\n\n");
27 for item in &file.items {
28 write!(
29 out,
30 "- [`{} {}`]({}.md)",
31 item.kind.label(),
32 item.name,
33 item.slug
34 )
35 .unwrap();
36 if let Some(summary) = first_line(&item.doc) {
37 let rewritten = rewrite(summary, table, ".md");
38 write!(out, " — {}", rewritten).unwrap();
39 }
40 out.push('\n');
41 }
42 out
43}
44
45pub fn render_markdown(item: &DocItem) -> String {
47 render_markdown_with(item, &LinkTable::new())
48}
49
50pub fn render_markdown_with(item: &DocItem, table: &LinkTable) -> String {
53 let mut out = String::new();
54 write!(out, "# `{} {}`\n\n", item.kind.label(), item.name).unwrap();
55 if let Some(doc) = &item.doc {
56 out.push_str(&rewrite(&doc.body, table, ".md"));
57 out.push_str("\n\n");
58 }
59 render_sections(&mut out, item, table);
60 out
61}
62
63fn render_sections(out: &mut String, item: &DocItem, table: &LinkTable) {
64 render_named_section(out, "Fields", &item.detail.fields, table);
65 render_named_section(out, "Variants", &item.detail.variants, table);
66 render_named_section(out, "Methods", &item.detail.methods, table);
67 if !item.detail.extern_fns.is_empty() {
68 match item.kind {
69 ItemKind::LinkExtern => {
70 render_named_section(out, "Extern functions", &item.detail.extern_fns, table)
71 }
72 _ => render_named_section(out, "Functions", &item.detail.extern_fns, table),
73 }
74 }
75}
76
77fn render_named_section(out: &mut String, heading: &str, items: &[NamedDoc], table: &LinkTable) {
78 if items.is_empty() {
79 return;
80 }
81 write!(out, "## {}\n\n", heading).unwrap();
82 for it in items {
83 write!(out, "- `{}`", it.name).unwrap();
84 if let Some(summary) = first_line(&it.doc) {
85 let rewritten = rewrite(summary, table, ".md");
86 write!(out, " — {}", rewritten).unwrap();
87 }
88 out.push('\n');
89 }
90 out.push('\n');
91}
92
93fn first_line(doc: &Option<gruel_parser::ast::Doc>) -> Option<&str> {
94 doc.as_ref().and_then(|d| d.body.lines().next())
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use gruel_parser::ast::Doc;
101 use gruel_util::Span;
102
103 fn make_doc(body: &str) -> Doc {
104 Doc {
105 body: body.to_string(),
106 span: Span::default(),
107 }
108 }
109
110 #[test]
111 fn render_fn_page() {
112 let item = DocItem {
113 slug: "fn.foo".into(),
114 name: "foo".into(),
115 kind: ItemKind::Function,
116 doc: Some(make_doc("Does the foo.\n\nMore details.")),
117 detail: Default::default(),
118 };
119 let md = render_markdown(&item);
120 assert!(md.contains("# `fn foo`"));
121 assert!(md.contains("Does the foo."));
122 assert!(md.contains("More details."));
123 }
124
125 #[test]
126 fn render_index_page() {
127 let file = DocFile {
128 stem: "math".into(),
129 module_doc: Some(make_doc("Module-level docs.")),
130 items: vec![
131 DocItem {
132 slug: "fn.add".into(),
133 name: "add".into(),
134 kind: ItemKind::Function,
135 doc: Some(make_doc("Adds two ints.")),
136 detail: Default::default(),
137 },
138 DocItem {
139 slug: "fn.sub".into(),
140 name: "sub".into(),
141 kind: ItemKind::Function,
142 doc: None,
143 detail: Default::default(),
144 },
145 ],
146 };
147 let md = render_index(&file);
148 assert!(md.contains("# math"));
149 assert!(md.contains("Module-level docs."));
150 assert!(md.contains("- [`fn add`](fn.add.md) — Adds two ints."));
152 assert!(md.contains("- [`fn sub`](fn.sub.md)"));
153 }
154
155 #[test]
156 fn intra_doc_link_rewritten_in_body() {
157 let mut table = LinkTable::new();
158 table.insert("Vec", "struct", "struct.Vec");
159 let item = DocItem {
160 slug: "fn.push".into(),
161 name: "push".into(),
162 kind: ItemKind::Function,
163 doc: Some(make_doc("Push to a [Vec].")),
164 detail: Default::default(),
165 };
166 let md = render_markdown_with(&item, &table);
167 assert!(md.contains("[Vec](struct.Vec.md)"));
168 }
169
170 #[test]
171 fn struct_page_lists_fields() {
172 let item = DocItem {
173 slug: "struct.Point".into(),
174 name: "Point".into(),
175 kind: ItemKind::Struct,
176 doc: Some(make_doc("A 2D point.")),
177 detail: crate::ItemDetail {
178 fields: vec![
179 NamedDoc {
180 name: "x".into(),
181 doc: Some(make_doc("x coordinate")),
182 },
183 NamedDoc {
184 name: "y".into(),
185 doc: None,
186 },
187 ],
188 ..Default::default()
189 },
190 };
191 let md = render_markdown(&item);
192 assert!(md.contains("## Fields"));
193 assert!(md.contains("- `x` — x coordinate"));
194 assert!(md.contains("- `y`\n"));
195 }
196}