diff options
| author | gennyble <gen@nyble.dev> | 2026-10-01 02:29:03 -0500 |
|---|---|---|
| committer | gennyble <gen@nyble.dev> | 2026-10-01 02:29:03 -0500 |
| commit | ffc831fa44274d4fd6cb9c30fa34e25e590cff44 (patch) | |
| tree | 97ec56af8efca04b2c19fcd80ff9d72cb86678d2 | |
| parent | 7513c3f9067a0916786bf8f852432d17d5db4f1e (diff) | |
splorm: opengraph using frontmatter
| -rw-r--r-- | splorm/src/handler/quark/api.rs | 29 | ||||
| -rw-r--r-- | splorm/src/handler/quark/markup/frontmatter.rs | 54 | ||||
| -rw-r--r-- | splorm/src/handler/quark/markup/mod.rs (renamed from splorm/src/handler/quark/markup.rs) | 24 | ||||
| -rw-r--r-- | splorm/src/handler/quark/markup/source.rs | 65 | ||||
| -rw-r--r-- | splorm/src/handler/quark/mod.rs | 13 | ||||
| -rw-r--r-- | splorm/src/handler/templated.rs | 15 | ||||
| -rw-r--r-- | splorm/src/templated/mod.rs | 4 |
7 files changed, 186 insertions, 18 deletions
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<String>, + frontmatter: PageFrontmatter<'static>, } /// what an endpoint does; mutually exclusive @@ -263,6 +269,7 @@ struct MultipartFields { name: Option<String>, content: Option<String>, commit: Option<String>, + og_description: Option<String>, 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<CreateForm> for MultipartFields { @@ -340,12 +360,15 @@ impl TryInto<CreateForm> for MultipartFields { impl TryInto<EditForm> for MultipartFields { type Error = RuntimeError; - fn try_into(self) -> Result<EditForm, RuntimeError> { + fn try_into(mut self) -> Result<EditForm, RuntimeError> { + 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/frontmatter.rs b/splorm/src/handler/quark/markup/frontmatter.rs new file mode 100644 index 0000000..30728f4 --- /dev/null +++ b/splorm/src/handler/quark/markup/frontmatter.rs @@ -0,0 +1,54 @@ +use std::{borrow::Cow, fmt}; + +use paige::Frontmatter; + +#[derive(Clone, Debug)] +pub struct PageFrontmatter<'r> { + description: Option<Cow<'r, str>>, +} + +impl<'r> PageFrontmatter<'r> { + pub fn description(&self) -> Option<&Cow<'r, str>> { + self.description.as_ref() + } + + pub fn set_description<D: Into<Cow<'r, str>>>(&mut self, description: D) { + self.description = Some(description.into()); + } + + pub fn has_any(&self) -> bool { + self.description.is_some() + } + + pub fn from_frontmatter(&mut self, fm: &'r Frontmatter<'r>) { + self.description = fm.get("description").map(|s| Cow::Borrowed(s)); + } +} + +impl<'r> Default for PageFrontmatter<'r> { + fn default() -> Self { + Self { description: None } + } +} + +impl<'r> fmt::Display for PageFrontmatter<'r> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !self.has_any() { + return Ok(()); + } + + macro_rules! write_field { + ($field:ident) => { + writeln!(f, "{}={}", stringify!($field), $field)?; + }; + } + + writeln!(f, "---")?; + + if let Some(description) = self.description.as_ref() { + write_field!(description); + } + + write!(f, "---") + } +} diff --git a/splorm/src/handler/quark/markup.rs b/splorm/src/handler/quark/markup/mod.rs index 6bc2799..422424b 100644 --- a/splorm/src/handler/quark/markup.rs +++ b/splorm/src/handler/quark/markup/mod.rs @@ -1,4 +1,9 @@ //! page/content formatting +mod frontmatter; +mod source; + +pub use frontmatter::PageFrontmatter; +pub use source::Source; use std::{borrow::Cow, rc::Rc}; @@ -124,11 +129,24 @@ impl<'r> PageTemplate<'r> { } } + fn apply_frontmatter<'p>(&self, template: &mut Document, pf: &PageFrontmatter<'p>) { + if let Some(description) = pf.description() { + let hostname = self.settings.hostname(); + let page = self.page; + + template.set("og_title", self.head_title); + template.set("og_url", format!("https://{hostname}/quark/{page}")); + template.set("og_description", description); + + template.set("og_site_name", format!("{hostname}/quark")); + } + } + pub async fn render(mut self) -> Result<String, RuntimeError> { 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 { + match Source::from_wik(self.wikarden, self.page).await { Err(er) => { self.apply_title(&mut template); let error_page = format!("# |Error|%red\n{er}"); @@ -137,7 +155,7 @@ impl<'r> PageTemplate<'r> { Ok(source) => { self.apply_tools_flags(&mut template); - let mut am = Amanda::parse(&source.data); + let mut am = source.amanda(); self.apply_recently_edited(&mut am).await; self.apply_recently_created(&mut am).await; @@ -147,6 +165,8 @@ impl<'r> PageTemplate<'r> { self.set_page_title(&title.html); } + // Apply frontmatter after we extract the title. It uses the title! + self.apply_frontmatter(&mut template, source.frontmatter()); self.apply_title(&mut template); template.set("main", render.body); diff --git a/splorm/src/handler/quark/markup/source.rs b/splorm/src/handler/quark/markup/source.rs new file mode 100644 index 0000000..41529fc --- /dev/null +++ b/splorm/src/handler/quark/markup/source.rs @@ -0,0 +1,65 @@ +use paige::{Amanda, Frontmatter}; +use wikarden::{WikError, Wikarden}; + +use crate::handler::quark::PageFrontmatter; + +pub struct Source<'p> { + full_content: &'p str, + + frontmatter: PageFrontmatter<'p>, + content: &'p str, +} + +impl<'p> Source<'p> { + pub async fn from_wik(wik: &Wikarden, path: &str) -> Result<Self, WikError> { + let source = wik.source(path).await?; + + //SAFETY: This reference must live for the lifetime of this struct + let source_text: &'p str = Box::leak(source.data.into_boxed_str()); + + let maybe_paige = Self::parse_frontmatter(source_text); + let mut pf = PageFrontmatter::default(); + let mut content = source_text; + + if let Some(paige) = maybe_paige { + let frontmatter_ptr = (&paige.frontmatter) as *const Frontmatter; + let frontmatter: &'p Frontmatter = unsafe { frontmatter_ptr.as_ref_unchecked() }; + pf.from_frontmatter(frontmatter); + content = paige.content; + } + + Ok(Self { + full_content: source_text, + + frontmatter: pf, + content, + }) + } + + fn parse_frontmatter(raw: &'p str) -> Option<paige::Templated<'p>> { + match paige::Templated::parse(raw) { + Ok(tpl) => Some(tpl), + Err(_) => None, + } + } + + pub fn frontmatter(&self) -> &PageFrontmatter<'p> { + &self.frontmatter + } + + pub fn content(&self) -> &str { + self.content + } + + pub fn amanda(&self) -> Amanda<'p> { + Amanda::parse(self.content) + } +} + +impl<'p> Drop for Source<'p> { + fn drop(&mut self) { + // Doing shenenegains because I cannot have a &mut str in the struct + let mut_ptr = self.full_content.as_ptr(); + drop(unsafe { Box::from_raw(mut_ptr as *mut u8) }); + } +} diff --git a/splorm/src/handler/quark/mod.rs b/splorm/src/handler/quark/mod.rs index 815bf2b..f7160d4 100644 --- a/splorm/src/handler/quark/mod.rs +++ b/splorm/src/handler/quark/mod.rs @@ -6,6 +6,8 @@ mod api; pub mod index; mod markup; +pub use markup::PageFrontmatter; + use std::{str::FromStr, sync::Arc}; use axum::{ @@ -28,7 +30,7 @@ use crate::{ config::Settings, extractor::Wrap, fs::{self, RequestPath, SafePath}, - handler::temporary_redirect, + handler::{quark::markup::Source, temporary_redirect}, state::SplormState, }; @@ -144,14 +146,19 @@ async fn page_edit( let edit = fs::read_file_to_string(rp.system_path()).await?; let mut page_template = open_template(&edit, rp.system_path(), &settings).await?; - let src = match wik.source(&page).await { + let src = match Source::from_wik(wik, &page).await { //FIXME: Make a variant for unfound wiki pages Err(_e) => return Err(RuntimeError::Generic404), Ok(src) => src, }; page_template.set("page-editing", page); - page_template.set("page-content", src.data); + page_template.set("page-content", src.content()); + + match src.frontmatter().description() { + Some(desc) => page_template.set("og-description", desc), + None => page_template.set("og-description", ""), + } template.set("mainClass", "main-create"); template.set("quarkForm", "true"); diff --git a/splorm/src/handler/templated.rs b/splorm/src/handler/templated.rs index e8bcf1c..5eec0f8 100644 --- a/splorm/src/handler/templated.rs +++ b/splorm/src/handler/templated.rs @@ -5,8 +5,6 @@ //! //! See [`templated`] for how templates are handled -use std::str::FromStr; - use axum::{body::Body, response::Response}; use bempline::{Document, Options, options::IncludeMethod}; use paige::Templated; @@ -26,7 +24,7 @@ pub async fn send_file( ) -> Result<Response, RuntimeError> { let safe = path.system_path().to_owned(); let string = fs::read_file_to_string(path.system_path()).await?; - let template = Templated::from_str(&string) + let template = Templated::parse(&string) .map_err(|_| RuntimeError::not_a_template(path.system_path().to_path_buf()))?; let tvars = TemplateVariables::build(safe.as_path(), &template.frontmatter); tracing::trace!( @@ -57,12 +55,13 @@ pub async fn send_file( tvars.apply_pathnav(&mut page_template, &path); tvars.apply_opengraph(&mut page_template, &settings, &path); - let mut content = template.content; - if !tvars.features().is_empty() { - content = tvars.apply_features(content, &state).await; - } + let content = if !tvars.features().is_empty() { + let with_features = tvars.apply_features(template.content, &state).await; + tvars.apply_markup(&mut page_template, &with_features) + } else { + tvars.apply_markup(&mut page_template, template.content) + }; - let content = tvars.apply_markup(&mut page_template, content); page_template.set("main", content); Response::builder() diff --git a/splorm/src/templated/mod.rs b/splorm/src/templated/mod.rs index f67674f..bc100ba 100644 --- a/splorm/src/templated/mod.rs +++ b/splorm/src/templated/mod.rs @@ -93,7 +93,7 @@ impl<'fm> TemplateVariables<'fm> { /// Use the `markup` variable to determine how to process the content of /// the template - pub fn apply_markup(&self, doc: &mut Document, content: String) -> String { + pub fn apply_markup(&self, doc: &mut Document, content: &str) -> String { match self.markup { None | Some(MarkupStyle::Legacy) => { let markedup = markup::legacy::process(&content); @@ -157,7 +157,7 @@ impl<'fm> TemplateVariables<'fm> { } /// Parse the provided content as a template and apply selected features. - pub async fn apply_features(&self, content: String, state: &SplormState) -> String { + pub async fn apply_features(&self, content: &str, state: &SplormState) -> String { let mut doc = Document::from_str(&content, Options::default()).unwrap(); for feat in &self.features { match feat { |
