1use ahash::HashMap;
4use ron2::schema::{Field, Schema, TypeKind, Variant, VariantKind};
5
6use crate::{
7 config::{DocConfig, OutputFormat, OutputMode},
8 discovery::DiscoveredSchema,
9 example::{build_schema_map, generate_example},
10 link::{LinkResolver, type_path_short_name},
11};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum LinkMode {
16 File,
18 Anchor,
20}
21
22impl From<OutputMode> for LinkMode {
23 fn from(mode: OutputMode) -> Self {
24 match mode {
25 OutputMode::MultiPage => LinkMode::File,
26 OutputMode::SinglePage => LinkMode::Anchor,
27 }
28 }
29}
30
31pub fn generate_markdown(
33 schema: &DiscoveredSchema,
34 config: &DocConfig,
35 all_schemas: &[DiscoveredSchema],
36) -> String {
37 let mut output = String::new();
38 let short_name = type_path_short_name(&schema.type_path);
39 let resolver = LinkResolver::new(all_schemas, config.base_url.as_deref());
40 let link_mode = LinkMode::from(config.output_mode);
41
42 write_header(&mut output, short_name, &schema.schema, config.format);
44
45 if let Some(doc) = &schema.schema.doc {
47 output.push_str(doc);
48 output.push_str("\n\n");
49 }
50
51 match &schema.schema.kind {
53 TypeKind::Struct { fields } => {
54 write_struct_docs(&mut output, fields, &resolver, link_mode, 2);
55 }
56 TypeKind::Enum { variants } => {
57 write_enum_docs(&mut output, variants, &resolver, link_mode, 2);
58 }
59 _ => {
60 output.push_str("## Type\n\n");
62 output.push_str(&format_type(&resolver, &schema.schema.kind, link_mode));
63 output.push_str("\n\n");
64 }
65 }
66
67 let schema_map = build_schema_map(all_schemas);
69 write_example(
70 &mut output,
71 &schema.schema,
72 config.example_depth,
73 &schema_map,
74 );
75
76 output
77}
78
79pub fn generate_type_content(
86 schema: &DiscoveredSchema,
87 resolver: &LinkResolver,
88 link_mode: LinkMode,
89 heading_level: u8,
90) -> String {
91 let mut output = String::new();
92
93 if let Some(doc) = &schema.schema.doc {
95 output.push_str(doc);
96 output.push_str("\n\n");
97 }
98
99 match &schema.schema.kind {
101 TypeKind::Struct { fields } => {
102 write_struct_docs(&mut output, fields, resolver, link_mode, heading_level);
103 }
104 TypeKind::Enum { variants } => {
105 write_enum_docs(&mut output, variants, resolver, link_mode, heading_level);
106 }
107 _ => {
108 output.push_str("**Type:** ");
109 output.push_str(&format_type(resolver, &schema.schema.kind, link_mode));
110 output.push_str("\n\n");
111 }
112 }
113
114 output
115}
116
117fn heading(level: u8, text: &str) -> String {
119 let hashes = "#".repeat(level as usize);
120 format!("{} {}\n\n", hashes, text)
121}
122
123pub fn generate_type_example(
125 schema: &Schema,
126 max_depth: usize,
127 all_schemas: &[DiscoveredSchema],
128) -> String {
129 let schema_map = build_schema_map(all_schemas);
130 let mut output = String::new();
131 output.push_str("```ron\n");
132 output.push_str(&generate_example(schema, max_depth, &schema_map));
133 output.push_str("\n```\n");
134 output
135}
136
137fn write_header(output: &mut String, name: &str, schema: &Schema, format: OutputFormat) {
138 match format {
139 OutputFormat::Starlight => {
140 output.push_str("---\n");
141 output.push_str(&format!("title: {}\n", name));
142 if let Some(doc) = &schema.doc {
143 let first_line = doc.lines().next().unwrap_or("");
144 let escaped = first_line.replace('"', "\\\"");
145 output.push_str(&format!("description: \"{}\"\n", escaped));
146 }
147 output.push_str("---\n\n");
148 }
149 OutputFormat::Plain => {
150 output.push_str(&format!("# {}\n\n", name));
151 }
152 }
153}
154
155fn format_type(resolver: &LinkResolver, ty: &TypeKind, link_mode: LinkMode) -> String {
157 match link_mode {
158 LinkMode::File => resolver.type_to_markdown(ty),
159 LinkMode::Anchor => resolver.type_to_markdown_anchor(ty),
160 }
161}
162
163fn write_struct_docs(
164 output: &mut String,
165 fields: &[Field],
166 resolver: &LinkResolver,
167 link_mode: LinkMode,
168 heading_level: u8,
169) {
170 if fields.is_empty() {
171 output.push_str("This is a unit struct with no fields.\n\n");
172 return;
173 }
174
175 output.push_str(&heading(heading_level, "Fields"));
176 output.push_str("| Field | Type | Required | Description |\n");
177 output.push_str("|-------|------|----------|-------------|\n");
178
179 for field in fields {
180 let type_str = format_type(resolver, &field.ty, link_mode);
181 let required = if field.optional { "No" } else { "Yes" };
182 let doc = field.doc.as_deref().unwrap_or("—");
183 let doc_escaped = doc.replace('|', "\\|").replace('\n', " ");
185
186 let flattened_note = if field.flattened {
187 " *(flattened)*"
188 } else {
189 ""
190 };
191
192 output.push_str(&format!(
193 "| `{}` | {} | {} | {}{} |\n",
194 field.name, type_str, required, doc_escaped, flattened_note
195 ));
196 }
197
198 output.push('\n');
199}
200
201fn write_enum_docs(
202 output: &mut String,
203 variants: &[Variant],
204 resolver: &LinkResolver,
205 link_mode: LinkMode,
206 heading_level: u8,
207) {
208 if variants.is_empty() {
209 output.push_str("This enum has no variants.\n\n");
210 return;
211 }
212
213 output.push_str(&heading(heading_level, "Variants"));
214
215 let all_unit = variants.iter().all(|v| matches!(v.kind, VariantKind::Unit));
217 let has_complex = variants
218 .iter()
219 .any(|v| !matches!(v.kind, VariantKind::Unit));
220
221 if all_unit {
222 output.push_str("| Variant | Description |\n");
224 output.push_str("|---------|-------------|\n");
225
226 for variant in variants {
227 let doc = variant.doc.as_deref().unwrap_or("—");
228 let doc_escaped = doc.replace('|', "\\|").replace('\n', " ");
229 output.push_str(&format!("| `{}` | {} |\n", variant.name, doc_escaped));
230 }
231 output.push('\n');
232 } else {
233 output.push_str("| Variant | Kind | Description |\n");
235 output.push_str("|---------|------|-------------|\n");
236
237 for variant in variants {
238 let doc = variant
239 .doc
240 .as_ref()
241 .map(|d| d.lines().next().unwrap_or(""))
242 .unwrap_or("—");
243 let doc_escaped = doc.replace('|', "\\|");
244
245 let kind_label = match &variant.kind {
246 VariantKind::Unit => "Unit",
247 VariantKind::Tuple(_) => "Tuple",
248 VariantKind::Struct(_) => "Struct",
249 };
250
251 let variant_cell = if matches!(variant.kind, VariantKind::Unit) {
253 format!("`{}`", variant.name)
254 } else {
255 let anchor = variant.name.to_lowercase();
256 format!("[`{}`](#{})", variant.name, anchor)
257 };
258
259 output.push_str(&format!(
260 "| {} | {} | {} |\n",
261 variant_cell, kind_label, doc_escaped
262 ));
263 }
264 output.push('\n');
265
266 let variant_heading_level = heading_level + 1;
268 if has_complex {
269 for variant in variants {
270 match &variant.kind {
271 VariantKind::Unit => {
272 }
274 VariantKind::Tuple(types) => {
275 output.push_str(&heading(
276 variant_heading_level,
277 &format!("`{}`", variant.name),
278 ));
279 if let Some(doc) = &variant.doc {
280 output.push_str(doc);
281 output.push_str("\n\n");
282 }
283 let type_strs: Vec<_> = types
284 .iter()
285 .map(|t| format_type(resolver, t, link_mode))
286 .collect();
287 output.push_str(&format!("**Type:** `({})`\n\n", type_strs.join(", ")));
288 }
289 VariantKind::Struct(fields) => {
290 output.push_str(&heading(
291 variant_heading_level,
292 &format!("`{}`", variant.name),
293 ));
294 if let Some(doc) = &variant.doc {
295 output.push_str(doc);
296 output.push_str("\n\n");
297 }
298 output.push_str("**Fields:**\n\n");
299 output.push_str("| Field | Type | Required | Description |\n");
300 output.push_str("|-------|------|----------|-------------|\n");
301
302 for field in fields {
303 let type_str = format_type(resolver, &field.ty, link_mode);
304 let required = if field.optional { "No" } else { "Yes" };
305 let doc = field.doc.as_deref().unwrap_or("—");
306 let doc_escaped = doc.replace('|', "\\|").replace('\n', " ");
307
308 output.push_str(&format!(
309 "| `{}` | {} | {} | {} |\n",
310 field.name, type_str, required, doc_escaped
311 ));
312 }
313 output.push('\n');
314 }
315 }
316 }
317 }
318 }
319}
320
321fn write_example(
322 output: &mut String,
323 schema: &Schema,
324 max_depth: usize,
325 schemas: &HashMap<&str, &Schema>,
326) {
327 output.push_str("## Example\n\n");
328 output.push_str("```ron\n");
329 output.push_str(&generate_example(schema, max_depth, schemas));
330 output.push_str("\n```\n");
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
338 fn test_write_header_plain() {
339 let mut output = String::new();
340 let schema = Schema::with_doc("Test description", TypeKind::Unit);
341 write_header(&mut output, "TestType", &schema, OutputFormat::Plain);
342 assert_eq!(output, "# TestType\n\n");
343 }
344
345 #[test]
346 fn test_write_header_starlight() {
347 let mut output = String::new();
348 let schema = Schema::with_doc("Test description", TypeKind::Unit);
349 write_header(&mut output, "TestType", &schema, OutputFormat::Starlight);
350 assert!(output.contains("---"));
351 assert!(output.contains("title: TestType"));
352 assert!(output.contains("description: \"Test description\""));
353 }
354
355 #[test]
356 fn test_write_struct_docs() {
357 let mut output = String::new();
358 let fields = vec![
359 Field::new("name", TypeKind::String).with_doc("The name"),
360 Field::optional("age", TypeKind::I32).with_doc("The age"),
361 ];
362 let schemas = vec![];
363 let resolver = LinkResolver::new(&schemas, None);
364 write_struct_docs(&mut output, &fields, &resolver, LinkMode::File, 2);
365
366 assert!(output.contains("## Fields"));
367 assert!(output.contains("| `name` |"));
368 assert!(output.contains("| Yes |"));
369 assert!(output.contains("| `age` |"));
370 assert!(output.contains("| No |"));
371 }
372}