Initial commit.
Signed-off-by: Andreas Widen <aw@luflow.net>
This commit is contained in:
commit
a4a5c7daa5
1389 changed files with 241399 additions and 0 deletions
500
src/site/blog.rs
Normal file
500
src/site/blog.rs
Normal file
|
|
@ -0,0 +1,500 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use chrono::{Datelike, Local};
|
||||
use sailfish::Template;
|
||||
use std::cmp;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::site::blog_post::BlogPost;
|
||||
use crate::site::blog_post::parse_markdown_file;
|
||||
use crate::site::helper::Helper;
|
||||
|
||||
pub struct BlogShared {
|
||||
state: Mutex<BlogState>,
|
||||
}
|
||||
|
||||
impl BlogShared {
|
||||
pub fn new(base_dir: String, num_previews_per_page: usize) -> Self {
|
||||
Self {
|
||||
state: Mutex::new(BlogState::new(base_dir, num_previews_per_page)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared(self) -> Arc<Self> {
|
||||
Arc::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "blog_overview.stpl")]
|
||||
pub struct BlogState {
|
||||
base_dir: String,
|
||||
num_previews_per_page: usize,
|
||||
blog_posts: Vec<BlogPost>,
|
||||
topic_blog_indices: HashMap<String, Vec<usize>>,
|
||||
topics: Vec<String>,
|
||||
topics_sanitized: Vec<String>,
|
||||
year_blog_indices: HashMap<String, Vec<usize>>,
|
||||
years: Vec<String>,
|
||||
overview_page_url: String,
|
||||
overview_current_page: usize,
|
||||
overview_num_pages: usize,
|
||||
overview_offset: usize,
|
||||
overview_num_posts: usize,
|
||||
overview_keywords: String,
|
||||
overview_title: String,
|
||||
overview_topic: String,
|
||||
overview_topic_sanitized: String,
|
||||
overview_year: String,
|
||||
overview_type: i32,
|
||||
}
|
||||
|
||||
impl BlogState {
|
||||
pub fn new(base_dir: String, num_previews_per_page: usize) -> Self {
|
||||
Self {
|
||||
base_dir: base_dir,
|
||||
num_previews_per_page: num_previews_per_page,
|
||||
blog_posts: Vec::new(),
|
||||
topic_blog_indices: HashMap::new(),
|
||||
topics: Vec::new(),
|
||||
topics_sanitized: Vec::new(),
|
||||
year_blog_indices: HashMap::new(),
|
||||
years: Vec::new(),
|
||||
overview_page_url: String::new(),
|
||||
overview_current_page: 0,
|
||||
overview_num_pages: 0,
|
||||
overview_offset: 0,
|
||||
overview_num_posts: 0,
|
||||
overview_keywords: String::new(),
|
||||
overview_title: String::new(),
|
||||
overview_topic: String::new(),
|
||||
overview_topic_sanitized: String::new(),
|
||||
overview_year: String::new(),
|
||||
overview_type: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_blog(shared: Arc<BlogShared>, base_dir: String) {
|
||||
// create output dirs for topic, year and feeds:
|
||||
create_output_dirs(shared.clone());
|
||||
|
||||
// parse all the markdown files in 'blog-posts' folder:
|
||||
parse_markdown_files(shared.clone(), base_dir).await;
|
||||
|
||||
// generate all individual blog posts:
|
||||
generate_blog_posts(shared.clone()).await;
|
||||
|
||||
// generate blog overview:
|
||||
generate_overview_posts(shared.clone()).await;
|
||||
|
||||
// generate blog overview by topic:
|
||||
generate_overview_topic(shared.clone()).await;
|
||||
|
||||
// generate blog overview by year:
|
||||
generate_overview_year(shared.clone()).await;
|
||||
|
||||
// generate blog atom feed:
|
||||
generate_atom_feed(shared.clone()).await;
|
||||
}
|
||||
|
||||
pub fn get_latest_blog_posts(shared: Arc<BlogShared>, num_posts: usize) -> Vec<BlogPost> {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
let mut blog_posts = Vec::new();
|
||||
|
||||
for i in 0..num_posts {
|
||||
if let Some(value) = lock.blog_posts.get(i) {
|
||||
blog_posts.push(value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
return blog_posts;
|
||||
}
|
||||
|
||||
fn create_output_dirs(shared: Arc<BlogShared>) {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.base_dir).join("topic"));
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.base_dir).join("year"));
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join("feeds"));
|
||||
}
|
||||
|
||||
async fn parse_markdown_files(shared: Arc<BlogShared>, base_dir: String) {
|
||||
let mut reader = tokio::fs::read_dir("blog-posts").await.unwrap();
|
||||
let mut tasks = vec![];
|
||||
loop {
|
||||
if let Some(f) = reader.next_entry().await.unwrap() {
|
||||
tasks.push(tokio::spawn(parse_markdown_file(
|
||||
f.path(),
|
||||
base_dir.clone(),
|
||||
)));
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// await all created blog_posts:
|
||||
let mut blog_posts = Vec::with_capacity(tasks.len());
|
||||
for task in tasks {
|
||||
blog_posts.push(task.await.unwrap());
|
||||
}
|
||||
|
||||
// sort so that latest is first:
|
||||
blog_posts.sort_by(|a, b| b.get_published_date().cmp(a.get_published_date()));
|
||||
|
||||
let mut topic_blog_indices: HashMap<String, Vec<usize>> = HashMap::new();
|
||||
let mut year_blog_indices: HashMap<String, Vec<usize>> = HashMap::new();
|
||||
for (i, post) in blog_posts.iter().enumerate() {
|
||||
// add blog_id for each blog topic for later lookup:
|
||||
for topic in post.get_topics() {
|
||||
if let Some(indices) = topic_blog_indices.get_mut(topic) {
|
||||
indices.push(i);
|
||||
} else {
|
||||
topic_blog_indices.insert(String::from(topic), vec![i]);
|
||||
}
|
||||
}
|
||||
|
||||
// add blog_id for each blog year for later lookup:
|
||||
let year_str = post.get_year();
|
||||
if let Some(indices) = year_blog_indices.get_mut(&year_str) {
|
||||
indices.push(i);
|
||||
} else {
|
||||
year_blog_indices.insert(String::from(year_str), vec![i]);
|
||||
}
|
||||
}
|
||||
|
||||
// move into state:
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
lock.blog_posts = blog_posts;
|
||||
|
||||
for (key, _value) in &topic_blog_indices {
|
||||
lock.topics.push(String::from(key));
|
||||
lock.topics_sanitized
|
||||
.push(String::from(Helper::sanitize_string(key)));
|
||||
}
|
||||
// sort by topic name:
|
||||
lock.topics
|
||||
.sort_by(|a, b| a.to_lowercase().cmp(&b.to_lowercase()));
|
||||
lock.topics_sanitized.sort_by(|a, b| a.cmp(b));
|
||||
|
||||
for (key, _value) in &year_blog_indices {
|
||||
lock.years.push(String::from(key));
|
||||
}
|
||||
// sort by year:
|
||||
lock.years.sort_by(|a, b| a.cmp(b));
|
||||
|
||||
// move created hashmaps:
|
||||
lock.topic_blog_indices = topic_blog_indices;
|
||||
lock.year_blog_indices = year_blog_indices;
|
||||
}
|
||||
|
||||
async fn generate_blog_posts(shared: Arc<BlogShared>) {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
|
||||
for post in &lock.blog_posts {
|
||||
post.create_output_dir();
|
||||
post.generate();
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_overview_posts(shared: Arc<BlogShared>) {
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
|
||||
lock.overview_offset = 0;
|
||||
let num = lock.blog_posts.len() as f32 / lock.num_previews_per_page as f32;
|
||||
lock.overview_num_pages = num.ceil() as usize;
|
||||
lock.overview_num_posts = lock.num_previews_per_page;
|
||||
lock.overview_keywords = String::from("overview");
|
||||
lock.overview_title = String::from("Blog Overview");
|
||||
lock.overview_type = 0;
|
||||
|
||||
for i in 1..lock.overview_num_pages + 1 {
|
||||
lock.overview_current_page = i;
|
||||
lock.overview_page_url = format!("{}/page/{}", lock.base_dir, i);
|
||||
|
||||
if i == lock.overview_num_pages {
|
||||
lock.overview_num_posts = cmp::min(
|
||||
lock.blog_posts.len() - lock.num_previews_per_page * (i - 1),
|
||||
lock.num_previews_per_page,
|
||||
);
|
||||
}
|
||||
|
||||
if lock.overview_current_page == 1 {
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.base_dir)
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// create dir recursively:
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.overview_page_url)
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lock.overview_offset += lock.overview_num_posts;
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_overview_topic(shared: Arc<BlogShared>) {
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
|
||||
// TODO: How to solve this without a clone??
|
||||
for (key, indices) in &lock.topic_blog_indices.clone() {
|
||||
lock.overview_offset = 0;
|
||||
let num = indices.len() as f32 / lock.num_previews_per_page as f32;
|
||||
lock.overview_num_pages = num.ceil() as usize;
|
||||
lock.overview_num_posts = lock.num_previews_per_page;
|
||||
lock.overview_keywords = format!("topic, {}", key);
|
||||
lock.overview_topic = String::from(key);
|
||||
lock.overview_topic_sanitized = Helper::sanitize_string(key);
|
||||
lock.overview_type = 1;
|
||||
|
||||
for i in 1..lock.overview_num_pages + 1 {
|
||||
lock.overview_current_page = i;
|
||||
lock.overview_page_url = format!(
|
||||
"{}/topic/{}/page/{}",
|
||||
lock.base_dir,
|
||||
Helper::sanitize_string(key),
|
||||
i
|
||||
);
|
||||
lock.overview_title = format!("Blog posts by topic: {}", key);
|
||||
|
||||
if i == lock.overview_num_pages {
|
||||
lock.overview_num_posts = cmp::min(
|
||||
indices.len() - lock.num_previews_per_page * (i - 1),
|
||||
lock.num_previews_per_page,
|
||||
);
|
||||
}
|
||||
|
||||
if lock.overview_current_page == 1 {
|
||||
// create dir recursively:
|
||||
Helper::create_dir_all(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.base_dir)
|
||||
.join("topic")
|
||||
.join(Helper::sanitize_string(key)),
|
||||
);
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.base_dir)
|
||||
.join("topic")
|
||||
.join(Helper::sanitize_string(key))
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// create dir recursively:
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.overview_page_url)
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lock.overview_offset += lock.overview_num_posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_overview_year(shared: Arc<BlogShared>) {
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
|
||||
// TODO: How to solve this without a clone??
|
||||
for (key, indices) in &lock.year_blog_indices.clone() {
|
||||
lock.overview_offset = 0;
|
||||
let num = indices.len() as f32 / lock.num_previews_per_page as f32;
|
||||
lock.overview_num_pages = num.ceil() as usize;
|
||||
lock.overview_num_posts = lock.num_previews_per_page;
|
||||
lock.overview_keywords = format!("year, {}", key);
|
||||
lock.overview_year = String::from(key);
|
||||
lock.overview_type = 2;
|
||||
|
||||
for i in 1..lock.overview_num_pages + 1 {
|
||||
lock.overview_current_page = i;
|
||||
lock.overview_page_url = format!(
|
||||
"{}/year/{}/page/{}",
|
||||
lock.base_dir,
|
||||
Helper::sanitize_string(key),
|
||||
i
|
||||
);
|
||||
lock.overview_title = format!("Blog posts by year: {}", key);
|
||||
|
||||
if i == lock.overview_num_pages {
|
||||
lock.overview_num_posts = cmp::min(
|
||||
indices.len() - lock.num_previews_per_page * (i - 1),
|
||||
lock.num_previews_per_page,
|
||||
);
|
||||
}
|
||||
|
||||
if lock.overview_current_page == 1 {
|
||||
// create dir recursively:
|
||||
Helper::create_dir_all(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.base_dir)
|
||||
.join("year")
|
||||
.join(Helper::sanitize_string(key)),
|
||||
);
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.base_dir)
|
||||
.join("year")
|
||||
.join(Helper::sanitize_string(key))
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// create dir recursively:
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.overview_page_url));
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&lock.overview_page_url)
|
||||
.join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
lock.overview_offset += lock.overview_num_posts;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn generate_atom_feed(shared: Arc<BlogShared>) {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
|
||||
let mut _feed_data = String::new();
|
||||
let date_now = get_date_now_for_feed();
|
||||
|
||||
// header info:
|
||||
_feed_data = String::from("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n");
|
||||
_feed_data += "<feed xmlns=\"http://www.w3.org/2005/Atom\">\n";
|
||||
_feed_data += " <id>https:/www.luflow.net/feeds/blog.atom</id>\n";
|
||||
_feed_data += " <title>luflow.net Blog</title>\n";
|
||||
_feed_data += " <updated>";
|
||||
_feed_data += &date_now;
|
||||
_feed_data += "</updated>\n";
|
||||
_feed_data += " <generator>https://luflow.net/git/hfsoulz/flow-web.git/</generator>\n";
|
||||
_feed_data += " <author>\n";
|
||||
_feed_data += " <name>luflow.net</name>\n";
|
||||
_feed_data += " <uri>https://www.luflow.net/</uri>\n";
|
||||
_feed_data += " </author>\n";
|
||||
_feed_data += " <link rel=\"alternate\" href=\"https:/www.luflow.net/blog/\"/>\n";
|
||||
_feed_data += " <link rel=\"self\" href=\"https:/www.luflow.net/feeds/blog.atom\"/>\n";
|
||||
_feed_data += " <subtitle>This blog is dedicated to free software in general.</subtitle>\n";
|
||||
_feed_data += " <logo>https:/www.luflow.net/static/img/icon.png</logo>\n";
|
||||
_feed_data += " <icon>https:/www.luflow.net/favicon.ico</icon>\n";
|
||||
|
||||
// each blog entry:
|
||||
for blog_post in &lock.blog_posts {
|
||||
_feed_data += " <entry>\n";
|
||||
|
||||
// author:
|
||||
_feed_data += " <author>\n";
|
||||
_feed_data += " <name>Andreas</name>\n";
|
||||
_feed_data += " </author>\n";
|
||||
|
||||
// title:
|
||||
_feed_data += " <title type=\"html\"><![CDATA[";
|
||||
_feed_data += &blog_post.title;
|
||||
_feed_data += "]]></title>\n";
|
||||
|
||||
// link href:
|
||||
_feed_data += " <link href=\"https:/www.luflow.net/";
|
||||
_feed_data += &lock.base_dir;
|
||||
_feed_data += "/";
|
||||
_feed_data += &blog_post.url;
|
||||
_feed_data += "/\"/>\n";
|
||||
|
||||
// id:
|
||||
_feed_data += " <id>https:/www.luflow.net/";
|
||||
_feed_data += &lock.base_dir;
|
||||
_feed_data += "/";
|
||||
_feed_data += &blog_post.url;
|
||||
_feed_data += "/</id>\n";
|
||||
|
||||
// updated:
|
||||
_feed_data += " <updated>";
|
||||
_feed_data += &blog_post.updated_for_feed;
|
||||
_feed_data += "</updated>\n";
|
||||
|
||||
// published:
|
||||
_feed_data += " <published>";
|
||||
_feed_data += &blog_post.published_for_feed;
|
||||
_feed_data += "</published>\n";
|
||||
|
||||
// categories:
|
||||
for topic in &blog_post.topics {
|
||||
_feed_data += " <category term=\"";
|
||||
_feed_data += &topic;
|
||||
_feed_data += "\"/>\n";
|
||||
}
|
||||
|
||||
// summary:
|
||||
_feed_data += " <summary type=\"html\"><![CDATA[";
|
||||
_feed_data += &blog_post.snippet;
|
||||
_feed_data += "]]></summary>\n";
|
||||
|
||||
// content:
|
||||
_feed_data += " <content type=\"html\"><![CDATA[";
|
||||
_feed_data += &blog_post.html;
|
||||
_feed_data += "]]></content>\n";
|
||||
|
||||
_feed_data += " </entry>\n";
|
||||
}
|
||||
|
||||
// close feed:
|
||||
_feed_data += "</feed>";
|
||||
|
||||
// write it out:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir().join("feeds").join("blog.atom"),
|
||||
_feed_data.as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn get_date_now_for_feed() -> String {
|
||||
let date = Local::now();
|
||||
|
||||
let month = date.month();
|
||||
let day = date.day();
|
||||
|
||||
let mut month_str = month.to_string();
|
||||
let mut day_str = day.to_string();
|
||||
|
||||
if month < 10 {
|
||||
month_str = format!("0{}", month);
|
||||
}
|
||||
|
||||
if day < 10 {
|
||||
day_str = format!("0{}", day);
|
||||
}
|
||||
|
||||
return format!("{}-{}-{}T{}Z", date.year(), month_str, day_str, date.time());
|
||||
}
|
||||
192
src/site/blog_post.rs
Normal file
192
src/site/blog_post.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use chrono::{Datelike, NaiveDateTime};
|
||||
use sailfish::Template;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::site::helper::Helper;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "blog_post.stpl")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BlogPost {
|
||||
pub base_dir: String,
|
||||
|
||||
pub author: String,
|
||||
pub published: NaiveDateTime,
|
||||
pub published_for_feed: String,
|
||||
pub updated: NaiveDateTime,
|
||||
pub updated_for_feed: String,
|
||||
pub topics_comma_separated: String,
|
||||
pub topics: Vec<String>,
|
||||
pub topics_sanitized: Vec<String>,
|
||||
pub topic_base_dir: String,
|
||||
pub title: String,
|
||||
pub snippet: String,
|
||||
pub html: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl BlogPost {
|
||||
pub fn new(base_dir: String) -> BlogPost {
|
||||
BlogPost {
|
||||
base_dir: base_dir,
|
||||
|
||||
author: String::new(),
|
||||
published: NaiveDateTime::parse_from_str("2000-01-01 23:56:04", "%Y-%m-%d %H:%M:%S")
|
||||
.unwrap(),
|
||||
published_for_feed: String::new(),
|
||||
updated: NaiveDateTime::parse_from_str("2000-01-01 23:56:04", "%Y-%m-%d %H:%M:%S")
|
||||
.unwrap(),
|
||||
updated_for_feed: String::new(),
|
||||
topics_comma_separated: String::new(),
|
||||
topics: Vec::new(),
|
||||
topics_sanitized: Vec::new(),
|
||||
topic_base_dir: String::from("topic"),
|
||||
title: String::new(),
|
||||
snippet: String::new(),
|
||||
html: String::new(),
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_topics(&self) -> &Vec<String> {
|
||||
return &self.topics;
|
||||
}
|
||||
|
||||
pub fn get_year(&self) -> String {
|
||||
return self.published.year().to_string();
|
||||
}
|
||||
|
||||
pub fn get_published_date(&self) -> &NaiveDateTime {
|
||||
return &self.published;
|
||||
}
|
||||
|
||||
fn get_date_for_feed(&self, date: &NaiveDateTime) -> String {
|
||||
let month = date.month();
|
||||
let day = date.day();
|
||||
|
||||
let mut month_str = month.to_string();
|
||||
let mut day_str = day.to_string();
|
||||
|
||||
if month < 10 {
|
||||
month_str = format!("0{}", month);
|
||||
}
|
||||
|
||||
if day < 10 {
|
||||
day_str = format!("0{}", day);
|
||||
}
|
||||
|
||||
return format!(
|
||||
"{}-{}-{}T{}.000Z",
|
||||
date.year(),
|
||||
month_str,
|
||||
day_str,
|
||||
date.time()
|
||||
);
|
||||
}
|
||||
|
||||
pub fn create_output_dir(&self) {
|
||||
Helper::create_dir_all(
|
||||
&Helper::get_output_dir()
|
||||
.join(&self.base_dir)
|
||||
.join(&self.url),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn generate(&self) {
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir()
|
||||
.join(&self.base_dir)
|
||||
.join(&self.url)
|
||||
.join("index.html"),
|
||||
&self.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn parse_markdown_file(path: PathBuf, base_dir: String) -> BlogPost {
|
||||
let contents = tokio::fs::read_to_string(path).await.unwrap();
|
||||
let mut post_start_found = false;
|
||||
let mut markdown = String::new();
|
||||
let mut blog_post = BlogPost::new(base_dir.clone());
|
||||
|
||||
for line in contents.lines() {
|
||||
if !post_start_found && line.len() > 0 {
|
||||
if line == "---" {
|
||||
post_start_found = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let v: Vec<&str> = line.splitn(2, ':').collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
||||
if let Some(key) = v.get(0) {
|
||||
if *key == "author" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.author = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'author'.");
|
||||
}
|
||||
} else if *key == "published" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.published =
|
||||
NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
blog_post.published_for_feed =
|
||||
blog_post.get_date_for_feed(&blog_post.published);
|
||||
} else {
|
||||
panic!("Unable to parse field: 'published'.");
|
||||
}
|
||||
} else if *key == "updated" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.updated =
|
||||
NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S").unwrap();
|
||||
blog_post.updated_for_feed =
|
||||
blog_post.get_date_for_feed(&blog_post.updated);
|
||||
} else {
|
||||
panic!("Unable to parse field: 'updated'.");
|
||||
}
|
||||
} else if *key == "topics" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.topics_comma_separated = String::from(value.trim());
|
||||
let v = blog_post.topics_comma_separated.split(",");
|
||||
for topic in v {
|
||||
let topic_trimmed = topic.trim();
|
||||
blog_post.topics.push(String::from(topic_trimmed));
|
||||
blog_post
|
||||
.topics_sanitized
|
||||
.push(Helper::sanitize_string(topic_trimmed));
|
||||
}
|
||||
} else {
|
||||
panic!("Unable to parse field: 'topics'.");
|
||||
}
|
||||
} else if *key == "title" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.title = String::from(value.trim());
|
||||
blog_post.url = Helper::sanitize_string(&blog_post.title);
|
||||
} else {
|
||||
panic!("Unable to parse field: 'title'.");
|
||||
}
|
||||
} else if *key == "snippet" {
|
||||
if let Some(value) = v.get(1) {
|
||||
blog_post.snippet = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'snippet'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if markdown.len() > 0 {
|
||||
markdown += "\n";
|
||||
}
|
||||
markdown += line;
|
||||
}
|
||||
}
|
||||
|
||||
// convert markdown to html:
|
||||
blog_post.html = markdown::to_html(&markdown);
|
||||
|
||||
return blog_post;
|
||||
}
|
||||
174
src/site/core.rs
Normal file
174
src/site/core.rs
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use log::info;
|
||||
use sailfish::Template;
|
||||
use sailfish::TemplateSimple;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::site::blog_post::BlogPost;
|
||||
use crate::site::helper::Helper;
|
||||
use crate::site::screenshot::Screenshot;
|
||||
|
||||
pub struct CoreShared {
|
||||
state: Mutex<CoreState>,
|
||||
}
|
||||
|
||||
impl CoreShared {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(CoreState::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared(self) -> Arc<Self> {
|
||||
Arc::new(self)
|
||||
}
|
||||
|
||||
pub fn set_core_index_data(
|
||||
&self,
|
||||
blog_base_dir: String,
|
||||
blog_posts: Vec<BlogPost>,
|
||||
screenshots: Vec<Screenshot>,
|
||||
) {
|
||||
let mut lock = self.state.lock().unwrap();
|
||||
lock.blog_base_dir = blog_base_dir;
|
||||
lock.blog_posts = blog_posts;
|
||||
lock.screenshots = screenshots;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.stpl")]
|
||||
struct CoreState {
|
||||
pub blog_posts: Vec<BlogPost>,
|
||||
pub screenshots: Vec<Screenshot>,
|
||||
pub hfge_url: String,
|
||||
pub blog_base_dir: String,
|
||||
}
|
||||
|
||||
impl CoreState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
blog_posts: Vec::new(),
|
||||
screenshots: Vec::new(),
|
||||
hfge_url: String::from("projects/hfge"),
|
||||
blog_base_dir: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_core() {
|
||||
// create output dirs needed:
|
||||
create_output_dirs();
|
||||
|
||||
let mut tasks = Vec::with_capacity(4);
|
||||
|
||||
// copy all static related files:
|
||||
tasks.push(tokio::spawn(copy_static_dirs()));
|
||||
|
||||
// generate all core pages (core index will be done as the very
|
||||
// last thing as screenshot and blog generation must be done first):
|
||||
tasks.push(tokio::spawn(generate_error_pages()));
|
||||
tasks.push(tokio::spawn(generate_project_pages()));
|
||||
tasks.push(tokio::spawn(generate_contact_page()));
|
||||
|
||||
// wait until all taks are done:
|
||||
for task in tasks {
|
||||
task.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn create_output_dirs() {
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join("contact"));
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join("projects/hfge"));
|
||||
}
|
||||
|
||||
async fn copy_static_dirs() {
|
||||
// copy static and static_root to output folder:
|
||||
Helper::copy_dir_all(Path::new("static"), Helper::get_output_dir().join("static"))
|
||||
.await
|
||||
.unwrap();
|
||||
info!(
|
||||
"Copied dir 'static' recursively to '{}'",
|
||||
Helper::get_output_dir().join("static").display()
|
||||
);
|
||||
|
||||
Helper::copy_dir_all(Path::new("static_root"), Helper::get_output_dir())
|
||||
.await
|
||||
.unwrap();
|
||||
info!(
|
||||
"Copied dir 'static_root' recursively to '{}'",
|
||||
Helper::get_output_dir().display()
|
||||
);
|
||||
}
|
||||
|
||||
async fn generate_error_pages() {
|
||||
// 404:
|
||||
#[derive(TemplateSimple)]
|
||||
#[template(path = "404.stpl")]
|
||||
struct Err404Template {}
|
||||
|
||||
let ctx = Err404Template {};
|
||||
Helper::write_file(
|
||||
&Helper::get_output_dir().join("404.html"),
|
||||
&ctx.render_once().unwrap().as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 500:
|
||||
#[derive(TemplateSimple)]
|
||||
#[template(path = "500.stpl")]
|
||||
struct Err500Template {}
|
||||
|
||||
let ctx = Err500Template {};
|
||||
Helper::write_file(
|
||||
&Helper::get_output_dir().join("500.html"),
|
||||
&ctx.render_once().unwrap().as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn generate_project_pages() {
|
||||
// projects/hfge:
|
||||
#[derive(TemplateSimple)]
|
||||
#[template(path = "hfge.stpl")]
|
||||
struct HFGETemplate {}
|
||||
|
||||
let ctx = HFGETemplate {};
|
||||
Helper::write_file(
|
||||
&Helper::get_output_dir().join("projects/hfge/index.html"),
|
||||
&ctx.render_once().unwrap().as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn generate_contact_page() {
|
||||
// contact:
|
||||
#[derive(TemplateSimple)]
|
||||
#[template(path = "contact.stpl")]
|
||||
struct ContactTemplate {}
|
||||
|
||||
let ctx = ContactTemplate {};
|
||||
Helper::write_file(
|
||||
&Helper::get_output_dir().join("contact/index.html"),
|
||||
&ctx.render_once().unwrap().as_bytes(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub async fn generate_root_index(shared: Arc<CoreShared>) {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir().join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
123
src/site/helper.rs
Normal file
123
src/site/helper.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use log::info;
|
||||
use regex::Regex;
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{self, AsyncWriteExt};
|
||||
|
||||
pub struct Helper {}
|
||||
|
||||
impl Helper {
|
||||
pub fn get_current_working_dir() -> PathBuf {
|
||||
let res = env::current_dir();
|
||||
match res {
|
||||
Ok(path) => path,
|
||||
Err(_) => PathBuf::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_output_dir() -> PathBuf {
|
||||
let cwd = Helper::get_current_working_dir();
|
||||
return cwd.join("output");
|
||||
}
|
||||
|
||||
/// Replaces spaces with '-', only allows 'a-z', 'A-Z', '0-9' and '-' characters and finally
|
||||
/// converts to lowercase.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `str` - is the string to sanitize.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// let str_to_sanitize = "This is a test, and so on.";
|
||||
/// let str_sanitized = Helper::sanitize_string(str_to_sanitize);
|
||||
/// assert_eq!(str_sanitized, "this-is-a-test-and-so-on");
|
||||
/// ```
|
||||
pub fn sanitize_string(str: &str) -> String {
|
||||
let re_whitespace = Regex::new(r"\s").unwrap();
|
||||
let re_unsupported = Regex::new(r"[^0-9a-zA-Z-]").unwrap();
|
||||
|
||||
// replace all spaces with '-':
|
||||
let after_whitespace = re_whitespace.replace_all(str, "-");
|
||||
// only allow 'a-z' 'A-Z' '0-9' and '-' characters:
|
||||
let after_unsupported = re_unsupported.replace_all(&after_whitespace, "");
|
||||
|
||||
// also convert to lower case:
|
||||
return after_unsupported.to_string().to_lowercase();
|
||||
}
|
||||
|
||||
pub fn create_dir_all<'a>(dir: &'a PathBuf) {
|
||||
match fs::create_dir_all(dir) {
|
||||
Ok(()) => info!("Created dir: '{}'", dir.display()),
|
||||
Err(err) => panic!(
|
||||
"Failed to create dir: '{}'. Error msg: '{}'",
|
||||
dir.display(),
|
||||
err
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn remove_dir_all<'a>(dir: &'a PathBuf) {
|
||||
match fs::remove_dir_all(dir) {
|
||||
Ok(()) => info!("Removed dir: '{}'", dir.display()),
|
||||
Err(err) => panic!(
|
||||
"Failed to remove dir: '{}'. Error msg: '{}'",
|
||||
dir.display(),
|
||||
err
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn exists_dir<'a>(dir: &'a PathBuf) -> bool {
|
||||
return dir.as_path().exists();
|
||||
}
|
||||
|
||||
#[async_recursion::async_recursion]
|
||||
pub async fn copy_dir_all<S, D>(src: S, dst: D) -> Result<(), std::io::Error>
|
||||
where
|
||||
S: AsRef<Path> + Send + Sync,
|
||||
D: AsRef<Path> + Send + Sync,
|
||||
{
|
||||
tokio::fs::create_dir_all(&dst).await?;
|
||||
let mut entries = tokio::fs::read_dir(src).await?;
|
||||
while let Some(entry) = entries.next_entry().await? {
|
||||
let ty = entry.file_type().await?;
|
||||
if ty.is_dir() {
|
||||
Self::copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name())).await?;
|
||||
} else {
|
||||
tokio::fs::copy(entry.path(), dst.as_ref().join(entry.file_name())).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn write_file<'a>(file_path: &'a PathBuf, data: &'a [u8]) -> io::Result<()> {
|
||||
// create output file:
|
||||
let mut file = File::create(file_path).await?;
|
||||
|
||||
// write data to file:
|
||||
file.write_all(data).await?;
|
||||
|
||||
info!("Wrote '{}' successfully", file_path.display());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_file_sync<'a>(file_path: &'a PathBuf, data: &'a [u8]) -> io::Result<()> {
|
||||
// create output file:
|
||||
let mut file = std::fs::File::create(file_path)?;
|
||||
|
||||
// write data to file:
|
||||
file.write_all(data)?;
|
||||
|
||||
info!("Wrote '{}' successfully", file_path.display());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
49
src/site/screenshot.rs
Normal file
49
src/site/screenshot.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use sailfish::Template;
|
||||
|
||||
use crate::site::helper::Helper;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "screenshot.stpl")]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Screenshot {
|
||||
pub screenshots_title: String,
|
||||
pub screenshots_url: String,
|
||||
pub title: String,
|
||||
pub image_min: String,
|
||||
pub image_big: String,
|
||||
pub url: String,
|
||||
pub screenshots: Vec<Screenshot>,
|
||||
}
|
||||
|
||||
impl Screenshot {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
screenshots_title: String::new(),
|
||||
screenshots_url: String::new(),
|
||||
title: String::new(),
|
||||
image_min: String::new(),
|
||||
image_big: String::new(),
|
||||
url: String::new(),
|
||||
screenshots: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate(&self) {
|
||||
// create output dir needed:
|
||||
self.create_output_dir();
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir().join(&self.url).join("index.html"),
|
||||
&self.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn create_output_dir(&self) {
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&self.url));
|
||||
}
|
||||
}
|
||||
217
src/site/screenshots.rs
Normal file
217
src/site/screenshots.rs
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
// luflow.net web site
|
||||
// AGPL-3.0 License (see LICENSE)
|
||||
|
||||
use sailfish::Template;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crate::site::helper::Helper;
|
||||
use crate::site::screenshot::Screenshot;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ScreenshotsInfo {
|
||||
pub title: String,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl ScreenshotsInfo {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
title: String::new(),
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ScreenshotsShared {
|
||||
state: Mutex<ScreenshotsState>,
|
||||
}
|
||||
|
||||
impl ScreenshotsShared {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: Mutex::new(ScreenshotsState::new()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared(self) -> Arc<Self> {
|
||||
Arc::new(self)
|
||||
}
|
||||
|
||||
pub fn get_screenshots(&self, num_screenshots: usize, key: &str) -> Vec<Screenshot> {
|
||||
let lock = self.state.lock().unwrap();
|
||||
let mut screenshots = Vec::new();
|
||||
|
||||
if let Some(value) = lock.screenshots.get(key) {
|
||||
for i in 0..num_screenshots {
|
||||
if let Some(screenshot) = value.get(i) {
|
||||
screenshots.push(screenshot.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panic!("Did not find any screenshots for key: {}", key);
|
||||
}
|
||||
|
||||
return screenshots;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "screenshots.stpl")]
|
||||
#[derive(Clone, Debug)]
|
||||
struct ScreenshotsState {
|
||||
screenshots: HashMap<String, Vec<Screenshot>>,
|
||||
screenshot_urls: HashMap<String, String>,
|
||||
title: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl ScreenshotsState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
screenshots: HashMap::new(),
|
||||
screenshot_urls: HashMap::new(),
|
||||
title: String::new(),
|
||||
url: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_screenshots(shared: Arc<ScreenshotsShared>) {
|
||||
parse_files(shared.clone(), "screenshots").await;
|
||||
}
|
||||
|
||||
async fn parse_files(shared: Arc<ScreenshotsShared>, base_dir: &str) {
|
||||
let mut reader = tokio::fs::read_dir(base_dir).await.unwrap();
|
||||
loop {
|
||||
if let Some(f) = reader.next_entry().await.unwrap() {
|
||||
let contents = tokio::fs::read_to_string(f.path()).await.unwrap();
|
||||
|
||||
parse_file(shared.clone(), contents);
|
||||
update_screenshots(shared.clone());
|
||||
generate(shared.clone());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_file(shared: Arc<ScreenshotsShared>, contents: String) {
|
||||
let mut screenshots_info = ScreenshotsInfo::new();
|
||||
let mut screenshot = Screenshot::new();
|
||||
|
||||
for line in contents.lines() {
|
||||
if line.len() == 0 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let v: Vec<&str> = line.splitn(2, ':').collect();
|
||||
assert_eq!(v.len(), 2);
|
||||
|
||||
if let Some(key) = v.get(0) {
|
||||
if *key == "screenshots_title" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshots_info.title = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'screenshots_title'.");
|
||||
}
|
||||
} else if *key == "screenshots_url" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshots_info.url = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'screenshots_url'.");
|
||||
}
|
||||
} else if *key == "title" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshot.title = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'title'.");
|
||||
}
|
||||
} else if *key == "image_min" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshot.image_min = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'image_min'.");
|
||||
}
|
||||
} else if *key == "image_big" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshot.image_big = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'image_big'.");
|
||||
}
|
||||
} else if *key == "url" {
|
||||
if let Some(value) = v.get(1) {
|
||||
screenshot.url = String::from(value.trim());
|
||||
} else {
|
||||
panic!("Unable to parse field: 'url'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// all info needed for one screenshot:
|
||||
if screenshot.title.len() > 0
|
||||
&& screenshot.image_min.len() > 0
|
||||
&& screenshot.image_big.len() > 0
|
||||
&& screenshot.url.len() > 0
|
||||
&& screenshots_info.title.len() > 0
|
||||
&& screenshots_info.url.len() > 0
|
||||
{
|
||||
screenshot.screenshots_title = screenshots_info.title.clone();
|
||||
screenshot.screenshots_url = screenshots_info.url.clone();
|
||||
|
||||
{
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
|
||||
if let Some(value) = lock.screenshots.get_mut(&screenshots_info.title) {
|
||||
value.push(screenshot);
|
||||
} else {
|
||||
lock.screenshot_urls
|
||||
.insert(screenshots_info.title.clone(), screenshots_info.url.clone());
|
||||
lock.screenshots
|
||||
.insert(screenshots_info.title.clone(), vec![screenshot]);
|
||||
}
|
||||
}
|
||||
|
||||
// reset for new screenshot:
|
||||
screenshot = Screenshot::new();
|
||||
}
|
||||
}
|
||||
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
|
||||
lock.title = screenshots_info.title.clone();
|
||||
lock.url = screenshots_info.url.clone();
|
||||
}
|
||||
|
||||
fn update_screenshots(shared: Arc<ScreenshotsShared>) {
|
||||
// TODO: this could be done more efficiently:
|
||||
// (need screenshots for each screenshot as they are linked below)
|
||||
let mut lock = shared.state.lock().unwrap();
|
||||
for (_key, value) in &mut lock.screenshots {
|
||||
let v = value.clone();
|
||||
for s in value {
|
||||
s.screenshots = v.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate(shared: Arc<ScreenshotsShared>) {
|
||||
let lock = shared.state.lock().unwrap();
|
||||
|
||||
for (_key, screenshots) in &lock.screenshots {
|
||||
// create output dir needed:
|
||||
Helper::create_dir_all(&Helper::get_output_dir().join(&lock.url));
|
||||
|
||||
// write page to disk:
|
||||
Helper::write_file_sync(
|
||||
&Helper::get_output_dir().join(&lock.url).join("index.html"),
|
||||
&lock.render().unwrap().as_bytes(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate all individual screenshot pages:
|
||||
for screenshot in screenshots {
|
||||
screenshot.generate();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue