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