1use 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#[derive(Clone, Debug, PartialEq)]
16pub struct LibraryInfo {
17 pub name: String,
18 pub description: String,
19}
20
21#[derive(Clone)]
23pub(crate) struct WorkspaceIndex {
24 workspace_info: ModuleInfo,
26 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 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 let assets_path = ASSETS_DIR_NAME.to_string();
59
60 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#[derive(Clone)]
203pub(crate) struct AllDocIndex {
204 project_name: ModuleInfo,
206 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
271pub(crate) struct ModuleIndex {
273 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}