commit f943d06302bad9359d690b6bc984ac3f777d6345
parent 749dd8a7032b747281c3abb7e6cb07e1262abe08
Author: egor-achkasov <eaachkasov@gmail.com>
Date: Mon, 23 Feb 2026 16:48:21 +0000
Add top level index.html
Diffstat:
2 files changed, 50 insertions(+), 6 deletions(-)
diff --git a/src/export.rs b/src/export.rs
@@ -2,6 +2,8 @@ use crate::{parse_args::Config, post::Post};
use anyhow::{Result, Context};
+const TEMPLATE: &'static str = include_str!("../template.html");
+
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
@@ -36,6 +38,38 @@ fn render_text_to_html(text: &str) -> String {
lines.join("<br>\n")
}
+/// Write a top-level index.html with one entry per thread (first post + link to thread folder)
+pub fn write_index_html(first_posts: &[Post], config: &Config) -> Result<()> {
+ if first_posts.is_empty() {
+ return Ok(());
+ }
+
+ let posts_html: String = first_posts
+ .iter()
+ .map(|p| {
+ let mut post_html = render_post(p, config.files, config.thumb);
+ // render_post references thumbnails and images in the same directory,
+ // so replace them with links to the thread folder
+ config.files.then(|| post_html = post_html.replace(
+ "<a href=\"files/",
+ &format!("<a href=\"{}/files/", p.id),
+ ));
+ config.thumb.then(|| post_html = post_html.replace(
+ "<img src=\"thumb/",
+ &format!("<img src=\"{}/thumb/", p.id),
+ ));
+ format!("<div><a href=\"{}/index.html\">В тред →</a></div>{}\n", p.id, post_html)
+ })
+ .collect::<Vec<String>>()
+ .join("\n");
+
+ let index_html = TEMPLATE.replace("{{posts}}", &posts_html);
+ std::fs::write("index.html", index_html)
+ .context("failed to write index.html")?;
+
+ Ok(())
+}
+
/// Export the thread to a simple static HTML
///
/// Creates a directory as follows:
@@ -77,7 +111,6 @@ pub fn export2html(posts: &[Post], config: &Config) -> Result<()> {
)?;
}
- const TEMPLATE: &'static str = include_str!("../template.html");
let index_html = TEMPLATE.replace("{{posts}}", &posts_html);
std::fs::write(format!("{}/index.html", dir), index_html)?;
diff --git a/src/main.rs b/src/main.rs
@@ -7,8 +7,9 @@ use parse_args::{Config, parse_args};
use post::Post;
use anyhow::{Context, Ok, Result};
+use std::result::Result::Ok as StdOk;
-fn scrape_thread(url: &str, config: &Config) -> Result<()> {
+fn scrape_thread(url: &str, config: &Config) -> Result<Post> {
use std::io::Write;
let t_total = std::time::Instant::now();
@@ -28,11 +29,13 @@ fn scrape_thread(url: &str, config: &Config) -> Result<()> {
.context("failed to parse thread HTML")?;
println!(" Done ({} ms)", t.elapsed().as_millis());
+ let first_post = posts.first().context("thread has no posts")?.clone();
+
export::export2html(&posts, &config)
.context("failed to export thread")?;
println!("Done processing {} ({} ms)", url, t_total.elapsed().as_millis());
- Ok(())
+ Ok(first_post)
}
@@ -43,11 +46,19 @@ fn main() -> Result<()> {
std::process::exit(1);
});
+ let mut first_posts: Vec<Post> = Vec::new();
+ let mut i = 1;
for url in &config.urls {
- println!("Processing {}:", url);
- scrape_thread(url, &config)
- .unwrap_or_else(|e| eprintln!("Error processing {}: {:#}", url, e));
+ println!("Processing {} ({} / {}):", url, i, config.urls.len());
+ i += 1;
+ match scrape_thread(url, &config) {
+ StdOk(first_post) => first_posts.push(first_post),
+ Err(e) => eprintln!("Error processing {}: {:#}", url, e),
+ }
}
+ export::write_index_html(&first_posts, &config)
+ .context("failed to write main index.html")?;
+
Ok(())
}