meli-website/src/main.rs

381 lines
16 KiB
Rust

/*
* meli website
*
* Copyright 2020 Manos Pitsidianakis
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with meli website. If not, see <http://www.gnu.org/licenses/>.
*/
extern crate linkify;
use libssg;
use rss;
use std::collections::HashMap;
use std::io::Read;
use std::path::Path;
use std::process::{Command, Stdio};
const RSS_BASE_URL: &'static str = "https://meli.delivery/";
const RSS_AUTHOR_EMAIL: &'static str = "epilys@nessuent.xyz (epilys)";
const RSS_FEED_SNAPSHOT: &'static str = "posts";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut state = libssg::State::new()?;
state
.then(libssg::match_pattern(
"^pages/index.md",
libssg::Route::Const("index.html".to_string()),
libssg::Renderer::Pipeline(vec![
libssg::Renderer::Custom(load_posts()),
libssg::Renderer::edit_metadata(Box::new(|_state, context| {
if let Some(posts) = context.get_mut("posts").and_then(|v| v.as_array_mut()) {
posts.truncate(4);
}
context.insert(
"ogImage".to_string(),
"/images/screenshots/conversations.webp".to_string().into(),
);
Ok(Default::default())
})),
libssg::Renderer::LoadAndApplyTemplate("templates/index.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::copy("^css/*", libssg::Route::Id))
.then(libssg::copy("^images/*", libssg::Route::Id))
.then(libssg::match_pattern(
"^posts/*",
libssg::Route::SetExtension("html"),
libssg::Renderer::Pipeline(vec![
libssg::Renderer::edit_metadata(Box::new(|_state, context| {
context.insert("showTitle".to_string(), libssg::Value::Bool(false));
context.insert(
"mainClass".to_string(),
libssg::Value::String("article".to_string()),
);
context.insert("ogType".to_string(), "article".to_string().into());
Ok(Default::default())
})),
libssg::Renderer::LoadAndApplyTemplate("templates/post.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::compiler_seq(
libssg::pandoc(),
Box::new(|state, path| {
let path = path
.strip_prefix(&state.output_dir().parent().unwrap())
.unwrap_or(&path)
.to_path_buf();
if state.verbosity() > 3 {
println!("adding {} to RSS snapshot", path.display());
}
let uuid = libssg::uuid_from_path(&path);
state.add_to_snapshot(RSS_FEED_SNAPSHOT.into(), uuid);
Ok(Default::default())
}),
),
))
.then(libssg::create(
"rss.xml".into(),
libssg::Renderer::Custom(Box::new(|_state, metadata| {
Ok(metadata["body"].as_str().unwrap().to_string())
})),
Box::new(|state, path| {
if !state.snapshots().contains_key(RSS_FEED_SNAPSHOT) {
// No posts configured/found
return Err(format!("There are no snapshots with key `{}`, is the source rule empty (ie producing no items) or have you typed the name wrong?", &RSS_FEED_SNAPSHOT))?;
}
let snapshot = &state.snapshots()[RSS_FEED_SNAPSHOT];
let mut rss_items = Vec::with_capacity(snapshot.len());
for artifact in snapshot.iter() {
let rss_item = rss::ItemBuilder::default()
.title(state.artifacts()[&artifact].metadata["title"].as_str().unwrap().to_string())
.link(format!(
"{}{}",
RSS_BASE_URL,
state.artifacts()[&artifact].path.display()
)
)
.author(RSS_AUTHOR_EMAIL.to_string())
.guid(
rss::GuidBuilder::default()
.value(format!(
"{}{}",
RSS_BASE_URL,
state.artifacts()[&artifact].path.display()
)
)
.permalink(true)
.build()
.unwrap(),
)
.pub_date(
libssg::chrono::DateTime::parse_from_rfc3339(
state.artifacts()[&artifact].metadata["date_iso_8601"].as_str().unwrap(),
)
.unwrap()
.to_rfc2822(),
)
.description(String::new())
.content(state.artifacts()[&artifact].metadata["body"].as_str().unwrap().to_string())
.build()
.unwrap();
rss_items.push(rss_item);
}
//<atom:link href="http://dallas.example.com/rss.xml" rel="self" type="application/rss+xml" />
let atom_link = [(
"link",
vec![rss::extension::ExtensionBuilder::default()
.name("atom:link".to_string())
.attrs(
[
("href".to_string(), format!("{}rss.xml", RSS_BASE_URL)),
("rel".to_string(), "self".to_string()),
("type".to_string(), "application/rss+xml".to_string()),
]
.iter()
.map(|(a, b)| (a.to_string(), b.clone()))
.collect::<HashMap<String, String>>(),
)
.build()
.unwrap()],
)]
.iter()
.map(|(a, b)| (a.to_string(), b.clone()))
.collect::<HashMap<_, _>>();
let mut channel = rss::ChannelBuilder::default()
.title("meli MUA")
.link("https://meli.delivery")
.description("")
.namespaces(
[
("content", "http://purl.org/rss/1.0/modules/content/"),
("atom", "http://www.w3.org/2005/Atom"),
]
.iter()
.cloned()
.map(|(a, b)| (a.to_string(), b.to_string()))
.collect::<std::collections::HashMap<String, String>>(),
)
.extensions(
[("atom", atom_link)]
.iter()
.cloned()
.map(|(a, b)| (a.to_string(), b.clone()))
.collect::<std::collections::HashMap<String, _>>(),
)
.ttl(1800.to_string())
.build()
.unwrap();
channel.set_items(rss_items);
let mut writer = vec![];
//channel.pretty_write_to(&mut writer, b' ', 2).unwrap();
channel.write_to(&mut writer).unwrap();
let mut ret = libssg::Map::default();
ret.insert("body".to_string(), String::from_utf8(writer)?.into());
Ok(ret)
}),
))
/*
.then(libssg::build_rss_feed(
"rss.xml".into(),
libssg::rss_feed(
RSS_FEED_SNAPSHOT.into(),
libssg::RssItem {
title: "meli".into(),
description: "example using libssg".into(),
link: "http://localhost".into(),
last_build_date: String::new(),
pub_date: libssg::chrono::Local::now().to_rfc2822(),
ttl: 1800,
},
),
))
*/
.then(libssg::match_pattern(
"^pages/documentation.md",
libssg::Route::Const("documentation.html".to_string()),
libssg::Renderer::Pipeline(vec![
default_metadata(),
libssg::Renderer::edit_metadata(Box::new(|_state, context| {
context.insert(
"mainClass".to_string(),
libssg::Value::String("article-doc".to_string()),
);
for manpage in &["meli.1", "meli.conf.5", "meli-themes.5"] {
let mut cmd = Command::new("./make_manpage.sh");
let out = cmd.arg(manpage).output()?.stdout;
context.insert(
manpage.replace(".", "_"),
String::from_utf8_lossy(&out).into(),
);
}
Ok(Default::default())
})),
libssg::Renderer::LoadAndApplyTemplate("templates/documentation.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::match_pattern(
"^pages/download.md",
libssg::Route::Const("download.html".to_string()),
libssg::Renderer::Pipeline(vec![
default_metadata(),
libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::match_pattern(
"^pages/screenshots.md",
libssg::Route::Const("screenshots.html".to_string()),
libssg::Renderer::Pipeline(vec![
libssg::Renderer::LoadAndApplyTemplate("templates/screenshots.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::match_pattern(
"^pages/mailing-lists.md",
libssg::Route::Const("mailing-lists.html".to_string()),
libssg::Renderer::Pipeline(vec![
default_metadata(),
libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::create(
"archive.html".into(),
libssg::Renderer::Pipeline(vec![
default_metadata(),
libssg::Renderer::Custom(load_posts()),
libssg::Renderer::LoadAndApplyTemplate("templates/post-list.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::pandoc(),
))
.then(libssg::create(
"sitemap.html".into(),
libssg::Renderer::Pipeline(vec![
default_metadata(),
libssg::Renderer::LoadAndApplyTemplate("templates/sitemap.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default_page.hbs"),
libssg::Renderer::LoadAndApplyTemplate("templates/default.hbs"),
]),
libssg::sitemap(),
))
.finish()?;
if std::env::var("CHECK").is_ok() {
use linkify::{LinkFinder, LinkKind};
fn visit_dirs(dir: &Path) -> std::io::Result<bool> {
let mut ret = true;
if dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
ret = ret && visit_dirs(&path)?;
} else {
if path.extension() == Some(std::ffi::OsStr::new("html")) {
let mut file = std::fs::File::open(&path)?;
let finder = LinkFinder::new();
let mut r = String::new();
file.read_to_string(&mut r)?;
for link in finder.links(&r) {
if *link.kind() != LinkKind::Url {
continue;
}
let out = Command::new("curl")
.args(&["--fail", "-s", "-L", link.as_str()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?
.wait()?;
if !out.success() {
ret = false;
println!(
"{}: Found dead/invalid outgoing link: {}",
path.display(),
link.as_str()
);
}
}
}
}
}
}
Ok(ret)
}
visit_dirs(state.output_dir())?;
}
Ok(())
}
fn load_posts() -> Box<dyn libssg::BFn> {
Box::new(|state, context| {
if !state.snapshots().contains_key(RSS_FEED_SNAPSHOT) {
// No posts configured/found
return Err(format!("There are no snapshots with key `{}`, is the source rule empty (ie producing no items) or have you typed the name wrong?", &RSS_FEED_SNAPSHOT))?;
}
let snapshot = &state.snapshots()[RSS_FEED_SNAPSHOT];
let mut items = Vec::with_capacity(snapshot.len());
for artifact in snapshot.iter() {
let mut item = state.artifacts()[&artifact].metadata.clone();
item.insert(
"url".into(),
state.artifacts()[&artifact]
.path
.display()
.to_string()
.into(),
);
items.push(item);
}
items.sort_by(|a, b| {
b["date_iso_8601"]
.as_str()
.unwrap()
.cmp(&a["date_iso_8601"].as_str().unwrap())
});
context.insert("posts".into(), items.into());
Ok(Default::default())
})
}
fn default_metadata() -> libssg::Renderer {
libssg::Renderer::edit_metadata(Box::new(|_state, context| {
context.insert("showTitle".to_string(), libssg::Value::Bool(true));
context.insert(
"ogImage".to_string(),
"/images/screenshots/conversations.webp".to_string().into(),
);
context.insert(
"mainClass".to_string(),
libssg::Value::String("article".to_string()),
);
Ok(Default::default())
}))
}