From ffc831fa44274d4fd6cb9c30fa34e25e590cff44 Mon Sep 17 00:00:00 2001 From: gennyble Date: Thu, 1 Oct 2026 02:29:03 -0500 Subject: splorm: opengraph using frontmatter --- splorm/src/handler/quark/api.rs | 29 ++- splorm/src/handler/quark/markup.rs | 272 ----------------------- splorm/src/handler/quark/markup/frontmatter.rs | 54 +++++ splorm/src/handler/quark/markup/mod.rs | 292 +++++++++++++++++++++++++ splorm/src/handler/quark/markup/source.rs | 65 ++++++ splorm/src/handler/quark/mod.rs | 13 +- splorm/src/handler/templated.rs | 15 +- 7 files changed, 454 insertions(+), 286 deletions(-) delete mode 100644 splorm/src/handler/quark/markup.rs create mode 100644 splorm/src/handler/quark/markup/frontmatter.rs create mode 100644 splorm/src/handler/quark/markup/mod.rs create mode 100644 splorm/src/handler/quark/markup/source.rs (limited to 'splorm/src/handler') diff --git a/splorm/src/handler/quark/api.rs b/splorm/src/handler/quark/api.rs index c0a6b8e..669ffdc 100644 --- a/splorm/src/handler/quark/api.rs +++ b/splorm/src/handler/quark/api.rs @@ -4,6 +4,8 @@ use axum::{Extension, body::Body, extract::Multipart, http::StatusCode, response use somethings::auth::Session; use wikarden::Wikarden; +use super::PageFrontmatter; + use crate::{ Query, RuntimeError, config::Settings, @@ -81,6 +83,7 @@ pub async fn edit( name, content, commit, + frontmatter, }: EditForm = data.try_into()?; let redirect = api_redirect(&settings, &name, format!("../{name}")); @@ -93,7 +96,9 @@ pub async fn edit( }; tracing::trace!("editing with commit='{commit}'"); - match wikarden.modify(&name, content, commit).await { + let source = format!("{frontmatter}\n{content}"); + + match wikarden.modify(&name, source, commit).await { Err(e) => api_error(&settings, e).await, Ok(_) => redirect.await, } @@ -240,11 +245,12 @@ struct CreateForm { } /// page editing form -#[derive(Debug, PartialEq)] +#[derive(Debug)] struct EditForm { name: String, content: String, commit: Option, + frontmatter: PageFrontmatter<'static>, } /// what an endpoint does; mutually exclusive @@ -263,6 +269,7 @@ struct MultipartFields { name: Option, content: Option, commit: Option, + og_description: Option, create: bool, edit: bool, @@ -274,6 +281,7 @@ impl MultipartFields { let mut name = None; let mut content = None; let mut commit = None; + let mut og_description = None; let mut create = false; let mut edit = false; @@ -301,6 +309,7 @@ impl MultipartFields { Some("name") => name = field_text!(field "name"), Some("content") => content = field_text!(field "content"), Some("commit") => commit = field_text!(field "commit"), + Some("og-description") => og_description = field_text!(field "og_description"), _ => (), } } @@ -309,6 +318,7 @@ impl MultipartFields { name, content, commit, + og_description, create, edit, preview, @@ -324,6 +334,16 @@ impl MultipartFields { _ => Err(RuntimeError::bad_request("too many operations")), } } + + fn frontmatter(&mut self) -> PageFrontmatter<'static> { + let mut pf = PageFrontmatter::default(); + + if let Some(description) = self.og_description.take() { + pf.set_description(description); + } + + pf + } } impl TryInto for MultipartFields { @@ -340,12 +360,15 @@ impl TryInto for MultipartFields { impl TryInto for MultipartFields { type Error = RuntimeError; - fn try_into(self) -> Result { + fn try_into(mut self) -> Result { + let pf = self.frontmatter(); + match (self.name, self.content) { (Some(name), Some(content)) => Ok(EditForm { name, content, commit: self.commit, + frontmatter: pf, }), _ => Err(RuntimeError::bad_request("edit request missing fields")), } diff --git a/splorm/src/handler/quark/markup.rs b/splorm/src/handler/quark/markup.rs deleted file mode 100644 index 6bc2799..0000000 --- a/splorm/src/handler/quark/markup.rs +++ /dev/null @@ -1,272 +0,0 @@ -//! page/content formatting - -use std::{borrow::Cow, rc::Rc}; - -use axum::{ - body::Body, - http::{StatusCode, header}, - response::Response, -}; - -use bempline::Document; -use markup::{ - breakdown::Block, - lineup::{self, InterlinkSolver}, -}; -use paige::Amanda; -use wikarden::{JournalName, Wikarden}; - -use crate::{RuntimeError, config::Settings}; - -pub struct PageTemplate<'r> { - pub settings: &'r Settings, - wikarden: &'r Wikarden, - - pub head_title: &'r str, - pub page_title: &'r str, - pub page: &'r str, - pub with_tools: bool, - pub can_be_deleted: bool, - pub can_be_moved: bool, -} - -impl<'r> PageTemplate<'r> { - pub fn new(settings: &'r Settings, wikarden: &'r Wikarden, page: &'r str) -> Self { - //let template_path = settings.quark_template_dir().join("page.html"); - //let template = super::open_template_path(template_path, &settings).await?; - - Self { - settings, - wikarden, - head_title: Self::no_section(page), - page_title: Self::no_section(page), - page, - with_tools: false, - can_be_deleted: false, - can_be_moved: false, - } - } - - fn no_section(s: &'r str) -> &'r str { - match s.rsplit_once('/') { - None => s, - Some((_, n)) => n, - } - } - - fn page_section(&self) -> Option<&'r str> { - self.page.rsplit_once('/').map(|(s, _)| s) - } - - pub fn set_head_title(&mut self, title: &'r str) { - self.head_title = title; - } - - pub fn set_page_title(&mut self, title: &'r str) { - self.page_title = title; - } - - pub fn set_with_tools(mut self, flag: bool) -> Self { - self.with_tools = flag; - self - } - - pub fn set_can_be_deleted(mut self, flag: bool) -> Self { - self.can_be_deleted = flag; - self - } - - pub fn set_can_be_moved(mut self, flag: bool) -> Self { - self.can_be_moved = flag; - self - } - - fn apply_title(&self, template: &mut Document) { - template.set("title", self.head_title); - template.set("page-title", self.page_title); - } - - fn apply_tools_flags(&self, template: &mut Document) { - if self.with_tools { - template.set("withTools", true); - - if let Some(section) = self.page_section() { - template.set("section", section); - } - } - - if self.can_be_deleted { - template.set("withDelete", true); - } - - if self.can_be_moved { - template.set("withMove", true); - } - } - - async fn apply_recently_edited<'page>(&self, am: &mut Amanda<'page>) { - if am.has("recently-modified") { - tracing::trace!("has modified"); - let modified = self.wikarden.journal_tail(JournalName::Modified, 5).await; - - let iter = modified.iter().map(|jl| format!("- [[{}]]\n", jl.path())); - am.set_many("recently-modified", iter); - } - } - - async fn apply_recently_created<'page>(&self, page: &mut Amanda<'page>) { - if page.has("recently-created") { - tracing::trace!("has created"); - let created = self.wikarden.journal_tail(JournalName::Created, 5).await; - - let iter = created.iter().map(|jl| format!("- [[{}]]\n", jl.path())); - page.set_many("recently-created", iter); - } - } - - pub async fn render(mut self) -> Result { - let template_path = self.settings.quark_template_dir().join("page.html"); - let mut template = super::open_template_path(template_path, &self.settings).await?; - - match self.wikarden.source(self.page).await { - Err(er) => { - self.apply_title(&mut template); - let error_page = format!("# |Error|%red\n{er}"); - template.set("main", render(&error_page, false).body); - } - Ok(source) => { - self.apply_tools_flags(&mut template); - - let mut am = Amanda::parse(&source.data); - self.apply_recently_edited(&mut am).await; - self.apply_recently_created(&mut am).await; - - let render = render(&am.to_string(), true); - if let Some(title) = render.extracted_title.as_ref() { - self.set_head_title(&title.text); - self.set_page_title(&title.html); - } - - self.apply_title(&mut template); - - template.set("main", render.body); - } - } - - Ok(template.compile()) - } - - pub async fn rendered_response(self) -> Result { - let render = self.render().await?; - - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, "text/html") - .body(Body::new(render)) - .map_err(|_| RuntimeError::Unknown) - } -} - -pub struct ExtractedTitle { - pub html: String, - pub text: String, -} - -pub struct RenderResult { - pub extracted_title: Option, - pub body: String, -} - -/// render page content -/// -/// renders page content as `lineup` using the [`Resolver`] `InterlinkResolver` -pub fn render(content: &str, extract_title: bool) -> RenderResult { - let breakdown = markup::breakdown::Document::parse(content); - let resolver = Rc::new(Resolver); - - let mut blocks = breakdown.blocks().iter().peekable(); - - // If the very first block is a header, we take it and use it as the page title. - let title = if extract_title && let Some(Block::Header { .. }) = blocks.peek() { - match blocks.next() { - Some(Block::Header { text, .. }) => { - let parsed = lineup::process(text, resolver.clone()); - let text = parsed.as_text(); - let html = parsed.into_html(); - - Some(ExtractedTitle { html, text }) - } - _ => None, - } - } else { - None - }; - - let mut string = String::new(); - for block in blocks { - match block { - Block::Header { level, text } => { - let rendered = lineup::process(text, resolver.clone()); - string.push_str(&format!("{}\n", rendered.into_html())); - } - Block::Paragraph(str) => { - let rendered = markup::lineup::process(str, resolver.clone()); - string.push_str(&format!("

{}

\n", rendered.into_html())); - } - Block::HorizontalLine => { - string.push_str("
\n"); - } - Block::Code { language, lines } => { - let lang_tag = match language { - Some(lang) => Cow::Owned(format!(" data-lang='{lang}'")), - None => Cow::Borrowed(""), - }; - - string.push_str(&format!("
{lines}
")); - } - Block::List { items } => { - string.push_str("
    \n"); - items.iter().for_each(|li| { - let rendered = markup::lineup::process(li.trim(), resolver.clone()); - string.push_str(&format!("\t
  • {}
  • \n", rendered.into_html())); - }); - string.push_str("
"); - } - } - } - - RenderResult { - extracted_title: title, - body: string, - } -} - -/// build preview response -/// -/// Renders the provided `content`, inserting it into a minimal template for -/// preview in an `