381 lines
16 KiB
Rust
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())
|
|
}))
|
|
}
|