/* * 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 . */ 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> { 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); } // 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::>(), ) .build() .unwrap()], )] .iter() .map(|(a, b)| (a.to_string(), b.clone())) .collect::>(); 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::>(), ) .extensions( [("atom", atom_link)] .iter() .cloned() .map(|(a, b)| (a.to_string(), b.clone())) .collect::>(), ) .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 { 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 { 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()) })) }