896 lines
29 KiB
Rust
896 lines
29 KiB
Rust
use chrono::prelude::*;
|
|
use std::{env, vec};
|
|
|
|
use blogdb::posts::Post;
|
|
use blogdb::BlogDb;
|
|
use lol_html::html_content::ContentType;
|
|
use lol_html::{comments, element, rewrite_str, RewriteStrSettings};
|
|
|
|
use crate::routes::api::editor::Blocks;
|
|
use crate::routes::SearchResponse;
|
|
|
|
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));
|
|
|
|
/// Gets template html string from `src/templates`
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// // `src/templates/blog-roll.html`
|
|
/// //
|
|
/// // <h1>Blog</h1>
|
|
/// // <ul></ul>
|
|
///
|
|
/// let template = template!("blog-roll");
|
|
/// assert_eq!(template, "<h1>Blog</h1>\n<ul></ul>");
|
|
/// ```
|
|
macro_rules! template {
|
|
($t:tt) => {
|
|
TEMPLATES.get($t).unwrap_or(&"")
|
|
};
|
|
}
|
|
|
|
/// Makes a page with default settings and content inserted into <main></main>
|
|
/// (or replacing <main></main> if content has a <main> tag)
|
|
pub(crate) fn make_page<S>(content: S, settings: PageSettings) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
template!("default"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![
|
|
element!("head", |head| {
|
|
head.prepend(
|
|
&["<title>", &settings.title, "</title>"].concat(),
|
|
ContentType::Html,
|
|
);
|
|
if let Some(stylesheets) = &settings.stylesheets {
|
|
for url in stylesheets {
|
|
head.append(
|
|
&[r#"<link rel="stylesheet" href=""#, url, r#"" />"#].concat(),
|
|
ContentType::Html,
|
|
)
|
|
}
|
|
}
|
|
Ok(())
|
|
}),
|
|
element!("body", |body| {
|
|
body.prepend("<script>0</script>", ContentType::Html);
|
|
Ok(())
|
|
}),
|
|
element!("main", |main| {
|
|
if content.as_ref().contains("<main>") {
|
|
main.replace(content.as_ref(), ContentType::Html);
|
|
} else {
|
|
main.set_inner_content(content.as_ref(), ContentType::Html);
|
|
}
|
|
Ok(())
|
|
}),
|
|
element!("site-header", |site_header| {
|
|
if settings.site_header {
|
|
site_header.replace(template!("site-header"), ContentType::Html);
|
|
}
|
|
Ok(())
|
|
}),
|
|
element!("site-footer", |site_footer| {
|
|
if settings.site_footer {
|
|
site_footer.replace(template!("site-footer"), ContentType::Html);
|
|
}
|
|
Ok(())
|
|
}),
|
|
element!("br", |br| {
|
|
br.remove();
|
|
Ok(())
|
|
}),
|
|
comments!("*", |comments| {
|
|
comments.remove();
|
|
Ok(())
|
|
}),
|
|
],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn rewrite_el<S, T, R>(input: S, name: T, with: R) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
R: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
input.as_ref(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!(name.as_ref(), |el| {
|
|
el.replace(with.as_ref(), ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn remove_el<S, T>(input: S, name: T) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
input.as_ref(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!(name.as_ref(), |el| {
|
|
el.remove();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
fn remove_comments<S: AsRef<str>>(input: S) -> String {
|
|
rewrite_str(
|
|
input.as_ref(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![comments!("*", |co| {
|
|
co.remove();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn homepage_editor<S: AsRef<str>>(homepage_content: S) -> String {
|
|
editor(homepage_content, "/api/editor/update/home")
|
|
}
|
|
|
|
pub(crate) fn about_editor<S: AsRef<str>>(about_content: S) -> String {
|
|
editor(about_content, "/api/editor/update/about")
|
|
}
|
|
|
|
pub(crate) fn draft_editor<S, T>(post_content: S, post_id: T) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
{
|
|
editor(
|
|
post_content,
|
|
["/api/editor/update/", post_id.as_ref()].concat(),
|
|
)
|
|
}
|
|
|
|
fn editor<S, T>(content: S, endpoint: T) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
{
|
|
let content = content.as_ref();
|
|
let data = if !content.is_empty() && content.starts_with("[") && content.ends_with("]") {
|
|
content
|
|
} else {
|
|
"[]"
|
|
};
|
|
let settings = [
|
|
"const data={blocks:",
|
|
data,
|
|
"};const onChange=()=>{editor.save().then(async(savedData)=>{const endpoint = \"",endpoint.as_ref(),"\"; const response = await fetch(endpoint, { method: \"POST\", mode: \"same-origin\", headers: {\"Content-Type\": \"application/json\"}, body: JSON.stringify(savedData.blocks) });})};"
|
|
].concat();
|
|
let html = rewrite_str(
|
|
template!("post-editor"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("#editor", move |script| {
|
|
script.prepend(&settings, ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
strict: false,
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
make_page(
|
|
html,
|
|
PageSettings::new("Editor", Some(vec!["/assets/css/editor.css"]), true, false),
|
|
)
|
|
}
|
|
|
|
/// Rewrites `<login-status></login-status>` with html from template, and sets the `next` hidden
|
|
/// input element to the next location
|
|
pub(crate) fn login_status(next: &str, success: bool) -> String {
|
|
let status = if success {
|
|
""
|
|
} else {
|
|
template!("login-status")
|
|
};
|
|
|
|
let next = next.strip_prefix("/").unwrap_or(next);
|
|
|
|
let html = rewrite_el(template!("login"), "login-status", status);
|
|
|
|
let html = rewrite_str(
|
|
&html,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!(r#"input[name="next"]"#, |input| {
|
|
input.set_attribute("value", next).unwrap_or_default();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
let html = rewrite_str(
|
|
&html,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!(r#"input[name="next"]"#, |input| {
|
|
input.set_attribute("value", next).unwrap_or_default();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
make_page(
|
|
html,
|
|
PageSettings::new("Login", Some(vec!["/assets/css/login.css"]), false, false),
|
|
)
|
|
}
|
|
|
|
pub(crate) async fn admin_page(session_id: i64, db: &BlogDb) -> String {
|
|
let content = admin_widgets(template!("admin"), session_id, db).await;
|
|
make_page(
|
|
&content,
|
|
PageSettings::new("Admin", Some(vec!["/assets/css/admin.css"]), true, false),
|
|
)
|
|
}
|
|
|
|
pub(crate) async fn admin_widgets(input: &str, session_id: i64, db: &BlogDb) -> String {
|
|
let posts_html = admin_entries(EntryType::Post, session_id, db).await;
|
|
let drafts_html = admin_entries(EntryType::Draft, session_id, db).await;
|
|
let user_html = template!("admin-widgets/user");
|
|
|
|
rewrite_str(
|
|
input,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("admin-widget", move |a| {
|
|
match a.get_attribute("type") {
|
|
Some(attr) => {
|
|
match attr.as_str() {
|
|
"drafts" => a.replace(&drafts_html, ContentType::Html),
|
|
"posts" => a.replace(&posts_html, ContentType::Html),
|
|
"user" => a.replace(user_html, ContentType::Html),
|
|
_ => a.remove(),
|
|
}
|
|
Ok(())
|
|
}
|
|
None => todo!(),
|
|
}
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
enum EntryType {
|
|
Post,
|
|
Draft,
|
|
}
|
|
|
|
async fn admin_entries(entry_type: EntryType, session_id: i64, db: &BlogDb) -> String {
|
|
let (list_template, entry_template) = match entry_type {
|
|
EntryType::Post => (
|
|
template!("admin-widgets/posts"),
|
|
template!("admin-widgets/post"),
|
|
),
|
|
EntryType::Draft => (
|
|
template!("admin-widgets/drafts"),
|
|
template!("admin-widgets/draft"),
|
|
),
|
|
};
|
|
let is_empty: bool;
|
|
let entries_html = match entry_type {
|
|
EntryType::Post => {
|
|
let mut entries = db.get_posts().await.unwrap_or(vec![]);
|
|
entries.reverse();
|
|
let mut entry_list_html = String::new();
|
|
is_empty = entries.is_empty();
|
|
if !is_empty {
|
|
for entry in entries {
|
|
let href = ["/blog/", &entry.id].concat();
|
|
let mut entry_html = admin_entry(
|
|
entry_template,
|
|
&entry.id,
|
|
&entry.title,
|
|
&href,
|
|
&entry.content,
|
|
);
|
|
entry_html = rewrite_str(
|
|
&entry_html,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("time", |time| {
|
|
time.set_inner_content(&entry.date, ContentType::Text);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
entry_list_html.push_str(&entry_html);
|
|
}
|
|
}
|
|
entry_list_html
|
|
}
|
|
EntryType::Draft => {
|
|
let mut entries = db.get_drafts(session_id).await.unwrap_or(vec![]);
|
|
entries.reverse();
|
|
let mut entry_list_html = String::new();
|
|
is_empty = entries.is_empty();
|
|
if !is_empty {
|
|
for entry in entries {
|
|
let href = ["/editor/", &entry.id].concat();
|
|
let entry_html = admin_entry(
|
|
entry_template,
|
|
&entry.id,
|
|
&entry.title,
|
|
&href,
|
|
&entry.content,
|
|
);
|
|
entry_list_html.push_str(&entry_html);
|
|
}
|
|
}
|
|
entry_list_html
|
|
}
|
|
};
|
|
let final_html = rewrite_str(
|
|
list_template,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("ul", |ul| {
|
|
if is_empty {
|
|
match entry_type {
|
|
EntryType::Post => ul.replace("<h2 class=\"empty-message\">No Published Posts</h2>", ContentType::Html),
|
|
EntryType::Draft => ul.replace("<div class=\"entry-content empty-message\"><h2>No Drafts</h2><p>Start a <a href=\"/editor/new\" class=\"button\">New Draft</a></p></div>", ContentType::Html)
|
|
}
|
|
} else {
|
|
ul.set_inner_content(&entries_html, ContentType::Html);
|
|
}
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
final_html
|
|
}
|
|
|
|
fn admin_entry(entry_template: &str, id: &str, title: &str, href: &str, content: &str) -> String {
|
|
rewrite_str(
|
|
entry_template,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![
|
|
element!("a.entry-content", |a| {
|
|
a.set_attribute("href", href).unwrap_or_default();
|
|
Ok(())
|
|
}),
|
|
element!("h2", |h2| {
|
|
h2.set_inner_content(title, ContentType::Html);
|
|
Ok(())
|
|
}),
|
|
element!("p", |p| {
|
|
let post_content: Blocks = serde_json::from_str(content).unwrap_or_default();
|
|
let content = rewrite_str(
|
|
&post_content.desc(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("*", |all| {
|
|
all.remove_and_keep_content();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
p.set_inner_content(&content, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("input", |input| {
|
|
input.set_attribute("value", id).unwrap_or_default();
|
|
Ok(())
|
|
}),
|
|
],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) async fn about_page(db: &BlogDb) -> String {
|
|
let about = db.get_about().await;
|
|
let about_content: Blocks = serde_json::from_str(&about).unwrap_or_default();
|
|
let about_html = remove_comments(about_content.to_html());
|
|
make_page(&about_html, PageSettings::title("About"))
|
|
}
|
|
|
|
pub(crate) async fn home_page(db: &BlogDb) -> String {
|
|
let home = db.get_homepage().await;
|
|
let home_content: Blocks = serde_json::from_str(&home).unwrap_or_default();
|
|
let home_html = home_content.to_html();
|
|
make_page(&home_html, PageSettings::title("Evie Ippolito"))
|
|
}
|
|
|
|
pub(crate) fn insert_blog_widget<S>(input: S) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
template!("blog"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("meta", |meta| { Ok(()) })],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap()
|
|
}
|
|
|
|
fn meta<S, T, R, I>(slug: S, title: T, description: R, image: I) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
R: AsRef<str>,
|
|
I: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
template!("head/meta"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("meta", |meta| {
|
|
if let Some(attr) = meta.get_attribute("property") {
|
|
let content = match attr.as_str() {
|
|
"og:url" => {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
&["http://localhost", slug.as_ref()].concat()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
&["https://", env!("DOMAIN"), slug.as_ref()].concat()
|
|
}
|
|
}
|
|
"og:type" => "article",
|
|
"og:title" => title.as_ref(),
|
|
"og:description" => description.as_ref(),
|
|
"og:image" => {
|
|
let image = image.as_ref();
|
|
if !image.is_empty() {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
&["http://localhost", image].concat()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
&["https://", env!("DOMAIN"), image].concat()
|
|
}
|
|
} else {
|
|
meta.remove();
|
|
""
|
|
}
|
|
}
|
|
_ => "",
|
|
};
|
|
let _ = meta.set_attribute("content", content);
|
|
}
|
|
Ok(())
|
|
})],
|
|
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
/// Creates a blog page from a post's content
|
|
pub(crate) async fn blog_page<S>(id: S, db: &BlogDb) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
let template = template!("blog-page");
|
|
let html = make_page(template, PageSettings::title("Blog"));
|
|
|
|
let post = db.get_post(id).await.unwrap_or_default();
|
|
let head_content = head::<_, _, [String; 0]>(post.title, []);
|
|
let post_content: Blocks = serde_json::from_str(&post.content).unwrap_or_default();
|
|
let post_html: String = post_content.to_html();
|
|
let image = if let Some(url) = post_content.image() {
|
|
&[url].concat()
|
|
} else {
|
|
""
|
|
};
|
|
let meta = meta(
|
|
["/blog/", &post.id].concat(),
|
|
post_content.title(),
|
|
post_content.desc(),
|
|
image,
|
|
);
|
|
|
|
let html = rewrite_str(
|
|
&html,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("time", |time| {
|
|
time.after(&post_html, ContentType::Html);
|
|
time.replace(
|
|
&[
|
|
r#"<a class="animated-link" style="align-self: flex-start; margin-block-end: 0" href=""#,
|
|
{
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
"http://localhost"
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
&["https://", env!("DOMAIN")].concat()
|
|
}
|
|
},
|
|
"/blog/",
|
|
&post.id,
|
|
r#""><time>"#,
|
|
&post.date,
|
|
"</time></a>",
|
|
]
|
|
.concat(),
|
|
ContentType::Html,
|
|
);
|
|
Ok(())
|
|
}), element!("head", |head| {
|
|
head.replace(&head_content, ContentType::Html);
|
|
head.append(&meta, ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
html
|
|
}
|
|
|
|
/// Get `count` number of blog entries, if `count` is [None] then get all entries
|
|
async fn get_blog_entries_count(db: &BlogDb, count: Option<u32>) -> String {
|
|
let posts = match count {
|
|
Some(count) => match db.get_count_posts(count).await {
|
|
Ok(posts) => posts,
|
|
Err(_) => return "".to_string(),
|
|
},
|
|
None => match db.get_posts().await {
|
|
Ok(posts) => posts,
|
|
Err(_) => return "".to_string(),
|
|
},
|
|
};
|
|
|
|
let mut post_entries = String::new();
|
|
|
|
for post in posts.iter() {
|
|
let post_entry = post_entry(post);
|
|
post_entries.push_str(&post_entry);
|
|
}
|
|
post_entries
|
|
}
|
|
|
|
pub(crate) async fn blog_roll(db: &BlogDb) -> String {
|
|
let post_entries = get_blog_entries_count(db, None).await;
|
|
|
|
let blog_roll_html = rewrite_str(
|
|
template!("blog-roll"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("ul", move |ul| {
|
|
ul.set_inner_content(&post_entries, ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
make_page(
|
|
blog_roll_html,
|
|
PageSettings::new("Blog", Some(vec!["/assets/css/blog.css"]), true, true),
|
|
)
|
|
}
|
|
|
|
fn post_entry(post: &Post) -> String {
|
|
rewrite_str(
|
|
template!("blog-roll-entry"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![
|
|
element!("a", |a| {
|
|
a.set_attribute("href", &["/blog/", &post.id].concat())
|
|
.unwrap_or_default();
|
|
Ok(())
|
|
}),
|
|
element!("img", |img| {
|
|
let post_content: Blocks =
|
|
serde_json::from_str(&post.content).unwrap_or_default();
|
|
match post_content.image() {
|
|
Some(src) => {
|
|
let _ = img.set_attribute("src", src);
|
|
}
|
|
None => img.remove(),
|
|
}
|
|
Ok(())
|
|
}),
|
|
element!("h2", |h2| {
|
|
h2.set_inner_content(&post.title, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("time", |time| {
|
|
time.set_inner_content(&post.date, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("p", |p| {
|
|
let post_content: Blocks =
|
|
serde_json::from_str(&post.content).unwrap_or_default();
|
|
let content = remove_el(post_content.desc(), "br");
|
|
p.set_inner_content(&content, ContentType::Html);
|
|
Ok(())
|
|
}),
|
|
],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn search_page_no_query() -> String {
|
|
let search_page_html = make_page(template!("search-page"), PageSettings::title("Search"));
|
|
rewrite_str(
|
|
&search_page_html,
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("ul", move |ul| {
|
|
ul.remove();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) async fn search_page(
|
|
query: String,
|
|
search_response: SearchResponse,
|
|
db: &BlogDb,
|
|
) -> String {
|
|
let mut entries = String::new();
|
|
let search_page_html = if !search_response.is_empty() {
|
|
for entry in search_response {
|
|
let post = match db.get_post(&entry.id[0]).await {
|
|
Ok(post) => post,
|
|
Err(_) => continue,
|
|
};
|
|
let entry_html = post_entry(&post);
|
|
entries.push_str(&entry_html);
|
|
}
|
|
[
|
|
"<h1>Results for “",
|
|
&query,
|
|
"”</h1>",
|
|
"<ul>",
|
|
&entries,
|
|
"</ul>",
|
|
]
|
|
.concat()
|
|
} else {
|
|
["<h1>No results for “", &query, "”</h1>"].concat()
|
|
};
|
|
make_page(
|
|
&search_page_html,
|
|
PageSettings::new(
|
|
["Results for “", &query, "”"].concat(),
|
|
Some(vec!["/assets/css/blog.css"]),
|
|
true,
|
|
true,
|
|
),
|
|
)
|
|
}
|
|
|
|
/// Creates a <link rel="stylesheet" href="style_url"/> for each given style_url
|
|
fn stylesheets<S, I>(style_urls: I) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
I: IntoIterator<Item = S>,
|
|
{
|
|
style_urls
|
|
.into_iter()
|
|
.map(|url| [r#"<link href="#, url.as_ref(), r#" rel="stylesheet"/>"#].concat())
|
|
.collect::<String>()
|
|
}
|
|
|
|
fn head<S, T, I>(title: S, style_urls: I) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
T: AsRef<str>,
|
|
I: IntoIterator<Item = T>,
|
|
{
|
|
let content = rewrite_str(
|
|
template!("head/content"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("title", move |t| {
|
|
t.set_inner_content(title.as_ref(), ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
let links_html = stylesheets(style_urls);
|
|
[content, links_html].concat()
|
|
}
|
|
|
|
pub(crate) struct PageSettings {
|
|
title: String,
|
|
stylesheets: Option<Vec<String>>,
|
|
site_header: bool,
|
|
site_footer: bool,
|
|
}
|
|
|
|
impl PageSettings {
|
|
pub(crate) fn title<S>(title: S) -> Self
|
|
where
|
|
S: ToString,
|
|
{
|
|
Self {
|
|
title: title.to_string(),
|
|
stylesheets: None,
|
|
site_header: true,
|
|
site_footer: true,
|
|
}
|
|
}
|
|
pub(crate) fn new<S, T>(
|
|
title: S,
|
|
stylesheets: Option<Vec<T>>,
|
|
site_header: bool,
|
|
site_footer: bool,
|
|
) -> Self
|
|
where
|
|
S: ToString,
|
|
T: ToString,
|
|
{
|
|
let stylesheets =
|
|
stylesheets.map(|s| s.iter().map(|t| t.to_string()).collect::<Vec<String>>());
|
|
Self {
|
|
title: title.to_string(),
|
|
stylesheets,
|
|
site_header,
|
|
site_footer,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn make_404<S>(message: S) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
make_page(message, PageSettings::title("Not found"))
|
|
}
|
|
|
|
pub(crate) fn zhuzh_anchors<S>(input: S) -> String
|
|
where
|
|
S: AsRef<str>,
|
|
{
|
|
rewrite_str(
|
|
input.as_ref(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("a", move |t| {
|
|
if let Some(href) = t.get_attribute("href") {
|
|
if !href.starts_with("/") {
|
|
t.set_attribute("target", "_blank").unwrap_or_default();
|
|
}
|
|
}
|
|
match t.get_attribute("class") {
|
|
Some(class) => t
|
|
.set_attribute("class", &[&class, " animated-link-underline"].concat())
|
|
.unwrap_or_default(),
|
|
None => {
|
|
t.set_attribute("class", "animated-link-underline")
|
|
.unwrap_or_default();
|
|
}
|
|
}
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) fn sanitize<S: AsRef<str>>(input: S) -> String {
|
|
rewrite_str(
|
|
input.as_ref(),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("*", move |all| {
|
|
all.remove_and_keep_content();
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub(crate) async fn rss(db: &BlogDb) -> String {
|
|
let posts = db.get_posts().await.unwrap_or_default();
|
|
let mut items = String::new();
|
|
for post in posts {
|
|
let url = {
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
["http://localhost/blog/", &post.id].concat()
|
|
}
|
|
#[cfg(not(debug_assertions))]
|
|
{
|
|
["https://", env!("DOMAIN"), "/blog/", &entry.id].concat()
|
|
}
|
|
};
|
|
let item = rewrite_str(
|
|
template!("item"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![
|
|
element!("title", |title| {
|
|
title.set_inner_content(&post.title, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("link", |link| {
|
|
link.replace(&["<link>", &url, "</link>"].concat(), ContentType::Html); // slight hack to get around lol_html weirdness
|
|
Ok(())
|
|
}),
|
|
element!("guid", |guid| {
|
|
guid.set_inner_content(&url, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("pubDate", |pub_date| {
|
|
let date = {
|
|
let date: [u32; 3] = post
|
|
.date
|
|
.splitn(3, "-")
|
|
.map(|d| d.parse::<u32>().unwrap_or_default())
|
|
.collect::<Vec<u32>>()
|
|
.try_into()
|
|
.unwrap_or_default();
|
|
let date_time: DateTime<Utc> = Utc
|
|
.with_ymd_and_hms(date[0] as i32, date[1], date[2], 0, 0, 0)
|
|
.unwrap();
|
|
date_time.to_rfc2822()
|
|
};
|
|
pub_date.set_inner_content(&date, ContentType::Text);
|
|
Ok(())
|
|
}),
|
|
element!("description", |description| {
|
|
let post_content: Blocks =
|
|
serde_json::from_str(&post.content).unwrap_or_default();
|
|
let desc = ["<![CDATA[", &post_content.to_html(), "]]>"].concat();
|
|
description.set_inner_content(&desc, ContentType::Html);
|
|
Ok(())
|
|
}),
|
|
],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
items.push_str(&item);
|
|
}
|
|
|
|
let feed = rewrite_str(
|
|
template!("feed"),
|
|
RewriteStrSettings {
|
|
element_content_handlers: vec![element!("channel", move |channel| {
|
|
channel.append(&items, ContentType::Html);
|
|
Ok(())
|
|
})],
|
|
..RewriteStrSettings::new()
|
|
},
|
|
)
|
|
.unwrap_or_default();
|
|
|
|
feed
|
|
}
|