forc_doc/render/
index.rs

1//! Handles creation of `index.html` files.
2use crate::{
3    doc::module::ModuleInfo,
4    render::{
5        link::DocLinks, search::generate_searchbar, sidebar::*, BlockTitle, DocStyle, Renderable,
6        IDENTITY,
7    },
8    RenderPlan, ASSETS_DIR_NAME,
9};
10use anyhow::Result;
11use horrorshow::{box_html, Raw, RenderBox};
12use std::collections::BTreeMap;
13
14/// Information about a documented library including its name and description
15#[derive(Clone, Debug, PartialEq)]
16pub struct LibraryInfo {
17    pub name: String,
18    pub description: String,
19}
20
21/// Workspace level index page
22#[derive(Clone)]
23pub(crate) struct WorkspaceIndex {
24    /// The workspace root module info
25    workspace_info: ModuleInfo,
26    /// All documented libraries in the workspace with their descriptions
27    documented_libraries: Vec<LibraryInfo>,
28}
29impl WorkspaceIndex {
30    pub(crate) fn new(workspace_info: ModuleInfo, documented_libraries: Vec<LibraryInfo>) -> Self {
31        Self {
32            workspace_info,
33            documented_libraries,
34        }
35    }
36}
37impl SidebarNav for WorkspaceIndex {
38    fn sidebar(&self) -> Sidebar {
39        // Create empty doc links for workspace sidebar
40        let doc_links = DocLinks {
41            style: DocStyle::WorkspaceIndex,
42            links: BTreeMap::new(),
43        };
44
45        Sidebar::new(
46            None,
47            DocStyle::WorkspaceIndex,
48            self.workspace_info.clone(),
49            doc_links,
50        )
51    }
52}
53impl Renderable for WorkspaceIndex {
54    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>> {
55        let sidebar = self.sidebar().render(render_plan)?;
56
57        // For workspace index, we're at the root level, so no path prefix needed
58        let assets_path = ASSETS_DIR_NAME.to_string();
59
60        // Create a custom searchbar for workspace
61        let workspace_searchbar = box_html! {
62            script(src="search.js", type="text/javascript");
63            script {
64                : Raw(r#"
65                function onSearchFormSubmit(event) {
66                    event.preventDefault();
67                    const searchQuery = document.getElementById("search-input").value;
68                    const url = new URL(window.location.href);
69                    if (searchQuery) {
70                        url.searchParams.set('search', searchQuery);
71                    } else {
72                        url.searchParams.delete('search');
73                    }
74                    history.pushState({ search: searchQuery }, "", url);
75                    window.dispatchEvent(new HashChangeEvent("hashchange"));
76                }
77                
78                document.addEventListener('DOMContentLoaded', () => {
79                    const searchbar = document.getElementById("search-input");
80                    searchbar.addEventListener("keyup", function(event) {
81                        onSearchFormSubmit(event);
82                    });
83                    searchbar.addEventListener("search", function(event) {
84                        onSearchFormSubmit(event);
85                    });
86                
87                    function onQueryParamsChange() {
88                        const searchParams = new URLSearchParams(window.location.search);
89                        const query = searchParams.get("search");
90                        const searchSection = document.getElementById('search');
91                        const mainSection = document.getElementById('main-content');
92                        const searchInput = document.getElementById('search-input');
93                        if (query) {
94                            searchInput.value = query;
95                            const results = Object.values(SEARCH_INDEX).flat().filter(item => {
96                                const lowerQuery = query.toLowerCase();
97                                return item.name.toLowerCase().includes(lowerQuery);
98                            });
99                            const header = `<h1>Results for ${query}</h1>`;
100                            if (results.length > 0) {
101                                const resultList = results.map(item => {
102                                    const formattedName = `<span class="type ${item.type_name}">${item.name}</span>`;
103                                    const name = item.type_name === "module"
104                                        ? [...item.module_info.slice(0, -1), formattedName].join("::")
105                                        : [...item.module_info, formattedName].join("::");
106                                    // Fix path generation for workspace - no leading slash, proper relative path
107                                    const path = [...item.module_info, item.html_filename].join("/");
108                                    const left = `<td><span>${name}</span></td>`;
109                                    const right = `<td><p>${item.preview}</p></td>`;
110                                    return `<tr onclick="window.location='${path}';">${left}${right}</tr>`;
111                                }).join('');
112                                searchSection.innerHTML = `${header}<table>${resultList}</table>`;
113                            } else {
114                                searchSection.innerHTML = `${header}<p>No results found.</p>`;
115                            }
116                            searchSection.setAttribute("class", "search-results");
117                            mainSection.setAttribute("class", "content hidden");
118                        } else {
119                            searchSection.setAttribute("class", "search-results hidden");
120                            mainSection.setAttribute("class", "content");
121                        }
122                    }
123                    window.addEventListener('hashchange', onQueryParamsChange);
124                    onQueryParamsChange();
125                });
126                "#)
127            }
128            nav(class="sub") {
129                form(id="search-form", class="search-form", onsubmit="onSearchFormSubmit(event)") {
130                    div(class="search-container") {
131                        input(
132                            id="search-input",
133                            class="search-input",
134                            name="search",
135                            autocomplete="off",
136                            spellcheck="false",
137                            placeholder="Search the docs...",
138                            type="search"
139                        );
140                    }
141                }
142            }
143        };
144
145        Ok(box_html! {
146            head {
147                meta(charset="utf-8");
148                meta(name="viewport", content="width=device-width, initial-scale=1.0");
149                meta(name="generator", content="swaydoc");
150                meta(
151                    name="description",
152                    content="Workspace documentation index"
153                );
154                meta(name="keywords", content="sway, swaylang, sway-lang, workspace");
155                link(rel="icon", href=format!("{}/sway-logo.svg", assets_path));
156                title: "Workspace Documentation";
157                link(rel="stylesheet", type="text/css", href=format!("{}/normalize.css", assets_path));
158                link(rel="stylesheet", type="text/css", href=format!("{}/swaydoc.css", assets_path), id="mainThemeStyle");
159                link(rel="stylesheet", type="text/css", href=format!("{}/ayu.css", assets_path));
160                link(rel="stylesheet", href=format!("{}/ayu.min.css", assets_path));
161            }
162            body(class="swaydoc mod") {
163                : sidebar;
164                main {
165                    div(class="width-limiter") {
166                        : *workspace_searchbar;
167                        section(id="main-content", class="content") {
168                            div(class="main-heading") {
169                                p { : "This workspace contains the following libraries:" }
170                            }
171                            h2(class="small-section-header") {
172                                : "Libraries";
173                            }
174                            div(class="item-table") {
175                                @ for lib in &self.documented_libraries {
176                                    div(class="item-row") {
177                                        div(class="item-left module-item") {
178                                            a(class="mod", href=format!("{}/index.html", lib.name)) {
179                                                : &lib.name;
180                                            }
181                                        }
182                                        div(class="item-right docblock-short") {
183                                            : &lib.description;
184                                        }
185                                    }
186                                }
187                            }
188                        }
189                        section(id="search", class="search-results");
190                    }
191                }
192                script(src=format!("{}/highlight.js", assets_path));
193                script {
194                    : "hljs.highlightAll();";
195                }
196            }
197        })
198    }
199}
200
201/// Project level, all items belonging to a project
202#[derive(Clone)]
203pub(crate) struct AllDocIndex {
204    /// A [ModuleInfo] with only the project name.
205    project_name: ModuleInfo,
206    /// All doc items.
207    all_docs: DocLinks,
208}
209impl AllDocIndex {
210    pub(crate) fn new(project_name: ModuleInfo, all_docs: DocLinks) -> Self {
211        Self {
212            project_name,
213            all_docs,
214        }
215    }
216}
217impl SidebarNav for AllDocIndex {
218    fn sidebar(&self) -> Sidebar {
219        Sidebar::new(
220            None,
221            self.all_docs.style.clone(),
222            self.project_name.clone(),
223            self.all_docs.clone(),
224        )
225    }
226}
227impl Renderable for AllDocIndex {
228    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>> {
229        let doc_links = self.all_docs.clone().render(render_plan.clone())?;
230        let sidebar = self.sidebar().render(render_plan)?;
231        Ok(box_html! {
232            head {
233                meta(charset="utf-8");
234                meta(name="viewport", content="width=device-width, initial-scale=1.0");
235                meta(name="generator", content="swaydoc");
236                meta(
237                    name="description",
238                    content="List of all items in this project"
239                );
240                meta(name="keywords", content="sway, swaylang, sway-lang");
241                link(rel="icon", href=format!("../{ASSETS_DIR_NAME}/sway-logo.svg"));
242                title: "List of all items in this project";
243                link(rel="stylesheet", type="text/css", href=format!("../{ASSETS_DIR_NAME}/normalize.css"));
244                link(rel="stylesheet", type="text/css", href=format!("../{ASSETS_DIR_NAME}/swaydoc.css"), id="mainThemeStyle");
245                link(rel="stylesheet", type="text/css", href=format!("../{ASSETS_DIR_NAME}/ayu.css"));
246                link(rel="stylesheet", href=format!("../{ASSETS_DIR_NAME}/ayu.min.css"));
247            }
248            body(class="swaydoc mod") {
249                : sidebar;
250                main {
251                    div(class="width-limiter") {
252                        : generate_searchbar(&self.project_name);
253                        section(id="main-content", class="content") {
254                            h1(class="fqn") {
255                                span(class="in-band") { : "List of all items" }
256                            }
257                            : doc_links;
258                        }
259                        section(id="search", class="search-results");
260                    }
261                }
262                script(src=format!("../{ASSETS_DIR_NAME}/highlight.js"));
263                script {
264                    : "hljs.highlightAll();";
265                }
266            }
267        })
268    }
269}
270
271/// The index for each module in a Sway project.
272pub(crate) struct ModuleIndex {
273    /// used only for the root module
274    version_opt: Option<String>,
275    module_info: ModuleInfo,
276    module_docs: DocLinks,
277}
278impl ModuleIndex {
279    pub(crate) fn new(
280        version_opt: Option<String>,
281        module_info: ModuleInfo,
282        module_docs: DocLinks,
283    ) -> Self {
284        Self {
285            version_opt,
286            module_info,
287            module_docs,
288        }
289    }
290}
291impl SidebarNav for ModuleIndex {
292    fn sidebar(&self) -> Sidebar {
293        let style = if self.module_info.is_root_module() {
294            self.module_docs.style.clone()
295        } else {
296            DocStyle::ModuleIndex
297        };
298        Sidebar::new(
299            self.version_opt.clone(),
300            style,
301            self.module_info.clone(),
302            self.module_docs.clone(),
303        )
304    }
305}
306impl Renderable for ModuleIndex {
307    fn render(self, render_plan: RenderPlan) -> Result<Box<dyn RenderBox>> {
308        let doc_links = self.module_docs.clone().render(render_plan.clone())?;
309        let sidebar = self.sidebar().render(render_plan)?;
310        let title_prefix = match self.module_docs.style {
311            DocStyle::ProjectIndex(ref program_type) => format!("{program_type} "),
312            DocStyle::ModuleIndex => "Module ".to_string(),
313            _ => unreachable!("Module Index can only be either a project or module at this time."),
314        };
315
316        let favicon = self
317            .module_info
318            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/sway-logo.svg"));
319        let normalize = self
320            .module_info
321            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/normalize.css"));
322        let swaydoc = self
323            .module_info
324            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/swaydoc.css"));
325        let ayu = self
326            .module_info
327            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/ayu.css"));
328        let sway_hjs = self
329            .module_info
330            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/highlight.js"));
331        let ayu_hjs = self
332            .module_info
333            .to_html_shorthand_path_string(&format!("{ASSETS_DIR_NAME}/ayu.min.css"));
334        let mut rendered_module_anchors = self.module_info.get_anchors()?;
335        rendered_module_anchors.pop();
336
337        Ok(box_html! {
338            head {
339                meta(charset="utf-8");
340                meta(name="viewport", content="width=device-width, initial-scale=1.0");
341                meta(name="generator", content="swaydoc");
342                meta(
343                    name="description",
344                    content=format!(
345                        "API documentation for the Sway `{}` module in `{}`.",
346                        self.module_info.location(), self.module_info.project_name(),
347                    )
348                );
349                meta(name="keywords", content=format!("sway, swaylang, sway-lang, {}", self.module_info.location()));
350                link(rel="icon", href=favicon);
351                title: format!("{} in {} - Sway", self.module_info.location(), self.module_info.project_name());
352                link(rel="stylesheet", type="text/css", href=normalize);
353                link(rel="stylesheet", type="text/css", href=swaydoc, id="mainThemeStyle");
354                link(rel="stylesheet", type="text/css", href=ayu);
355                link(rel="stylesheet", href=ayu_hjs);
356            }
357            body(class="swaydoc mod") {
358                : sidebar;
359                main {
360                    div(class="width-limiter") {
361                        : generate_searchbar(&self.module_info);
362                        section(id="main-content", class="content") {
363                            div(class="main-heading") {
364                                h1(class="fqn") {
365                                    span(class="in-band") {
366                                        : title_prefix;
367                                        @ for anchor in rendered_module_anchors {
368                                            : Raw(anchor);
369                                        }
370                                        a(class=BlockTitle::Modules.class_title_str(), href=IDENTITY) {
371                                            : self.module_info.location();
372                                        }
373                                    }
374                                }
375                            }
376                            @ if self.module_info.attributes.is_some() {
377                                details(class="swaydoc-toggle top-doc", open) {
378                                    summary(class="hideme") {
379                                        span { : "Expand description" }
380                                    }
381                                    div(class="docblock") {
382                                        : Raw(self.module_info.attributes.unwrap())
383                                    }
384                                }
385                            }
386                            : doc_links;
387                        }
388                        section(id="search", class="search-results");
389                    }
390                }
391                script(src=sway_hjs);
392                script {
393                    : "hljs.highlightAll();";
394                }
395            }
396        })
397    }
398}