1use alloc::{
12 string::{String, ToString},
13 vec::Vec,
14};
15
16use crate::error::KernelError;
17
18pub const DEFAULT_DOC_OUTPUT: &str = "/usr/share/doc/rust";
24
25pub const DEFAULT_DOC_SERVER_PORT: u16 = 8080;
27
28pub const MAX_SEARCH_INDEX_ITEMS: usize = 100_000;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum DocTheme {
38 Default,
40 Dark,
42 HighContrast,
44}
45
46impl DocTheme {
47 pub fn flag_value(self) -> &'static str {
49 match self {
50 Self::Default => "light",
51 Self::Dark => "ayu",
52 Self::HighContrast => "high-contrast",
53 }
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum DocItemKind {
64 Function,
65 Struct,
66 Enum,
67 Trait,
68 Module,
69 Macro,
70 Const,
71 Type,
72}
73
74impl DocItemKind {
75 pub fn label(self) -> &'static str {
77 match self {
78 Self::Function => "fn",
79 Self::Struct => "struct",
80 Self::Enum => "enum",
81 Self::Trait => "trait",
82 Self::Module => "mod",
83 Self::Macro => "macro",
84 Self::Const => "const",
85 Self::Type => "type",
86 }
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
92pub struct DocItem {
93 pub name: String,
95 pub kind: DocItemKind,
97 pub path: String,
99 pub description: String,
101 pub is_public: bool,
103}
104
105impl DocItem {
106 pub fn new(
108 name: &str,
109 kind: DocItemKind,
110 path: &str,
111 description: &str,
112 is_public: bool,
113 ) -> Self {
114 Self {
115 name: name.to_string(),
116 kind,
117 path: path.to_string(),
118 description: description.to_string(),
119 is_public,
120 }
121 }
122
123 pub fn html_path(&self) -> String {
127 let module_path = self.path.replace("::", "/");
128 alloc::format!("{}/{}.{}.html", module_path, self.kind.label(), self.name)
129 }
130}
131
132#[derive(Debug, Clone)]
138pub struct DocIndex {
139 items: Vec<DocItem>,
140}
141
142impl Default for DocIndex {
143 fn default() -> Self {
144 Self::new()
145 }
146}
147
148impl DocIndex {
149 pub fn new() -> Self {
151 Self { items: Vec::new() }
152 }
153
154 pub fn add(&mut self, item: DocItem) -> Result<(), KernelError> {
156 if self.items.len() >= MAX_SEARCH_INDEX_ITEMS {
157 return Err(KernelError::ResourceExhausted {
158 resource: "doc_search_index",
159 });
160 }
161 self.items.push(item);
162 Ok(())
163 }
164
165 pub fn len(&self) -> usize {
167 self.items.len()
168 }
169
170 pub fn is_empty(&self) -> bool {
172 self.items.is_empty()
173 }
174
175 pub fn search_by_name(&self, query: &str) -> Vec<&DocItem> {
177 let query_lower = query.to_ascii_lowercase();
178 self.items
179 .iter()
180 .filter(|item| {
181 let name_lower = item.name.to_ascii_lowercase();
182 name_lower.contains(&query_lower)
183 })
184 .collect()
185 }
186
187 pub fn search_by_path(&self, prefix: &str) -> Vec<&DocItem> {
189 self.items
190 .iter()
191 .filter(|item| item.path.starts_with(prefix))
192 .collect()
193 }
194
195 pub fn statistics(&self) -> DocIndexStats {
197 let mut stats = DocIndexStats::default();
198 for item in &self.items {
199 match item.kind {
200 DocItemKind::Function => stats.functions += 1,
201 DocItemKind::Struct => stats.structs += 1,
202 DocItemKind::Enum => stats.enums += 1,
203 DocItemKind::Trait => stats.traits += 1,
204 DocItemKind::Module => stats.modules += 1,
205 DocItemKind::Macro => stats.macros += 1,
206 DocItemKind::Const => stats.consts += 1,
207 DocItemKind::Type => stats.types += 1,
208 }
209 }
210 stats.total = self.items.len();
211 stats
212 }
213}
214
215#[derive(Debug, Clone, Default, PartialEq, Eq)]
217pub struct DocIndexStats {
218 pub total: usize,
219 pub functions: usize,
220 pub structs: usize,
221 pub enums: usize,
222 pub traits: usize,
223 pub modules: usize,
224 pub macros: usize,
225 pub consts: usize,
226 pub types: usize,
227}
228
229#[derive(Debug, Clone)]
235pub struct RustdocConfig {
236 pub source_path: String,
238 pub output_dir: String,
240 pub theme: DocTheme,
242 pub cross_references: bool,
244 pub features: Vec<String>,
246 pub cfg_flags: Vec<String>,
248 pub document_private: bool,
250}
251
252impl Default for RustdocConfig {
253 fn default() -> Self {
254 Self {
255 source_path: String::from("/src"),
256 output_dir: DEFAULT_DOC_OUTPUT.to_string(),
257 theme: DocTheme::Default,
258 cross_references: true,
259 features: Vec::new(),
260 cfg_flags: Vec::new(),
261 document_private: false,
262 }
263 }
264}
265
266impl RustdocConfig {
267 pub fn for_crate(crate_path: &str, output_dir: &str) -> Self {
269 Self {
270 source_path: crate_path.to_string(),
271 output_dir: output_dir.to_string(),
272 ..Self::default()
273 }
274 }
275}
276
277#[derive(Debug, Clone)]
283pub struct DocServerConfig {
284 pub doc_root: String,
286 pub port: u16,
288 pub bind_address: String,
290}
291
292impl Default for DocServerConfig {
293 fn default() -> Self {
294 Self {
295 doc_root: DEFAULT_DOC_OUTPUT.to_string(),
296 port: DEFAULT_DOC_SERVER_PORT,
297 bind_address: "127.0.0.1".to_string(),
298 }
299 }
300}
301
302impl DocServerConfig {
303 pub fn url(&self) -> String {
305 alloc::format!("http://{}:{}", self.bind_address, self.port)
306 }
307}
308
309pub struct RustdocBuilder {
315 config: RustdocConfig,
316 index: DocIndex,
317}
318
319impl RustdocBuilder {
320 pub fn new(config: RustdocConfig) -> Self {
322 Self {
323 config,
324 index: DocIndex::new(),
325 }
326 }
327
328 pub fn config(&self) -> &RustdocConfig {
330 &self.config
331 }
332
333 pub fn index(&self) -> &DocIndex {
335 &self.index
336 }
337
338 pub fn configure_theme(&mut self, theme: DocTheme) {
340 self.config.theme = theme;
341 }
342
343 fn base_command(&self) -> String {
347 let mut cmd = String::from("cargo doc --no-deps");
348
349 if self.config.document_private {
350 cmd.push_str(" --document-private-items");
351 }
352
353 if !self.config.features.is_empty() {
354 cmd.push_str(&alloc::format!(
355 " --features {}",
356 self.config.features.join(",")
357 ));
358 }
359
360 for cfg in &self.config.cfg_flags {
361 cmd.push_str(&alloc::format!(" --cfg {}", cfg));
362 }
363
364 cmd.push_str(&alloc::format!(" --target-dir {}", self.config.output_dir));
365
366 cmd
367 }
368
369 pub fn generate_docs(&self) -> String {
371 self.base_command()
372 }
373
374 pub fn generate_package_docs(&self, package_name: &str) -> String {
377 let base = self.base_command();
378 alloc::format!("{} -p {}", base, package_name)
379 }
380
381 pub fn generate_workspace_docs(&self) -> String {
383 let base = self.base_command();
384 alloc::format!("{} --workspace", base)
385 }
386
387 pub fn generate_search_index(&mut self, items: Vec<DocItem>) -> Result<&DocIndex, KernelError> {
395 self.index = DocIndex::new();
396 for item in items {
397 self.index.add(item)?;
398 }
399 Ok(&self.index)
400 }
401
402 pub fn crate_doc_path(&self, crate_name: &str) -> String {
406 alloc::format!("{}/doc/{}", self.config.output_dir, crate_name)
407 }
408
409 pub fn index_html_path(&self) -> String {
411 alloc::format!("{}/doc/index.html", self.config.output_dir)
412 }
413
414 pub fn server_config(&self, port: u16) -> DocServerConfig {
416 DocServerConfig {
417 doc_root: alloc::format!("{}/doc", self.config.output_dir),
418 port,
419 bind_address: "127.0.0.1".to_string(),
420 }
421 }
422
423 pub fn cross_reference_flags(&self, crate_names: &[&str]) -> Vec<String> {
426 if !self.config.cross_references {
427 return Vec::new();
428 }
429 crate_names
430 .iter()
431 .map(|name| {
432 alloc::format!(
433 "--extern-html-root-url {}={}/doc/{}",
434 name,
435 self.config.output_dir,
436 name
437 )
438 })
439 .collect()
440 }
441}
442
443#[allow(dead_code)]
449trait AsciiLowerExt {
450 fn to_ascii_lowercase(&self) -> String;
451}
452
453impl AsciiLowerExt for str {
454 fn to_ascii_lowercase(&self) -> String {
455 let mut s = String::with_capacity(self.len());
456 for c in self.chars() {
457 if c.is_ascii_uppercase() {
458 s.push((c as u8 + 32) as char);
459 } else {
460 s.push(c);
461 }
462 }
463 s
464 }
465}
466
467#[cfg(test)]
472mod tests {
473 #[allow(unused_imports)]
474 use alloc::vec;
475
476 use super::*;
477
478 #[test]
479 fn test_rustdoc_config_default() {
480 let config = RustdocConfig::default();
481 assert_eq!(config.output_dir, DEFAULT_DOC_OUTPUT);
482 assert_eq!(config.theme, DocTheme::Default);
483 assert!(config.cross_references);
484 assert!(!config.document_private);
485 assert!(config.features.is_empty());
486 }
487
488 #[test]
489 fn test_generate_docs_command() {
490 let builder = RustdocBuilder::new(RustdocConfig::default());
491 let cmd = builder.generate_docs();
492 assert!(cmd.contains("cargo doc --no-deps"));
493 assert!(cmd.contains("--target-dir"));
494 assert!(cmd.contains(DEFAULT_DOC_OUTPUT));
495 }
496
497 #[test]
498 fn test_generate_package_docs() {
499 let builder = RustdocBuilder::new(RustdocConfig::default());
500 let cmd = builder.generate_package_docs("veridian-kernel");
501 assert!(cmd.contains("-p veridian-kernel"));
502 assert!(cmd.contains("cargo doc"));
503 }
504
505 #[test]
506 fn test_generate_workspace_docs() {
507 let builder = RustdocBuilder::new(RustdocConfig::default());
508 let cmd = builder.generate_workspace_docs();
509 assert!(cmd.contains("--workspace"));
510 }
511
512 #[test]
513 fn test_search_index_build_and_query() {
514 let mut builder = RustdocBuilder::new(RustdocConfig::default());
515 let items = vec![
516 DocItem::new(
517 "HashMap",
518 DocItemKind::Struct,
519 "std::collections",
520 "A hash map",
521 true,
522 ),
523 DocItem::new(
524 "hash_map",
525 DocItemKind::Module,
526 "std::collections",
527 "Hash map module",
528 true,
529 ),
530 DocItem::new(
531 "Vec",
532 DocItemKind::Struct,
533 "alloc::vec",
534 "A growable array",
535 true,
536 ),
537 ];
538 let index = builder.generate_search_index(items).unwrap();
539 assert_eq!(index.len(), 3);
540
541 let results = index.search_by_name("hash");
542 assert_eq!(results.len(), 2);
543
544 let results = index.search_by_path("std::collections");
545 assert_eq!(results.len(), 2);
546 }
547
548 #[test]
549 fn test_doc_index_statistics() {
550 let mut index = DocIndex::new();
551 index
552 .add(DocItem::new(
553 "foo",
554 DocItemKind::Function,
555 "crate",
556 "desc",
557 true,
558 ))
559 .unwrap();
560 index
561 .add(DocItem::new(
562 "Bar",
563 DocItemKind::Struct,
564 "crate",
565 "desc",
566 true,
567 ))
568 .unwrap();
569 index
570 .add(DocItem::new(
571 "Baz",
572 DocItemKind::Enum,
573 "crate",
574 "desc",
575 true,
576 ))
577 .unwrap();
578 let stats = index.statistics();
579 assert_eq!(stats.total, 3);
580 assert_eq!(stats.functions, 1);
581 assert_eq!(stats.structs, 1);
582 assert_eq!(stats.enums, 1);
583 }
584
585 #[test]
586 fn test_configure_theme_and_flags() {
587 let mut builder = RustdocBuilder::new(RustdocConfig::default());
588 builder.configure_theme(DocTheme::Dark);
589 assert_eq!(builder.config().theme, DocTheme::Dark);
590 assert_eq!(DocTheme::Dark.flag_value(), "ayu");
591 assert_eq!(DocTheme::HighContrast.flag_value(), "high-contrast");
592 }
593
594 #[test]
595 fn test_doc_server_config() {
596 let builder = RustdocBuilder::new(RustdocConfig::default());
597 let server = builder.server_config(9090);
598 assert_eq!(server.port, 9090);
599 assert_eq!(server.url(), "http://127.0.0.1:9090");
600 assert!(server.doc_root.contains("doc"));
601 }
602}