about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgennyble <gen@nyble.dev>2026-10-01 02:29:03 -0500
committergennyble <gen@nyble.dev>2026-10-01 02:29:03 -0500
commitffc831fa44274d4fd6cb9c30fa34e25e590cff44 (patch)
tree97ec56af8efca04b2c19fcd80ff9d72cb86678d2
parent7513c3f9067a0916786bf8f852432d17d5db4f1e (diff)
splorm: opengraph using frontmatter
-rw-r--r--splorm/src/handler/quark/api.rs29
-rw-r--r--splorm/src/handler/quark/markup/frontmatter.rs54
-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.rs65
-rw-r--r--splorm/src/handler/quark/mod.rs13
-rw-r--r--splorm/src/handler/templated.rs15
-rw-r--r--splorm/src/templated/mod.rs4
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 {