//! page/content formatting mod frontmatter; mod source; pub use frontmatter::PageFrontmatter; pub use source::Source; 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); } } fn apply_frontmatter<'p>(&self, template: &mut Document, pf: &PageFrontmatter<'p>) { if let Some(description) = pf.description() { let og = self.settings.opengraph(); 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); let image = og.default_image(); let image_alt = og.default_image_alt(); template.set("og_image", format!("https://{hostname}/{image}")); template.set("og_image_alt", image_alt); template.set("og_site_name", format!("{hostname}/quark")); } } 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 Source::from_wik(self.wikarden, 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 = source.amanda(); 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); } // 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); } } 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 `