forc_doc/render/
mod.rs

1//! Renders [Documentation] to HTML.
2use crate::{
3    doc::{
4        module::{ModuleInfo, ModulePrefixes},
5        Document, Documentation,
6    },
7    render::{
8        index::{AllDocIndex, ModuleIndex},
9        link::{DocLink, DocLinks},
10        title::BlockTitle,
11        util::format::docstring::DocStrings,
12    },
13    RenderPlan,
14};
15use anyhow::Result;
16use horrorshow::{box_html, helper::doctype, html, prelude::*};
17use rayon::prelude::*;
18use std::{
19    collections::BTreeMap,
20    ops::{Deref, DerefMut},
21};
22use sway_core::{language::ty::TyProgramKind, transform::Attributes};
23use sway_types::BaseIdent;
24
25pub mod index;
26pub mod item;
27pub mod link;
28mod search;
29mod sidebar;
30mod title;
31pub mod util;
32
33pub const ALL_DOC_FILENAME: &str = "all.html";
34pub const INDEX_FILENAME: &str = "index.html";
35pub const IDENTITY: &str = "#";
36
37type DocLinkMap = BTreeMap<BlockTitle, Vec<DocLink>>;
38type ModuleMap = BTreeMap<ModulePrefixes, DocLinkMap>;
39type RenderResult = (RenderedDocument, ModuleMap, DocLinks);
40
41/// Something that can be rendered to HTML.
42pub(crate) trait Renderable {
43    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>>;
44}
45impl Renderable for Document {
46    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>> {
47        let header = self.item_header.render(render_plan.clone())?;
48        let body = self.item_body.render(render_plan)?;
49        Ok(box_html! {
50            : header;
51            : body;
52        })
53    }
54}
55
56/// A [Document] rendered to HTML.
57#[derive(Debug)]
58pub struct RenderedDocument {
59    pub module_info: ModuleInfo,
60    pub html_filename: String,
61    pub file_contents: HTMLString,
62}
63impl RenderedDocument {
64    fn from_doc(doc: &Document, render_plan: RenderPlan) -> Result<Self> {
65        Ok(Self {
66            module_info: doc.module_info.clone(),
67            html_filename: doc.html_filename(),
68            file_contents: HTMLString::from_rendered_content(doc.clone().render(render_plan)?)?,
69        })
70    }
71}
72
73#[derive(Default)]
74pub struct RenderedDocumentation(pub Vec<RenderedDocument>);
75
76impl RenderedDocumentation {
77    /// Top level HTML rendering for all [Documentation] of a program.
78    pub fn from_raw_docs(
79        raw_docs: Documentation,
80        render_plan: RenderPlan,
81        root_attributes: Option<Attributes>,
82        program_kind: &TyProgramKind,
83        forc_version: Option<String>,
84    ) -> Result<RenderedDocumentation> {
85        let mut rendered_docs: RenderedDocumentation = RenderedDocumentation::default();
86        let root_module = match raw_docs.0.first() {
87            Some(doc) => ModuleInfo::from_ty_module(
88                vec![doc.module_info.project_name().to_owned()],
89                root_attributes.map(|attrs_map| attrs_map.to_html_string()),
90            ),
91            None => panic!("Project does not contain a root module"),
92        };
93
94        let mut all_docs = DocLinks {
95            style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
96            links: BTreeMap::default(),
97        };
98        // Parallel document rendering
99        let rendered_results: Result<Vec<RenderResult>, anyhow::Error> = raw_docs
100            .0
101            .par_iter()
102            .map(|doc| {
103                let rendered_doc = RenderedDocument::from_doc(doc, render_plan.clone())?;
104                let mut local_module_map = ModuleMap::new();
105                let mut local_all_docs = DocLinks {
106                    style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
107                    links: BTreeMap::default(),
108                };
109
110                populate_decls(doc, &mut local_module_map);
111                populate_modules(doc, &mut local_module_map);
112                populate_doc_links(doc, &mut local_all_docs.links);
113
114                Ok((rendered_doc, local_module_map, local_all_docs))
115            })
116            .collect();
117
118        // Merge results sequentially
119        let mut module_map = ModuleMap::new();
120        for (rendered_doc, local_module_map, local_all_docs) in rendered_results? {
121            rendered_docs.0.push(rendered_doc);
122
123            // Merge module maps without overwriting existing categories; append and dedup links.
124            for (key, value) in local_module_map {
125                let entry = module_map.entry(key).or_default();
126                for (block, mut links) in value {
127                    let list = entry.entry(block).or_default();
128                    // Append new links while avoiding duplicates.
129                    for link in links.drain(..) {
130                        if !list.contains(&link) {
131                            list.push(link);
132                        }
133                    }
134                }
135            }
136
137            // Merge "all docs" links similarly, preserving existing items.
138            for (block, mut links) in local_all_docs.links {
139                let list = all_docs.links.entry(block).or_default();
140                for link in links.drain(..) {
141                    if !list.contains(&link) {
142                        list.push(link);
143                    }
144                }
145            }
146        }
147
148        // ProjectIndex
149        match module_map.get(&root_module.module_prefixes) {
150            Some(doc_links) => rendered_docs.push(RenderedDocument {
151                module_info: root_module.clone(),
152                html_filename: INDEX_FILENAME.to_string(),
153                file_contents: HTMLString::from_rendered_content(
154                    ModuleIndex::new(
155                        forc_version,
156                        root_module.clone(),
157                        DocLinks {
158                            style: DocStyle::ProjectIndex(program_kind.as_title_str().to_string()),
159                            links: doc_links.to_owned(),
160                        },
161                    )
162                    .render(render_plan.clone())?,
163                )?,
164            }),
165            None => panic!("Project does not contain a root module."),
166        }
167        if module_map.len() > 1 {
168            module_map.remove_entry(&root_module.module_prefixes);
169
170            // ModuleIndex(s)
171            for (module_prefixes, doc_links) in module_map {
172                let module_info_opt = match doc_links.values().last() {
173                    Some(doc_links) => doc_links
174                        .first()
175                        .map(|doc_link| doc_link.module_info.clone()),
176                    // No module to be documented
177                    None => None,
178                };
179                if let Some(module_info) = module_info_opt {
180                    rendered_docs.push(RenderedDocument {
181                        module_info: module_info.clone(),
182                        html_filename: INDEX_FILENAME.to_string(),
183                        file_contents: HTMLString::from_rendered_content(
184                            ModuleIndex::new(
185                                None,
186                                module_info.clone(),
187                                DocLinks {
188                                    style: DocStyle::ModuleIndex,
189                                    links: doc_links.to_owned(),
190                                },
191                            )
192                            .render(render_plan.clone())?,
193                        )?,
194                    });
195                    if module_info.module_prefixes != module_prefixes {
196                        let module_info = ModuleInfo::from_ty_module(module_prefixes, None);
197                        rendered_docs.push(RenderedDocument {
198                            module_info: module_info.clone(),
199                            html_filename: INDEX_FILENAME.to_string(),
200                            file_contents: HTMLString::from_rendered_content(
201                                ModuleIndex::new(
202                                    None,
203                                    module_info,
204                                    DocLinks {
205                                        style: DocStyle::ModuleIndex,
206                                        links: doc_links.clone(),
207                                    },
208                                )
209                                .render(render_plan.clone())?,
210                            )?,
211                        });
212                    }
213                }
214            }
215        }
216        // AllDocIndex
217        rendered_docs.push(RenderedDocument {
218            module_info: root_module.clone(),
219            html_filename: ALL_DOC_FILENAME.to_string(),
220            file_contents: HTMLString::from_rendered_content(
221                AllDocIndex::new(root_module, all_docs).render(render_plan)?,
222            )?,
223        });
224
225        Ok(rendered_docs)
226    }
227}
228
229impl Deref for RenderedDocumentation {
230    type Target = Vec<RenderedDocument>;
231    fn deref(&self) -> &Self::Target {
232        &self.0
233    }
234}
235
236impl DerefMut for RenderedDocumentation {
237    fn deref_mut(&mut self) -> &mut Self::Target {
238        &mut self.0
239    }
240}
241
242/// Adds a document's link to the appropriate category in the doc links map.
243fn populate_doc_links(doc: &Document, doc_links: &mut DocLinkMap) {
244    let key = doc.item_body.ty.as_block_title();
245    match doc_links.get_mut(&key) {
246        Some(links) => links.push(doc.link()),
247        None => {
248            doc_links.insert(key, vec![doc.link()]);
249        }
250    }
251}
252/// Organizes document links by module prefix for navigation.
253fn populate_decls(doc: &Document, module_map: &mut ModuleMap) {
254    let module_prefixes = &doc.module_info.module_prefixes;
255    if let Some(doc_links) = module_map.get_mut(module_prefixes) {
256        populate_doc_links(doc, doc_links)
257    } else {
258        let mut doc_links = DocLinkMap::new();
259        populate_doc_links(doc, &mut doc_links);
260        module_map.insert(module_prefixes.clone(), doc_links);
261    }
262}
263/// Creates links to parent modules for hierarchical navigation.
264fn populate_modules(doc: &Document, module_map: &mut ModuleMap) {
265    let mut module_clone = doc.module_info.clone();
266    while module_clone.parent().is_some() {
267        let html_filename = if module_clone.depth() > 2 {
268            format!("{}/{INDEX_FILENAME}", module_clone.location())
269        } else {
270            INDEX_FILENAME.to_string()
271        };
272        let module_link = DocLink {
273            name: module_clone.location().to_owned(),
274            module_info: module_clone.clone(),
275            html_filename,
276            preview_opt: doc.module_info.preview_opt(),
277        };
278        let module_prefixes = module_clone
279            .module_prefixes
280            .clone()
281            .split_last()
282            .unwrap()
283            .1
284            .to_vec();
285        if let Some(doc_links) = module_map.get_mut(&module_prefixes) {
286            match doc_links.get_mut(&BlockTitle::Modules) {
287                Some(links) => {
288                    if !links.contains(&module_link) {
289                        links.push(module_link);
290                    }
291                }
292                None => {
293                    doc_links.insert(BlockTitle::Modules, vec![module_link]);
294                }
295            }
296        } else {
297            let mut doc_links = DocLinkMap::new();
298            doc_links.insert(BlockTitle::Modules, vec![module_link]);
299            module_map.insert(module_prefixes.clone(), doc_links);
300        }
301        module_clone.module_prefixes.pop();
302    }
303}
304
305/// The finalized HTML file contents.
306#[derive(Debug)]
307pub struct HTMLString(pub String);
308impl HTMLString {
309    /// Final rendering of a [Document] HTML page to String.
310    pub fn from_rendered_content(rendered_content: Box<dyn RenderBox>) -> Result<Self> {
311        Ok(Self(
312            html! {
313                : doctype::HTML;
314                html {
315                    : rendered_content
316                }
317            }
318            .into_string()?,
319        ))
320    }
321}
322
323/// The type of document. Helpful in determining what to represent in
324/// the sidebar & page content.
325#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
326pub enum DocStyle {
327    AllDoc(String),
328    ProjectIndex(String),
329    WorkspaceIndex,
330    ModuleIndex,
331    Item {
332        title: Option<BlockTitle>,
333        name: Option<BaseIdent>,
334    },
335}