Skip to main content

wowlab_docgen_cli/
lib.rs

1//! Template-driven workspace documentation generation and metadata projection.
2
3#![expect(
4    clippy::multiple_crate_versions,
5    reason = "workspace tooling spans dependency families that have not converged on one version"
6)]
7
8/// Workspace context collection and template-facing projections.
9pub mod context;
10/// Native Rustdoc homepage and canonical catalog generation.
11pub mod hosted_rustdoc;
12/// Metadata, rendering, traversal, and formatting infrastructure.
13pub mod infra;
14mod workspace;
15
16use infra::metadata::Metadata;
17use wowlab_fs::path::Path;
18
19#[rustfmt::skip]
20pub use workspace::{WorkspaceFile, WorkspaceIndex};
21
22/// Context passed to template rendering.
23#[derive(Debug)]
24pub struct RenderCtx<'a> {
25    pub workspace: WorkspaceIndex,
26    pub root: &'a Path,
27    pub dir: &'a Path,
28    pub rel_dir: &'a str,
29    pub metadata: &'a Metadata,
30    pub output_stem: &'a str,
31}
32
33impl RenderCtx<'_> {
34    /// Return the indexed workspace visible to this rendering target.
35    #[must_use]
36    pub fn workspace(&self) -> &WorkspaceIndex {
37        &self.workspace
38    }
39
40    /// Whether the current rendering target is the workspace root.
41    #[must_use]
42    pub fn is_root(&self) -> bool {
43        self.rel_dir.is_empty() || self.rel_dir == "."
44    }
45}
46
47/// Strip the `wowlab-` prefix from a crate name for display.
48#[must_use]
49pub fn display_name(name: &str) -> String {
50    name.strip_prefix("wowlab-").unwrap_or(name).to_string()
51}
52
53/// Sort a slice in place by the `name` field.
54pub fn sort_by_name<T>(items: &mut [T])
55where
56    T: HasName,
57{
58    items.sort_by(|a, b| a.name().cmp(b.name()));
59}
60
61/// Trait for items that have a name field, used by [`sort_by_name`].
62pub trait HasName {
63    /// Return the stable name used for deterministic sorting.
64    fn name(&self) -> &str;
65}
66
67#[cfg(test)]
68pub mod test_helpers {
69    use wowlab_fs::{file, path::Path, walk};
70
71    use crate::{
72        RenderCtx, WorkspaceFile, WorkspaceIndex,
73        infra::metadata::{Metadata, PackageKind},
74    };
75
76    #[must_use]
77    pub fn workspace_at(root: &Path) -> WorkspaceIndex {
78        let (files, _failures) = walk::workspace_source_files(root).into_parts();
79        let entries = files
80            .into_iter()
81            .map(|path| {
82                let contents = file::read_bytes(&path)
83                    .ok()
84                    .and_then(|bytes| String::from_utf8(bytes).ok())
85                    .map(String::into_boxed_str);
86
87                WorkspaceFile { path, contents }
88            })
89            .collect();
90
91        WorkspaceIndex::new(root.to_path_buf(), entries)
92    }
93
94    /// Build a `RenderCtx` for tests with minimal boilerplate.
95    #[must_use]
96    pub fn test_ctx<'a>(
97        _kind: PackageKind,
98        rel_dir: &'a str,
99        stem: &'a str,
100        meta: &'a Metadata,
101    ) -> RenderCtx<'a> {
102        let root = Path::new("/ws");
103        let dir = if rel_dir.is_empty() || rel_dir == "." {
104            root.to_path_buf()
105        } else {
106            root.join(rel_dir)
107        };
108        // LEAK: the path is retained for this process so the test context can borrow it.
109        let dir: &'a Path = Box::leak(Box::new(dir));
110
111        RenderCtx {
112            workspace: WorkspaceIndex::new(root.to_path_buf(), Vec::new()),
113            root,
114            dir,
115            rel_dir,
116            metadata: meta,
117            output_stem: stem,
118        }
119    }
120
121    /// Create a default `Metadata` with the given `PackageKind`.
122    #[must_use]
123    pub fn meta(kind: PackageKind) -> Metadata {
124        Metadata {
125            kind,
126            ..Default::default()
127        }
128    }
129}