Skip to main content

sphinx_ultra/
html_builder.rs

1use anyhow::{Context, Result};
2use log::{debug, info, warn};
3use serde::{Deserialize, Serialize};
4use serde_json::{Map, Value as JsonValue};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use tokio::fs;
8
9use crate::config::BuildConfig;
10use crate::document::Document;
11use crate::inventory::{InvObject, InventoryFile};
12use crate::template::TemplateEngine;
13use crate::utils;
14
15/// The filename for the inventory of objects (matches Sphinx)
16pub const INVENTORY_FILENAME: &str = "objects.inv";
17
18/// HTML Builder that mirrors Sphinx's StandaloneHTMLBuilder
19#[derive(Debug)]
20pub struct HTMLBuilder {
21    pub name: String,
22    pub format: String,
23    pub epilog: String,
24    pub out_suffix: String,
25    pub link_suffix: String,
26    pub searchindex_filename: String,
27    pub allow_parallel: bool,
28    pub copysource: bool,
29    pub use_index: bool,
30    pub embedded: bool,
31    pub search: bool,
32    pub download_support: bool,
33    pub supported_image_types: Vec<String>,
34    pub supported_remote_images: bool,
35    pub supported_data_uri_images: bool,
36
37    // Directories
38    pub outdir: PathBuf,
39    pub srcdir: PathBuf,
40    pub confdir: PathBuf,
41    pub static_dir: PathBuf,
42    pub sources_dir: PathBuf,
43    pub downloads_dir: PathBuf,
44    pub images_dir: PathBuf,
45
46    // Internal state
47    pub config: BuildConfig,
48    pub current_docname: String,
49    pub secnumbers: HashMap<String, Vec<u32>>,
50    pub imgpath: String,
51    pub dlpath: String,
52
53    // Asset management
54    pub css_files: Vec<CSSFile>,
55    pub js_files: Vec<JSFile>,
56
57    // Template engine
58    pub template_engine: TemplateEngine,
59
60    /// Global template context
61    pub global_context: Map<String, JsonValue>,
62
63    // Relations between documents
64    pub relations: HashMap<String, DocumentRelation>,
65
66    // Domain indices
67    pub domain_indices: Vec<DomainIndex>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct CSSFile {
72    pub filename: String,
73    pub priority: i32,
74    pub media: Option<String>,
75    pub id: Option<String>,
76    pub rel: String,
77    pub type_: String,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct JSFile {
82    pub filename: String,
83    pub priority: i32,
84    pub loading_method: String,
85    pub async_: bool,
86    pub defer: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct DocumentRelation {
91    pub parent: Option<String>,
92    pub prev: Option<String>,
93    pub next: Option<String>,
94}
95
96#[derive(Debug, Clone)]
97pub struct DomainIndex {
98    pub name: String,
99    pub localname: String,
100    pub shortname: Option<String>,
101    pub content: Vec<IndexEntry>,
102    pub collapse: bool,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct IndexEntry {
107    pub name: String,
108    pub subentries: Vec<IndexEntry>,
109    pub uri: String,
110    pub display_name: String,
111}
112
113impl HTMLBuilder {
114    pub fn new(config: BuildConfig, srcdir: PathBuf, outdir: PathBuf) -> Result<Self> {
115        let confdir = srcdir.clone();
116        let static_dir = outdir.join("_static");
117        let sources_dir = outdir.join("_sources");
118        let downloads_dir = outdir.join("_downloads");
119        let images_dir = outdir.join("_images");
120
121        let template_engine = TemplateEngine::new(&config)?;
122
123        Ok(Self {
124            name: "html".to_string(),
125            format: "html".to_string(),
126            epilog: "The HTML pages are in %(outdir)s.".to_string(),
127            out_suffix: ".html".to_string(),
128            link_suffix: ".html".to_string(),
129            searchindex_filename: "searchindex.js".to_string(),
130            allow_parallel: true,
131            copysource: true,
132            use_index: false,
133            embedded: false,
134            search: true,
135            download_support: true,
136            supported_image_types: vec![
137                "image/svg+xml".to_string(),
138                "image/png".to_string(),
139                "image/gif".to_string(),
140                "image/jpeg".to_string(),
141            ],
142            supported_remote_images: true,
143            supported_data_uri_images: true,
144
145            outdir,
146            srcdir,
147            confdir,
148            static_dir,
149            sources_dir,
150            downloads_dir,
151            images_dir,
152
153            config,
154            current_docname: String::new(),
155            secnumbers: HashMap::new(),
156            imgpath: String::new(),
157            dlpath: String::new(),
158
159            css_files: Vec::new(),
160            js_files: Vec::new(),
161
162            template_engine,
163
164            global_context: Map::new(),
165            relations: HashMap::new(),
166            domain_indices: Vec::new(),
167        })
168    }
169
170    /// Initialize the builder (mirrors Sphinx's init method)
171    pub async fn init(&mut self) -> Result<()> {
172        info!("Initializing HTML builder");
173
174        // Create necessary directories
175        fs::create_dir_all(&self.outdir).await?;
176        fs::create_dir_all(&self.static_dir).await?;
177        fs::create_dir_all(&self.sources_dir).await?;
178        fs::create_dir_all(&self.downloads_dir).await?;
179        fs::create_dir_all(&self.images_dir).await?;
180
181        // Initialize CSS and JS files
182        self.init_css_files()?;
183        self.init_js_files()?;
184
185        // Set up global template context
186        self.init_global_context()?;
187
188        // Configure use_index based on config
189        self.use_index = self.config.html_use_index.unwrap_or(true);
190
191        Ok(())
192    }
193
194    /// Initialize CSS files (mirrors Sphinx's init_css_files)
195    fn init_css_files(&mut self) -> Result<()> {
196        self.css_files.clear();
197
198        // Add pygments CSS
199        self.add_css_file("pygments.css", 200, None, None)?;
200
201        // Add theme stylesheets
202        let styles = self.config.html_style.clone();
203        for style in &styles {
204            self.add_css_file(style, 200, None, None)?;
205        }
206
207        // Add user CSS files
208        let css_files = self.config.html_css_files.clone();
209        for css_file in &css_files {
210            self.add_css_file(css_file, 800, None, None)?;
211        }
212
213        Ok(())
214    }
215
216    /// Initialize JS files (mirrors Sphinx's init_js_files)
217    fn init_js_files(&mut self) -> Result<()> {
218        self.js_files.clear();
219
220        // Add core JS files
221        self.add_js_file("documentation_options.js", 200, false, false)?;
222        self.add_js_file("doctools.js", 200, false, false)?;
223        self.add_js_file("sphinx_highlight.js", 200, false, false)?;
224
225        // Add user JS files
226        let js_files = self.config.html_js_files.clone();
227        for js_file in &js_files {
228            self.add_js_file(js_file, 800, false, false)?;
229        }
230
231        // Add translations if available
232        if self.has_translations() {
233            self.add_js_file("translations.js", 500, false, false)?;
234        }
235
236        Ok(())
237    }
238
239    /// Add a CSS file
240    fn add_css_file(
241        &mut self,
242        filename: &str,
243        priority: i32,
244        media: Option<&str>,
245        id: Option<&str>,
246    ) -> Result<()> {
247        let filename = if !filename.contains("://") {
248            format!("_static/{}", filename)
249        } else {
250            filename.to_string()
251        };
252
253        let css_file = CSSFile {
254            filename,
255            priority,
256            media: media.map(|s| s.to_string()),
257            id: id.map(|s| s.to_string()),
258            rel: "stylesheet".to_string(),
259            type_: "text/css".to_string(),
260        };
261
262        if !self.css_files.contains(&css_file) {
263            self.css_files.push(css_file);
264        }
265
266        Ok(())
267    }
268
269    /// Add a JS file
270    fn add_js_file(
271        &mut self,
272        filename: &str,
273        priority: i32,
274        async_: bool,
275        defer: bool,
276    ) -> Result<()> {
277        let filename = if !filename.is_empty() && !filename.contains("://") {
278            format!("_static/{}", filename)
279        } else {
280            filename.to_string()
281        };
282
283        let js_file = JSFile {
284            filename,
285            priority,
286            loading_method: "normal".to_string(),
287            async_,
288            defer,
289        };
290
291        if !self.js_files.contains(&js_file) {
292            self.js_files.push(js_file);
293        }
294
295        Ok(())
296    }
297
298    /// Check if translations are available
299    fn has_translations(&self) -> bool {
300        // Check for translation files
301        let locale_dir = self.confdir.join("locale");
302        let lang = self.config.language.as_deref().unwrap_or("en");
303        let js_file = locale_dir.join(lang).join("LC_MESSAGES").join("sphinx.js");
304        js_file.exists()
305    }
306
307    /// Initialize global template context (mirrors Sphinx's prepare_writing)
308    fn init_global_context(&mut self) -> Result<()> {
309        use serde_json::json;
310
311        let _now = std::time::SystemTime::now()
312            .duration_since(std::time::UNIX_EPOCH)
313            .unwrap()
314            .as_secs();
315
316        let last_updated = if let Some(fmt) = &self.config.html_last_updated_fmt {
317            Some(utils::format_date(fmt, &self.config.language))
318        } else {
319            None
320        };
321
322        self.global_context = json!({
323            "embedded": self.embedded,
324            "project": self.config.project,
325            "release": self.config.release.as_deref().unwrap_or(""),
326            "version": self.config.version.as_deref().unwrap_or(""),
327            "last_updated": last_updated,
328            "copyright": self.config.copyright.as_deref().unwrap_or(""),
329            "master_doc": self.config.root_doc.as_deref().unwrap_or("index"),
330            "root_doc": self.config.root_doc.as_deref().unwrap_or("index"),
331            "use_opensearch": self.config.html_use_opensearch.unwrap_or(false),
332            "docstitle": self.config.html_title.as_deref().unwrap_or(&self.config.project),
333            "shorttitle": self.config.html_short_title.as_deref().unwrap_or(&self.config.project),
334            "show_copyright": self.config.html_show_copyright.unwrap_or(true),
335            "show_sphinx": self.config.html_show_sphinx.unwrap_or(true),
336            "has_source": self.config.html_copy_source.unwrap_or(true),
337            "show_source": self.config.html_show_sourcelink.unwrap_or(true),
338            "sourcelink_suffix": self.config.html_sourcelink_suffix.as_deref().unwrap_or(".txt"),
339            "file_suffix": &self.out_suffix,
340            "link_suffix": &self.link_suffix,
341            "script_files": &self.js_files,
342            "language": self.config.language.as_deref().unwrap_or("en"),
343            "css_files": &self.css_files,
344            "sphinx_version": env!("CARGO_PKG_VERSION"),
345            "styles": self.config.html_style.clone(),
346            "builder": &self.name,
347            "parents": Vec::<String>::new(),
348            "logo_url": self.config.html_logo.as_deref().unwrap_or(""),
349            "favicon_url": self.config.html_favicon.as_deref().unwrap_or(""),
350            "html5_doctype": true,
351        })
352        .as_object()
353        .unwrap()
354        .clone();
355
356        Ok(())
357    }
358
359    /// Write a single document (mirrors Sphinx's write_doc)
360    pub async fn write_doc(&mut self, docname: &str, doctree: &Document) -> Result<()> {
361        info!("Writing document: {}", docname);
362
363        self.current_docname = docname.to_string();
364        self.imgpath = self.get_relative_uri(docname, "_images");
365        self.dlpath = self.get_relative_uri(docname, "_downloads");
366
367        // Render the document to HTML
368        let body = format!(
369            "<div class=\"document\">\n{}\n</div>",
370            html_escape::encode_text(&doctree.content.to_string())
371        );
372        let metatags = format!(
373            "<meta name=\"source\" content=\"{}\" />",
374            html_escape::encode_double_quoted_attribute(&doctree.source_path.to_string_lossy())
375        );
376
377        // Get document context
378        let ctx = self.get_doc_context(docname, &body, &metatags).await?;
379
380        // Handle the page
381        self.handle_page(docname, ctx, "page.html").await?;
382
383        Ok(())
384    }
385
386    /// Get document context for template (mirrors Sphinx's get_doc_context)
387    async fn get_doc_context(
388        &self,
389        docname: &str,
390        body: &str,
391        metatags: &str,
392    ) -> Result<serde_json::Map<String, serde_json::Value>> {
393        use serde_json::json;
394
395        let mut ctx = self.global_context.clone();
396
397        // Find relations
398        let relation = self.relations.get(docname);
399        let (prev, next) = if let Some(rel) = relation {
400            (rel.prev.clone(), rel.next.clone())
401        } else {
402            (None, None)
403        };
404
405        // Build parents chain
406        let mut parents = Vec::new();
407        let mut current = relation.and_then(|r| r.parent.clone());
408        while let Some(parent_name) = current {
409            if let Some(parent_rel) = self.relations.get(&parent_name) {
410                parents.push(json!({
411                    "link": self.get_relative_uri(docname, &parent_name),
412                    "title": parent_name, // TODO: Get actual title
413                }));
414                current = parent_rel.parent.clone();
415            } else {
416                break;
417            }
418        }
419        parents.reverse();
420
421        // Title and metadata
422        let title = docname; // TODO: Extract actual title from document
423        let source_suffix = ".rst"; // TODO: Detect actual suffix
424        let sourcename = if self.config.html_copy_source.unwrap_or(true) {
425            format!(
426                "{}{}",
427                docname,
428                self.config
429                    .html_sourcelink_suffix
430                    .as_deref()
431                    .unwrap_or(".txt")
432            )
433        } else {
434            String::new()
435        };
436
437        // Local TOC
438        let toc = self.generate_local_toc(docname).await?;
439
440        ctx.insert("parents".to_string(), json!(parents));
441        if let Some(p) = prev {
442            ctx.insert(
443                "prev".to_string(),
444                json!({
445                    "link": self.get_relative_uri(docname, &p),
446                    "title": p, // TODO: Get actual title
447                }),
448            );
449        }
450        if let Some(n) = next {
451            ctx.insert(
452                "next".to_string(),
453                json!({
454                    "link": self.get_relative_uri(docname, &n),
455                    "title": n, // TODO: Get actual title
456                }),
457            );
458        }
459        ctx.insert("title".to_string(), json!(title));
460        ctx.insert("body".to_string(), json!(body));
461        ctx.insert("metatags".to_string(), json!(metatags));
462        ctx.insert("sourcename".to_string(), json!(sourcename));
463        ctx.insert("toc".to_string(), json!(toc));
464        ctx.insert("display_toc".to_string(), json!(true));
465        ctx.insert("page_source_suffix".to_string(), json!(source_suffix));
466
467        Ok(ctx)
468    }
469
470    /// Generate local table of contents
471    async fn generate_local_toc(&self, _docname: &str) -> Result<String> {
472        // TODO: Implement actual TOC generation
473        Ok("<div class=\"toc\"></div>".to_string())
474    }
475
476    /// Handle a page (render and write) - mirrors Sphinx's handle_page
477    async fn handle_page(
478        &self,
479        pagename: &str,
480        context: serde_json::Map<String, serde_json::Value>,
481        template_name: &str,
482    ) -> Result<()> {
483        debug!(
484            "Handling page: {} with template: {}",
485            pagename, template_name
486        );
487
488        // Render the template
489        let output = self.template_engine.render(template_name, &context)?;
490
491        // Write to file
492        let output_path = self.get_output_path(pagename);
493        utils::ensure_dir(output_path.parent().unwrap()).await?;
494
495        fs::write(&output_path, output)
496            .await
497            .with_context(|| format!("Failed to write page: {}", output_path.display()))?;
498
499        // Copy source file if needed
500        if self.copysource
501            && context
502                .get("sourcename")
503                .and_then(|s| s.as_str())
504                .map(|s| !s.is_empty())
505                .unwrap_or(false)
506        {
507            let sourcename = context["sourcename"].as_str().unwrap();
508            let source_path = self.sources_dir.join(sourcename);
509            utils::ensure_dir(source_path.parent().unwrap()).await?;
510
511            let doc_path = self.srcdir.join(format!("{}.rst", pagename)); // TODO: Detect actual extension
512            if doc_path.exists() {
513                fs::copy(&doc_path, &source_path).await?;
514            }
515        }
516
517        Ok(())
518    }
519
520    /// Get output path for a document
521    fn get_output_path(&self, docname: &str) -> PathBuf {
522        self.outdir.join(format!("{}{}", docname, self.out_suffix))
523    }
524
525    /// Get relative URI between two documents
526    fn get_relative_uri(&self, from: &str, to: &str) -> String {
527        utils::relative_uri(from, to, &self.link_suffix)
528    }
529
530    /// Get target URI for a document: `quote(docname) + self.link_suffix`,
531    /// as `StandaloneHTMLBuilder.get_target_uri` percent-encodes it.
532    pub fn get_target_uri(&self, docname: &str) -> String {
533        format!("{}{}", utils::quote_url_path(docname), self.link_suffix)
534    }
535
536    /// Generate indices (mirrors Sphinx's gen_indices)
537    pub async fn gen_indices(&mut self) -> Result<()> {
538        info!("Generating indices");
539
540        // Generate general index if enabled
541        if self.use_index {
542            self.write_genindex().await?;
543        }
544
545        // Generate domain-specific indices
546        self.write_domain_indices().await?;
547
548        Ok(())
549    }
550
551    /// Write general index
552    async fn write_genindex(&self) -> Result<()> {
553        info!("Writing general index");
554
555        // TODO: Implement actual index generation
556        let genindex_context = serde_json::json!({
557            "genindexentries": [],
558            "genindexcounts": [],
559            "split_index": false,
560        });
561
562        self.handle_page(
563            "genindex",
564            genindex_context.as_object().unwrap().clone(),
565            "genindex.html",
566        )
567        .await?;
568
569        Ok(())
570    }
571
572    /// Write domain indices
573    async fn write_domain_indices(&self) -> Result<()> {
574        for domain_index in &self.domain_indices {
575            info!("Writing domain index: {}", domain_index.name);
576
577            let index_context = serde_json::json!({
578                "indextitle": domain_index.localname,
579                "content": domain_index.content,
580                "collapse_index": domain_index.collapse,
581            });
582
583            self.handle_page(
584                &domain_index.name,
585                index_context.as_object().unwrap().clone(),
586                "domainindex.html",
587            )
588            .await?;
589        }
590
591        Ok(())
592    }
593
594    /// Copy static files (mirrors Sphinx's copy_static_files)
595    pub async fn copy_static_files(&self) -> Result<()> {
596        info!("Copying static files");
597
598        // Copy theme static files
599        self.copy_theme_static_files().await?;
600
601        // Copy user static files
602        for static_path in &self.config.html_static_path {
603            let source_dir = self.confdir.join(static_path);
604            if source_dir.exists() {
605                utils::copy_dir_all(&source_dir, &self.static_dir).await?;
606            }
607        }
608
609        // Create pygments CSS
610        self.create_pygments_style_file().await?;
611
612        // Copy translations if available
613        if self.has_translations() {
614            self.copy_translation_js().await?;
615        }
616
617        Ok(())
618    }
619
620    /// Copy theme static files
621    async fn copy_theme_static_files(&self) -> Result<()> {
622        // TODO: Implement theme system
623        Ok(())
624    }
625
626    /// Create pygments style file
627    async fn create_pygments_style_file(&self) -> Result<()> {
628        let css_content = "/* Basic syntax highlighting */\n.highlight { background: #f8f8f8; }\n";
629        let css_path = self.static_dir.join("pygments.css");
630        fs::write(css_path, css_content).await?;
631        Ok(())
632    }
633
634    /// Copy translation JS file
635    async fn copy_translation_js(&self) -> Result<()> {
636        let locale_dir = self.confdir.join("locale");
637        let lang = self.config.language.as_deref().unwrap_or("en");
638        let js_file = locale_dir.join(lang).join("LC_MESSAGES").join("sphinx.js");
639
640        if js_file.exists() {
641            let dest = self.static_dir.join("translations.js");
642            fs::copy(js_file, dest).await?;
643        }
644
645        Ok(())
646    }
647
648    /// Copy image files
649    pub async fn copy_image_files(&self, images: &HashMap<String, String>) -> Result<()> {
650        info!("Copying {} images", images.len());
651
652        for (src, dest) in images {
653            let src_path = self.srcdir.join(src);
654            let dest_path = self.images_dir.join(dest);
655
656            utils::ensure_dir(dest_path.parent().unwrap()).await?;
657
658            if src_path.exists() {
659                fs::copy(&src_path, &dest_path).await.with_context(|| {
660                    format!(
661                        "Failed to copy image {} to {}",
662                        src_path.display(),
663                        dest_path.display()
664                    )
665                })?;
666            } else {
667                warn!("Image file not found: {}", src_path.display());
668            }
669        }
670
671        Ok(())
672    }
673
674    /// Copy download files
675    pub async fn copy_download_files(&self, downloads: &HashMap<String, String>) -> Result<()> {
676        info!("Copying {} download files", downloads.len());
677
678        for (src, dest) in downloads {
679            let src_path = self.srcdir.join(src);
680            let dest_path = self.downloads_dir.join(dest);
681
682            utils::ensure_dir(dest_path.parent().unwrap()).await?;
683
684            if src_path.exists() {
685                fs::copy(&src_path, &dest_path).await.with_context(|| {
686                    format!(
687                        "Failed to copy download {} to {}",
688                        src_path.display(),
689                        dest_path.display()
690                    )
691                })?;
692            } else {
693                warn!("Download file not found: {}", src_path.display());
694            }
695        }
696
697        Ok(())
698    }
699
700    /// Dump search index
701    pub async fn dump_search_index(&self, search_index: &crate::search::SearchIndex) -> Result<()> {
702        if !self.search {
703            return Ok(());
704        }
705
706        info!("Dumping search index");
707
708        let search_index_path = self.outdir.join(&self.searchindex_filename);
709        let contents = crate::search::dumps_sphinx_index(&search_index.to_sphinx_value())?;
710        fs::write(search_index_path, contents).await?;
711
712        Ok(())
713    }
714
715    /// Write build info file
716    pub async fn write_build_info(&self) -> Result<()> {
717        let build_info_path = self.outdir.join(".buildinfo");
718        fs::write(
719            build_info_path,
720            crate::builder::sphinx_build_info_contents(&self.config, &self.config.tags)?,
721        )
722        .await?;
723
724        Ok(())
725    }
726
727    /// Write the direct builder's inventory from the same search-index object
728    /// records used by its Sphinx-shaped JSON output. The full builder has
729    /// richer environment registries, but both paths use `InventoryFile` for
730    /// the wire format, the std domain's preseeded labels, and the one
731    /// priority table the search index applies to the same objects.
732    pub async fn dump_object_inventory(
733        &self,
734        search_index: &crate::search::SearchIndex,
735    ) -> Result<()> {
736        let mut std_objects = crate::env::std_domain::preseeded_label_objects();
737        for (index, docname) in search_index.docnames.iter().enumerate() {
738            std_objects.push(InvObject {
739                name: docname.clone(),
740                objtype: "doc".to_string(),
741                priority: -1,
742                docname: docname.clone(),
743                anchor: String::new(),
744                dispname: search_index.titles.get(index).cloned().unwrap_or_default(),
745            });
746        }
747
748        let mut py_objects = Vec::new();
749        for object in search_index.objects.values() {
750            let (domain, objtype) = crate::search::split_object_type(&object.obj_type);
751            let Some(target_docname) = search_index.docnames.get(object.docname_idx) else {
752                continue;
753            };
754            let destination = match domain {
755                "std" => &mut std_objects,
756                "py" => &mut py_objects,
757                _ => continue,
758            };
759            destination.push(InvObject {
760                name: object.name.clone(),
761                objtype: objtype.to_string(),
762                priority: crate::search::direct_object_priority(domain, objtype),
763                docname: target_docname.clone(),
764                anchor: object.anchor.clone().unwrap_or_default(),
765                dispname: object
766                    .description
767                    .clone()
768                    .unwrap_or_else(|| object.name.clone()),
769            });
770        }
771
772        let domains = [("std", std_objects), ("py", py_objects)];
773        InventoryFile::dump(
774            self.outdir.join(INVENTORY_FILENAME),
775            &self.config.project,
776            self.config.version.as_deref().unwrap_or(""),
777            &domains,
778            |docname| self.get_target_uri(docname),
779        )
780        .await
781    }
782
783    /// Finish the build process
784    ///
785    pub async fn finish(&mut self, search_index: &crate::search::SearchIndex) -> Result<()> {
786        info!("Finishing HTML build");
787
788        // Generate indices
789        self.gen_indices().await?;
790
791        // Copy static files
792        self.copy_static_files().await?;
793
794        // Dump search index
795        self.dump_search_index(search_index).await?;
796
797        // Dump the object inventory through the same writer as the full
798        // SphinxBuilder path.
799        self.dump_object_inventory(search_index).await?;
800
801        // Write build info
802        self.write_build_info().await?;
803
804        Ok(())
805    }
806}
807
808impl PartialEq for CSSFile {
809    fn eq(&self, other: &Self) -> bool {
810        self.filename == other.filename
811    }
812}
813
814impl PartialEq for JSFile {
815    fn eq(&self, other: &Self) -> bool {
816        self.filename == other.filename
817    }
818}