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 std::{
18    collections::BTreeMap,
19    ops::{Deref, DerefMut},
20};
21use sway_core::{language::ty::TyProgramKind, transform::Attributes};
22use sway_types::BaseIdent;
23
24pub mod index;
25pub mod item;
26pub mod link;
27mod search;
28mod sidebar;
29mod title;
30pub mod util;
31
32pub const ALL_DOC_FILENAME: &str = "all.html";
33pub const INDEX_FILENAME: &str = "index.html";
34pub const IDENTITY: &str = "#";
35
36/// Something that can be rendered to HTML.
37pub(crate) trait Renderable {
38    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>>;
39}
40impl Renderable for Document {
41    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>> {
42        let header = self.item_header.render(render_plan.clone())?;
43        let body = self.item_body.render(render_plan)?;
44        Ok(box_html! {
45            : header;
46            : body;
47        })
48    }
49}
50
51/// A [Document] rendered to HTML.
52#[derive(Debug)]
53pub struct RenderedDocument {
54    pub module_info: ModuleInfo,
55    pub html_filename: String,
56    pub file_contents: HTMLString,
57}
58impl RenderedDocument {
59    fn from_doc(doc: &Document, render_plan: RenderPlan) -> Result<Self> {
60        Ok(Self {
61            module_info: doc.module_info.clone(),
62            html_filename: doc.html_filename(),
63            file_contents: HTMLString::from_rendered_content(doc.clone().render(render_plan)?)?,
64        })
65    }
66}
67
68#[derive(Default)]
69pub struct RenderedDocumentation(pub Vec<RenderedDocument>);
70
71impl RenderedDocumentation {
72    /// Top level HTML rendering for all [Documentation] of a program.
73    pub fn from_raw_docs(
74        raw_docs: Documentation,
75        render_plan: RenderPlan,
76        root_attributes: Option<Attributes>,
77        program_kind: &TyProgramKind,
78        forc_version: Option<String>,
79    ) -> Result<RenderedDocumentation> {
80        let mut rendered_docs: RenderedDocumentation = RenderedDocumentation::default();
81        let root_module = match raw_docs.0.first() {
82            Some(doc) => ModuleInfo::from_ty_module(
83                vec![doc.module_info.project_name().to_owned()],
84                root_attributes.map(|attrs_map| attrs_map.to_html_string()),
85            ),
86            None => panic!("Project does not contain a root module"),
87        };
88
89        let mut all_docs = DocLinks {
90            style: DocStyle::AllDoc(program_kind.as_title_str().to_string()),
91            links: BTreeMap::default(),
92        };
93        let mut module_map: BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>> =
94            BTreeMap::new();
95        for doc in raw_docs.0 {
96            rendered_docs
97                .0
98                .push(RenderedDocument::from_doc(&doc, render_plan.clone())?);
99
100            // Here we gather all of the `doc_links` based on which module they belong to.
101            populate_decls(&doc, &mut module_map);
102            // Create links to child modules.
103            populate_modules(&doc, &mut module_map);
104            // Above we check for the module a link belongs to, here we want _all_ links so the check is much more shallow.
105            populate_all_doc(&doc, &mut all_docs);
106        }
107
108        // ProjectIndex
109        match module_map.get(&root_module.module_prefixes) {
110            Some(doc_links) => rendered_docs.push(RenderedDocument {
111                module_info: root_module.clone(),
112                html_filename: INDEX_FILENAME.to_string(),
113                file_contents: HTMLString::from_rendered_content(
114                    ModuleIndex::new(
115                        forc_version,
116                        root_module.clone(),
117                        DocLinks {
118                            style: DocStyle::ProjectIndex(program_kind.as_title_str().to_string()),
119                            links: doc_links.to_owned(),
120                        },
121                    )
122                    .render(render_plan.clone())?,
123                )?,
124            }),
125            None => panic!("Project does not contain a root module."),
126        }
127        if module_map.len() > 1 {
128            module_map.remove_entry(&root_module.module_prefixes);
129
130            // ModuleIndex(s)
131            for (module_prefixes, doc_links) in module_map {
132                let module_info_opt = match doc_links.values().last() {
133                    Some(doc_links) => doc_links
134                        .first()
135                        .map(|doc_link| doc_link.module_info.clone()),
136                    // No module to be documented
137                    None => None,
138                };
139                if let Some(module_info) = module_info_opt {
140                    rendered_docs.push(RenderedDocument {
141                        module_info: module_info.clone(),
142                        html_filename: INDEX_FILENAME.to_string(),
143                        file_contents: HTMLString::from_rendered_content(
144                            ModuleIndex::new(
145                                None,
146                                module_info.clone(),
147                                DocLinks {
148                                    style: DocStyle::ModuleIndex,
149                                    links: doc_links.to_owned(),
150                                },
151                            )
152                            .render(render_plan.clone())?,
153                        )?,
154                    });
155                    if module_info.module_prefixes != module_prefixes {
156                        let module_info = ModuleInfo::from_ty_module(module_prefixes, None);
157                        rendered_docs.push(RenderedDocument {
158                            module_info: module_info.clone(),
159                            html_filename: INDEX_FILENAME.to_string(),
160                            file_contents: HTMLString::from_rendered_content(
161                                ModuleIndex::new(
162                                    None,
163                                    module_info,
164                                    DocLinks {
165                                        style: DocStyle::ModuleIndex,
166                                        links: doc_links.clone(),
167                                    },
168                                )
169                                .render(render_plan.clone())?,
170                            )?,
171                        });
172                    }
173                }
174            }
175        }
176        // AllDocIndex
177        rendered_docs.push(RenderedDocument {
178            module_info: root_module.clone(),
179            html_filename: ALL_DOC_FILENAME.to_string(),
180            file_contents: HTMLString::from_rendered_content(
181                AllDocIndex::new(root_module, all_docs).render(render_plan)?,
182            )?,
183        });
184
185        Ok(rendered_docs)
186    }
187}
188
189impl Deref for RenderedDocumentation {
190    type Target = Vec<RenderedDocument>;
191    fn deref(&self) -> &Self::Target {
192        &self.0
193    }
194}
195
196impl DerefMut for RenderedDocumentation {
197    fn deref_mut(&mut self) -> &mut Self::Target {
198        &mut self.0
199    }
200}
201
202fn populate_doc_links(doc: &Document, doc_links: &mut BTreeMap<BlockTitle, Vec<DocLink>>) {
203    let key = doc.item_body.ty.as_block_title();
204    match doc_links.get_mut(&key) {
205        Some(links) => links.push(doc.link()),
206        None => {
207            doc_links.insert(key, vec![doc.link()]);
208        }
209    }
210}
211fn populate_decls(
212    doc: &Document,
213    module_map: &mut BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>>,
214) {
215    let module_prefixes = &doc.module_info.module_prefixes;
216    if let Some(doc_links) = module_map.get_mut(module_prefixes) {
217        populate_doc_links(doc, doc_links)
218    } else {
219        let mut doc_links: BTreeMap<BlockTitle, Vec<DocLink>> = BTreeMap::new();
220        populate_doc_links(doc, &mut doc_links);
221        module_map.insert(module_prefixes.clone(), doc_links);
222    }
223}
224fn populate_modules(
225    doc: &Document,
226    module_map: &mut BTreeMap<ModulePrefixes, BTreeMap<BlockTitle, Vec<DocLink>>>,
227) {
228    let mut module_clone = doc.module_info.clone();
229    while module_clone.parent().is_some() {
230        let html_filename = if module_clone.depth() > 2 {
231            format!("{}/{INDEX_FILENAME}", module_clone.location())
232        } else {
233            INDEX_FILENAME.to_string()
234        };
235        let module_link = DocLink {
236            name: module_clone.location().to_owned(),
237            module_info: module_clone.clone(),
238            html_filename,
239            preview_opt: doc.module_info.preview_opt(),
240        };
241        let module_prefixes = module_clone
242            .module_prefixes
243            .clone()
244            .split_last()
245            .unwrap()
246            .1
247            .to_vec();
248        if let Some(doc_links) = module_map.get_mut(&module_prefixes) {
249            match doc_links.get_mut(&BlockTitle::Modules) {
250                Some(links) => {
251                    if !links.contains(&module_link) {
252                        links.push(module_link);
253                    }
254                }
255                None => {
256                    doc_links.insert(BlockTitle::Modules, vec![module_link]);
257                }
258            }
259        } else {
260            let mut doc_links: BTreeMap<BlockTitle, Vec<DocLink>> = BTreeMap::new();
261            doc_links.insert(BlockTitle::Modules, vec![module_link]);
262            module_map.insert(module_prefixes.clone(), doc_links);
263        }
264        module_clone.module_prefixes.pop();
265    }
266}
267fn populate_all_doc(doc: &Document, all_docs: &mut DocLinks) {
268    populate_doc_links(doc, &mut all_docs.links);
269}
270
271/// The finalized HTML file contents.
272#[derive(Debug)]
273pub struct HTMLString(pub String);
274impl HTMLString {
275    /// Final rendering of a [Document] HTML page to String.
276    pub fn from_rendered_content(rendered_content: Box<dyn RenderBox>) -> Result<Self> {
277        Ok(Self(
278            html! {
279                : doctype::HTML;
280                html {
281                    : rendered_content
282                }
283            }
284            .into_string()?,
285        ))
286    }
287}
288
289/// The type of document. Helpful in determining what to represent in
290/// the sidebar & page content.
291#[derive(Clone, Ord, PartialOrd, Eq, PartialEq)]
292pub enum DocStyle {
293    AllDoc(String),
294    ProjectIndex(String),
295    WorkspaceIndex,
296    ModuleIndex,
297    Item {
298        title: Option<BlockTitle>,
299        name: Option<BaseIdent>,
300    },
301}