cargo_doc_docusaurus/
writer.rs1use crate::converter::MarkdownOutput;
4use anyhow::{Context, Result};
5use std::fs;
6use std::path::Path;
7
8pub fn write_markdown(output_dir: &Path, content: &str) -> Result<()> {
10 fs::create_dir_all(output_dir).with_context(|| {
11 format!(
12 "Failed to create output directory: {}",
13 output_dir.display()
14 )
15 })?;
16
17 let output_file = output_dir.join("index.md");
18
19 fs::write(&output_file, content)
20 .with_context(|| format!("Failed to write file: {}", output_file.display()))?;
21
22 Ok(())
23}
24
25pub fn write_markdown_multifile(output_dir: &Path, output: &MarkdownOutput) -> Result<()> {
27 write_markdown_multifile_with_sidebar_path(output_dir, output, None)
28}
29
30pub fn write_markdown_multifile_with_sidebar_path(
32 output_dir: &Path,
33 output: &MarkdownOutput,
34 custom_sidebar_path: Option<&Path>,
35) -> Result<()> {
36 fs::create_dir_all(output_dir).with_context(|| {
37 format!(
38 "Failed to create output directory: {}",
39 output_dir.display()
40 )
41 })?;
42
43 for (file_path, content) in &output.files {
44 let full_path = output_dir.join(file_path);
45
46 if let Some(parent) = full_path.parent() {
48 fs::create_dir_all(parent)
49 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
50 }
51
52 fs::write(&full_path, content)
53 .with_context(|| format!("Failed to write file: {}", full_path.display()))?;
54 }
55
56 if let Some(sidebar_content) = &output.sidebar {
58 let sidebar_path = if let Some(custom_path) = custom_sidebar_path {
59 custom_path.to_path_buf()
60 } else {
61 if let Some(parent) = output_dir.parent() {
64 if let Some(grandparent) = parent.parent() {
65 grandparent.join("sidebars-rust.ts")
66 } else {
67 parent.join("sidebars-rust.ts")
68 }
69 } else {
70 output_dir.join("sidebars-rust.ts")
71 }
72 };
73
74 let final_content = if sidebar_path.exists() {
76 merge_sidebar_content(&sidebar_path, sidebar_content, &output.crate_name)?
77 } else {
78 sidebar_content.clone()
79 };
80
81 if let Some(parent) = sidebar_path.parent() {
83 fs::create_dir_all(parent)
84 .with_context(|| format!("Failed to create directory: {}", parent.display()))?;
85 }
86
87 fs::write(&sidebar_path, final_content)
88 .with_context(|| format!("Failed to write sidebar file: {}", sidebar_path.display()))?;
89
90 println!(
91 "✓ Generated sidebar configuration: {}",
92 sidebar_path.display()
93 );
94 println!(" Import it in your sidebars.ts file:");
95 println!(" import {{rustApiCategory}} from './sidebars-rust';");
96 }
97
98 Ok(())
99}
100
101fn merge_sidebar_content(
103 existing_path: &Path,
104 new_content: &str,
105 _crate_name: &str,
106) -> Result<String> {
107 let existing_content = fs::read_to_string(existing_path).with_context(|| {
110 format!(
111 "Failed to read existing sidebar: {}",
112 existing_path.display()
113 )
114 })?;
115
116 merge_rust_sidebars(&existing_content, new_content)
118}
119
120fn merge_rust_sidebars(existing: &str, new_content: &str) -> Result<String> {
122 let existing_start = "export const rustSidebars: Record<string, any[]> = {";
124 let existing_end = "};";
125
126 let existing_entries = if let Some(start_pos) = existing.find(existing_start) {
128 let start = start_pos + existing_start.len();
129 if let Some(end_pos) = existing[start..].find(existing_end) {
130 &existing[start..start + end_pos]
131 } else {
132 ""
133 }
134 } else {
135 ""
136 };
137
138 let new_start = "export const rustSidebars: Record<string, any[]> = {";
140 let new_end = "};";
141
142 let new_entries = if let Some(start_pos) = new_content.find(new_start) {
143 let start = start_pos + new_start.len();
144 if let Some(end_pos) = new_content[start..].find(new_end) {
145 &new_content[start..start + end_pos]
146 } else {
147 anyhow::bail!("Could not find rustSidebars object end in new content");
148 }
149 } else {
150 anyhow::bail!("Could not find rustSidebars object in new content");
151 };
152
153 let header_end = new_content.find(new_start).unwrap() + new_start.len();
155 let header = &new_content[..header_end];
156
157 let footer_start = header_end + new_entries.len() + new_end.len();
159 let footer = &new_content[footer_start..];
160
161 let mut entries_map: std::collections::HashMap<String, String> = std::collections::HashMap::new();
164
165 let parse_entries = |content: &str| -> Vec<(String, String)> {
167 let mut results = Vec::new();
168 let mut current_key = String::new();
169 let mut current_value = String::new();
170 let mut brace_depth = 0;
171 let mut in_value = false;
172
173 for line in content.lines() {
174 let trimmed = line.trim();
175
176 if trimmed.starts_with('\'') && trimmed.contains("': [") {
178 if !current_key.is_empty() {
180 results.push((current_key.clone(), current_value.clone()));
181 }
182
183 if let Some(end_quote) = trimmed[1..].find('\'') {
185 current_key = trimmed[1..=end_quote].to_string();
186 current_value = line.to_string() + "\n";
187 in_value = true;
188 brace_depth = 1; }
190 } else if in_value {
191 current_value.push_str(line);
192 current_value.push('\n');
193
194 for ch in line.chars() {
196 match ch {
197 '[' | '{' => brace_depth += 1,
198 ']' | '}' => {
199 brace_depth -= 1;
200 if brace_depth == 0 {
201 results.push((current_key.clone(), current_value.clone()));
203 current_key.clear();
204 current_value.clear();
205 in_value = false;
206 }
207 }
208 _ => {}
209 }
210 }
211 }
212 }
213
214 if !current_key.is_empty() {
216 results.push((current_key, current_value));
217 }
218
219 results
220 };
221
222 for (key, value) in parse_entries(existing_entries) {
224 entries_map.insert(key, value);
225 }
226
227 for (key, value) in parse_entries(new_entries) {
229 entries_map.insert(key, value); }
231
232 let mut sorted_keys: Vec<_> = entries_map.keys().collect();
234 sorted_keys.sort();
235
236 let mut merged_entries = String::new();
237 for (i, key) in sorted_keys.iter().enumerate() {
238 if let Some(value) = entries_map.get(*key) {
239 merged_entries.push_str(value);
240 if i < sorted_keys.len() - 1 && !value.trim_end().ends_with(',') {
242 merged_entries.push(',');
243 }
244 }
245 }
246
247 let root_sidebar = generate_root_sidebar(&merged_entries);
249
250 let mut result = String::new();
252 result.push_str(header);
253 result.push_str(&merged_entries);
254 result.push_str(new_end);
255 result.push_str(footer);
256
257 result.push_str("\n\n// Root sidebar with links to all crates (for main navigation)\n");
259 result.push_str("export const rootRustSidebar = [\n");
260 result.push_str(&root_sidebar);
261 result.push_str("];\n");
262
263 Ok(result)
264}
265
266fn generate_root_sidebar(merged_entries: &str) -> String {
269 let mut output = String::new();
270
271 use std::collections::HashSet;
276 let mut seen_crates: HashSet<String> = HashSet::new();
277 let mut crate_entries: Vec<(String, String)> = Vec::new(); for line in merged_entries.lines() {
280 let trimmed = line.trim();
281
282 if trimmed.contains("rustCrateTitle: true") && trimmed.contains("type: 'doc'") {
284 if let Some(id_start) = trimmed.find("id: '") {
288 if let Some(id_end) = trimmed[id_start + 5..].find('\'') {
289 let doc_id = &trimmed[id_start + 5..id_start + 5 + id_end];
290
291 if let Some(label_start) = trimmed.find("label: '") {
293 if let Some(label_end) = trimmed[label_start + 8..].find('\'') {
294 let label = &trimmed[label_start + 8..label_start + 8 + label_end];
295
296 if seen_crates.insert(doc_id.to_string()) {
298 crate_entries.push((doc_id.to_string(), label.to_string()));
299 }
300 }
301 }
302 }
303 }
304 }
305 }
306
307 crate_entries.sort_by(|a, b| a.0.cmp(&b.0));
309
310 for (doc_id, label) in crate_entries {
311 output.push_str(&format!(
312 " {{ type: 'doc', id: '{}', label: '{}', className: 'rust-mod' }},\n",
313 doc_id, label
314 ));
315 }
316
317 output
318}