forc_doc/doc/
module.rs

1//! Handles the gathering of module information used in navigation and documentation of modules.
2use crate::render::{constant::INDEX_FILENAME, util::format::docstring::create_preview};
3use anyhow::Result;
4use horrorshow::{box_html, Template};
5use std::{fmt::Write, path::PathBuf};
6use sway_core::language::CallPath;
7
8pub(crate) type ModulePrefixes = Vec<String>;
9
10/// Information about a Sway module.
11#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
12pub struct ModuleInfo {
13    /// The preceding module names, used in navigating between modules.
14    pub module_prefixes: ModulePrefixes,
15    /// Doc attributes of a module.
16    /// Renders into the module level docstrings.
17    ///
18    /// ```sway
19    /// //! Module level docstring
20    /// library;
21    /// ```
22    pub(crate) attributes: Option<String>,
23}
24impl ModuleInfo {
25    /// The current module.
26    ///
27    /// Panics if there are no modules.
28    pub(crate) fn location(&self) -> &str {
29        self.module_prefixes
30            .last()
31            .expect("Expected Some module location, found None")
32    }
33    /// The name of the project.
34    ///
35    /// Panics if the project root is missing.
36    pub(crate) fn project_name(&self) -> &str {
37        self.module_prefixes
38            .first()
39            .expect("Expected root module, project root missing")
40    }
41    /// The location of the parent of the current module.
42    ///
43    /// Returns `None` if there is no parent.
44    pub(crate) fn parent(&self) -> Option<&String> {
45        if self.has_parent() {
46            let mut iter = self.module_prefixes.iter();
47            iter.next_back();
48            iter.next_back()
49        } else {
50            None
51        }
52    }
53    /// Determines if the current module has a parent module.
54    fn has_parent(&self) -> bool {
55        self.depth() > 1
56    }
57    pub(crate) fn is_root_module(&self) -> bool {
58        self.location() == self.project_name()
59    }
60    /// Create a qualified path literal String that represents the full path to an item.
61    ///
62    /// Example: `project_name::module::Item`
63    pub(crate) fn to_path_literal_string(&self, item_name: &str, location: &str) -> String {
64        let prefix = self.to_path_literal_prefix(location);
65        match prefix.is_empty() {
66            true => item_name.to_owned(),
67            false => format!("{prefix}::{item_name}"),
68        }
69    }
70    /// Create a path literal prefix from the module prefixes.
71    /// Use in `to_path_literal_string()` to create a full literal path string.
72    ///
73    /// Example: `module::submodule`
74    fn to_path_literal_prefix(&self, location: &str) -> String {
75        let mut iter = self.module_prefixes.iter();
76        for prefix in iter.by_ref() {
77            if prefix == location {
78                break;
79            }
80        }
81        iter.map(String::as_str).collect::<Vec<&str>>().join("::")
82    }
83    /// Renders the [ModuleInfo] into a [CallPath] with anchors. We return this as a `Result<Vec<String>>`
84    /// since the `box_html!` macro returns a closure and no two closures are considered the same type.
85    pub(crate) fn get_anchors(&self) -> Result<Vec<String>> {
86        let mut count = self.depth();
87        let mut rendered_module_anchors = Vec::with_capacity(self.depth());
88        for prefix in &self.module_prefixes {
89            let mut href = (1..count).map(|_| "../").collect::<String>();
90            href.push_str(INDEX_FILENAME);
91            rendered_module_anchors.push(
92                box_html! {
93                    a(class="mod", href=href) {
94                        : prefix;
95                    }
96                    span: "::";
97                }
98                .into_string()?,
99            );
100            count -= 1;
101        }
102        Ok(rendered_module_anchors)
103    }
104    /// Creates a String version of the path to an item,
105    /// used in navigation between pages. The location given is the break point.
106    ///
107    /// This is only used for full path syntax, e.g `module/submodule/file_name.html`.
108    pub(crate) fn file_path_at_location(&self, file_name: &str, location: &str) -> Result<String> {
109        let mut iter = self.module_prefixes.iter();
110        for prefix in iter.by_ref() {
111            if prefix == location {
112                break;
113            }
114        }
115        let mut file_path = iter.collect::<PathBuf>();
116        file_path.push(file_name);
117
118        file_path
119            .to_str()
120            .map(|file_path_str| file_path_str.to_string())
121            .ok_or_else(|| anyhow::anyhow!("There will always be at least the item name"))
122    }
123
124    /// Compares the current `module_info` to the next `module_info` to determine how many directories to go back to make
125    /// the next file path valid, and returns that path as a `String`.
126    ///
127    /// Example:
128    /// ```
129    /// // number of dirs:               [match][    2    ][    1    ]
130    /// let current_location = "project_root/module/submodule1/submodule2/struct.Name.html";
131    /// let next_location    =              "module/other_submodule/enum.Name.html";
132    /// let result           =               "../../other_submodule/enum.Name.html";
133    /// ```
134    /// In this case the first module to match is "module", so we have no need to go back further than that.
135    pub(crate) fn file_path_from_location(
136        &self,
137        file_name: &str,
138        current_module_info: &ModuleInfo,
139        is_external_item: bool,
140    ) -> Result<String> {
141        if is_external_item {
142            let mut new_path = (0..current_module_info.module_prefixes.len())
143                .map(|_| "../")
144                .collect::<String>();
145            write!(new_path, "{}/{}", self.module_prefixes.join("/"), file_name)?;
146            Ok(new_path)
147        } else {
148            let mut mid = 0; // the index to split the module_info from call_path at
149            let mut offset = 0; // the number of directories to go back
150            let mut next_location_iter = self.module_prefixes.iter().rev().enumerate().peekable();
151            while let Some((index, prefix)) = next_location_iter.peek() {
152                for (count, module) in current_module_info.module_prefixes.iter().rev().enumerate()
153                {
154                    if module == *prefix {
155                        offset = count;
156                        mid = self.module_prefixes.len() - index;
157                        break;
158                    }
159                }
160                next_location_iter.next();
161            }
162            let mut new_path = (0..offset).map(|_| "../").collect::<String>();
163            write!(
164                new_path,
165                "{}/{}",
166                self.module_prefixes.split_at(mid).1.join("/"),
167                file_name
168            )?;
169            Ok(new_path)
170        }
171    }
172
173    /// Returns the relative path to the root of the project.
174    ///
175    /// Example:
176    /// ```
177    /// let current_location = "project_root/module/submodule1/submodule2/struct.Name.html";
178    /// let result           = "../..";
179    /// ```
180    /// In this case the first module to match is "module", so we have no need to go back further than that.
181    pub(crate) fn path_to_root(&self) -> String {
182        (0..self.module_prefixes.len())
183            .map(|_| "..")
184            .collect::<Vec<_>>()
185            .join("/")
186    }
187
188    /// Create a path `&str` for navigation from the `module.depth()` & `file_name`.
189    ///
190    /// This is only used for shorthand path syntax, e.g `../../file_name.html`.
191    pub(crate) fn to_html_shorthand_path_string(&self, file_name: &str) -> String {
192        format!("{}{}", self.to_html_path_prefix(), file_name)
193    }
194    /// Create a path prefix `&str` for navigation from the `module.depth()`.
195    fn to_html_path_prefix(&self) -> String {
196        (0..self.depth()).map(|_| "../").collect::<String>()
197    }
198    /// The depth of a module as `usize`.
199    pub(crate) fn depth(&self) -> usize {
200        self.module_prefixes.len()
201    }
202    /// Create a new [ModuleInfo] from a `TyModule`.
203    pub(crate) fn from_ty_module(module_prefixes: Vec<String>, attributes: Option<String>) -> Self {
204        Self {
205            module_prefixes,
206            attributes,
207        }
208    }
209    /// Create a new [ModuleInfo] from a `CallPath`.
210    pub(crate) fn from_call_path(call_path: &CallPath) -> Self {
211        let module_prefixes = call_path
212            .prefixes
213            .iter()
214            .map(|p| p.as_str().to_string())
215            .collect::<Vec<String>>();
216        Self {
217            module_prefixes,
218            attributes: None,
219        }
220    }
221    /// Create a new [ModuleInfo] from a `&[String]`.
222    pub(crate) fn from_vec_str(module_prefixes: &[String]) -> Self {
223        Self {
224            module_prefixes: module_prefixes.to_owned(),
225            attributes: None,
226        }
227    }
228    pub(crate) fn preview_opt(&self) -> Option<String> {
229        create_preview(self.attributes.clone())
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::ModuleInfo;
236
237    #[test]
238    fn test_parent() {
239        let project = String::from("project_name");
240        let module = String::from("module_name");
241        let mut module_vec = vec![project.clone(), module];
242
243        let module_info = ModuleInfo::from_ty_module(module_vec.clone(), None);
244        let project_opt = module_info.parent();
245        assert_eq!(Some(&project), project_opt);
246
247        module_vec.pop();
248        let module_info = ModuleInfo::from_ty_module(module_vec, None);
249        let project_opt = module_info.parent();
250        assert_eq!(None, project_opt);
251    }
252}