More modular rendering of posts, post publish time now lives in the header
parent
14726ca7d4
commit
341ccde5d5
@ -0,0 +1 @@
|
|||||||
|
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
use std::{error, fmt};
|
||||||
|
|
||||||
|
use kdl::KdlError;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
NoDelim,
|
||||||
|
Syntax(Box<KdlError>),
|
||||||
|
FieldMissing {
|
||||||
|
field: &'static str,
|
||||||
|
},
|
||||||
|
BadType {
|
||||||
|
field: &'static str,
|
||||||
|
expected: &'static str,
|
||||||
|
},
|
||||||
|
IdTooLong(usize),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::NoDelim => {
|
||||||
|
write!(f, "post has no header; no delimiter `\\n---\\n` found")
|
||||||
|
},
|
||||||
|
Self::Syntax(err) => {
|
||||||
|
write!(f, "syntax error in post header: {}", err)
|
||||||
|
},
|
||||||
|
Self::FieldMissing { field } => {
|
||||||
|
write!(f, "missing required post header field `{}`", field)
|
||||||
|
},
|
||||||
|
Self::BadType { field, expected } => {
|
||||||
|
write!(f, "expected post header field `{}` to be {}", field, expected)
|
||||||
|
},
|
||||||
|
Self::IdTooLong(len) => {
|
||||||
|
write!(f, "post id too long: {} bytes", len)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for Error {}
|
||||||
|
|
||||||
|
impl From<KdlError> for Error {
|
||||||
|
fn from(err: KdlError) -> Self {
|
||||||
|
Self::Syntax(Box::new(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,117 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use kdl::KdlDocument;
|
||||||
|
use libshire::strings::ShString22;
|
||||||
|
|
||||||
|
use crate::time::{datetime_unix_seconds, unix_epoch};
|
||||||
|
|
||||||
|
use super::error::Error;
|
||||||
|
|
||||||
|
pub struct Header {
|
||||||
|
pub(super) title: String,
|
||||||
|
pub(super) subtitle: Option<String>,
|
||||||
|
pub(super) author: ShString22,
|
||||||
|
pub(super) tags: Vec<ShString22>,
|
||||||
|
pub(super) published: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
&self.title
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn subtitle(&self) -> Option<&str> {
|
||||||
|
self.subtitle.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn author(&self) -> &str {
|
||||||
|
&self.author
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn tags(&self) -> &[ShString22] {
|
||||||
|
&self.tags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn published(&self) -> DateTime<Utc> {
|
||||||
|
self.published
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> TryFrom<&'a KdlDocument> for Header {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(doc: &'a KdlDocument) -> Result<Self, Self::Error> {
|
||||||
|
let title = doc
|
||||||
|
.get_arg("title")
|
||||||
|
.ok_or(Error::FieldMissing { field: "title" })
|
||||||
|
.and_then(|value| value
|
||||||
|
.as_string()
|
||||||
|
.ok_or(Error::BadType { field: "title", expected: "string" })
|
||||||
|
.map(|title| title.to_owned()))?;
|
||||||
|
|
||||||
|
let subtitle = doc
|
||||||
|
.get_arg("subtitle")
|
||||||
|
.map(|value| value
|
||||||
|
.as_string()
|
||||||
|
.ok_or(Error::BadType { field: "subtitle", expected: "string" })
|
||||||
|
.map(|subtitle| subtitle.to_owned()))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let author = doc
|
||||||
|
.get_arg("title")
|
||||||
|
.ok_or(Error::FieldMissing { field: "author" })
|
||||||
|
.and_then(|value| value
|
||||||
|
.as_string()
|
||||||
|
.ok_or(Error::BadType { field: "author", expected: "string" })
|
||||||
|
.map(ShString22::from))?;
|
||||||
|
|
||||||
|
let tags = doc
|
||||||
|
.get("tags")
|
||||||
|
.map(|node| node.entries())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|entry| match entry.name() {
|
||||||
|
Some(_) => None,
|
||||||
|
None => Some(entry.value()),
|
||||||
|
})
|
||||||
|
.map(|value| value
|
||||||
|
.as_string()
|
||||||
|
.ok_or(Error::BadType { field: "tag", expected: "string" })
|
||||||
|
.map(ShString22::from))
|
||||||
|
.collect::<Result<_, _>>()?;
|
||||||
|
|
||||||
|
let published = doc
|
||||||
|
.get_arg("published")
|
||||||
|
.map(|value| value
|
||||||
|
.as_i64()
|
||||||
|
.ok_or(Error::BadType { field: "published", expected: "integer unix timestamp (seconds)" })
|
||||||
|
.map(datetime_unix_seconds))
|
||||||
|
.transpose()?
|
||||||
|
.unwrap_or_else(unix_epoch);
|
||||||
|
|
||||||
|
Ok(Header {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
author,
|
||||||
|
tags,
|
||||||
|
published,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<KdlDocument> for Header {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(value: KdlDocument) -> Result<Self, Self::Error> {
|
||||||
|
Self::try_from(&value)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,31 @@
|
|||||||
|
use super::{error::Error, header::Header, source::PostSource};
|
||||||
|
|
||||||
|
pub struct MarkdownPost {
|
||||||
|
pub(super) header: Header,
|
||||||
|
pub(super) markdown: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MarkdownPost {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn header(&self) -> &Header {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn markdown(&self) -> &str {
|
||||||
|
&self.markdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<PostSource> for MarkdownPost {
|
||||||
|
type Error = Error;
|
||||||
|
|
||||||
|
fn try_from(source: PostSource) -> Result<Self, Self::Error> {
|
||||||
|
Ok(Self {
|
||||||
|
header: source.header.try_into()?,
|
||||||
|
markdown: source.markdown,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,61 +1,18 @@
|
|||||||
|
mod error;
|
||||||
mod id;
|
mod id;
|
||||||
mod parse;
|
mod header;
|
||||||
|
mod markdown_post;
|
||||||
use chrono::{DateTime, Utc};
|
mod render;
|
||||||
use libshire::{strings::ShString22, uuid::Uuid};
|
mod rendered_post;
|
||||||
use maud::{Markup, PreEscaped};
|
mod source;
|
||||||
|
|
||||||
pub use id::PostId;
|
pub use error::Error;
|
||||||
pub use parse::{parse, ParseError};
|
pub use header::Header;
|
||||||
|
pub use id::Id;
|
||||||
|
pub use markdown_post::MarkdownPost;
|
||||||
|
pub use rendered_post::RenderedPost;
|
||||||
|
pub use source::PostSource;
|
||||||
|
|
||||||
const POST_FILE_EXTENSION: &str = ".kdl.md";
|
const POST_FILE_EXTENSION: &str = ".kdl.md";
|
||||||
|
|
||||||
pub struct Post {
|
pub type Post = RenderedPost;
|
||||||
uuid: Uuid,
|
|
||||||
id: PostId,
|
|
||||||
title: String,
|
|
||||||
subtitle: Option<String>,
|
|
||||||
author: ShString22,
|
|
||||||
html: Markup,
|
|
||||||
tags: Vec<ShString22>,
|
|
||||||
created: DateTime<Utc>,
|
|
||||||
updated: DateTime<Utc>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Post {
|
|
||||||
pub fn uuid(&self) -> Uuid {
|
|
||||||
self.uuid
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id(&self) -> &PostId {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn title(&self) -> &str {
|
|
||||||
&self.title
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subtitle(&self) -> Option<&str> {
|
|
||||||
self.subtitle.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn author(&self) -> &str {
|
|
||||||
&self.author
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn html(&self) -> PreEscaped<&str> {
|
|
||||||
PreEscaped(&self.html.0)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tags(&self) -> &[ShString22] {
|
|
||||||
&self.tags
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn created(&self) -> DateTime<Utc> {
|
|
||||||
self.created
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn updated(&self) -> DateTime<Utc> {
|
|
||||||
self.updated
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,215 +0,0 @@
|
|||||||
use std::{error, fmt};
|
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
|
||||||
use libshire::{strings::ShString22, uuid::{Uuid, UuidV5Error}};
|
|
||||||
use maud::{html, PreEscaped};
|
|
||||||
|
|
||||||
use crate::codeblock::CodeBlockRenderer;
|
|
||||||
|
|
||||||
use super::{id::PostId, Post};
|
|
||||||
|
|
||||||
pub fn parse(
|
|
||||||
code_renderer: &CodeBlockRenderer,
|
|
||||||
namespace: Uuid,
|
|
||||||
post_id: PostId,
|
|
||||||
file_name: &str,
|
|
||||||
created: DateTime<Utc>,
|
|
||||||
updated: DateTime<Utc>,
|
|
||||||
source: &str,
|
|
||||||
) -> Result<Post, ParseError>
|
|
||||||
{
|
|
||||||
MdPost::parse(file_name, source)
|
|
||||||
.and_then(|post| render_mdpost(
|
|
||||||
code_renderer,
|
|
||||||
namespace,
|
|
||||||
post_id,
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
post
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_mdpost(
|
|
||||||
code_renderer: &CodeBlockRenderer,
|
|
||||||
namespace: Uuid,
|
|
||||||
id: PostId,
|
|
||||||
created: DateTime<Utc>,
|
|
||||||
updated: DateTime<Utc>,
|
|
||||||
mdpost: MdPost,
|
|
||||||
) -> Result<Post, ParseError>
|
|
||||||
{
|
|
||||||
use pulldown_cmark::{Options, Parser, html::push_html};
|
|
||||||
|
|
||||||
const PARSER_OPTIONS: Options = Options::ENABLE_TABLES
|
|
||||||
.union(Options::ENABLE_FOOTNOTES)
|
|
||||||
.union(Options::ENABLE_STRIKETHROUGH);
|
|
||||||
|
|
||||||
let uuid = Uuid::new_v5(namespace, &*id)
|
|
||||||
.map_err(|err| match err {
|
|
||||||
UuidV5Error::NameTooLong(len) => ParseError::IdTooLong(len),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let mut parser = PostMdParser::new(
|
|
||||||
Parser::new_ext(&mdpost.markdown, PARSER_OPTIONS),
|
|
||||||
code_renderer
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut html_buf = String::new();
|
|
||||||
push_html(&mut html_buf, parser.by_ref());
|
|
||||||
|
|
||||||
Ok(Post {
|
|
||||||
uuid,
|
|
||||||
id,
|
|
||||||
title: mdpost.title,
|
|
||||||
subtitle: mdpost.subtitle,
|
|
||||||
author: mdpost.author,
|
|
||||||
html: PreEscaped(html_buf),
|
|
||||||
tags: mdpost.tags,
|
|
||||||
created,
|
|
||||||
updated,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Iterator struct which wraps another event iterator in order to render code blocks, collect the links
|
|
||||||
/// encountered and generate a summary of the text content.
|
|
||||||
struct PostMdParser<'p, I> {
|
|
||||||
iter: I,
|
|
||||||
code_renderer: &'p CodeBlockRenderer,
|
|
||||||
links: Vec<String>,
|
|
||||||
summary: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'p, I> PostMdParser<'p, I> {
|
|
||||||
fn new(iter: I, code_renderer: &'p CodeBlockRenderer) -> Self {
|
|
||||||
Self {
|
|
||||||
iter,
|
|
||||||
code_renderer,
|
|
||||||
links: Vec::new(),
|
|
||||||
summary: String::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'e, 'p, I> Iterator for PostMdParser<'p, I> where I: Iterator<Item = pulldown_cmark::Event<'e>> {
|
|
||||||
type Item = pulldown_cmark::Event<'e>;
|
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
use pulldown_cmark::{CodeBlockKind, CowStr, Event, LinkType, Tag};
|
|
||||||
|
|
||||||
self.iter.next().map(|event| match event {
|
|
||||||
// When we reach a code block, we want to collect the text content until the code block finishes
|
|
||||||
// and have the `CodeBlockRenderer` render it
|
|
||||||
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
|
|
||||||
let mut code_buf = String::new();
|
|
||||||
|
|
||||||
for event in self.iter.by_ref() {
|
|
||||||
match event {
|
|
||||||
// The code block has finished, so break out of the loop
|
|
||||||
Event::End(Tag::CodeBlock(_)) => break,
|
|
||||||
// All text events until the end of the code block should be considered as code, so
|
|
||||||
// add the text to the `code_buf` to be rendered later
|
|
||||||
Event::Text(text) => code_buf.push_str(&text),
|
|
||||||
// Ignore all other events
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let highlighted = self.code_renderer.render(&lang, &code_buf);
|
|
||||||
Event::Html(CowStr::Boxed(highlighted.into_string().into_boxed_str()))
|
|
||||||
},
|
|
||||||
|
|
||||||
Event::Code(code) => {
|
|
||||||
Event::Html(CowStr::Boxed(html! {
|
|
||||||
code .inline_code { (code) }
|
|
||||||
}.into_string().into_boxed_str()))
|
|
||||||
},
|
|
||||||
|
|
||||||
event => {
|
|
||||||
match &event {
|
|
||||||
Event::Start(Tag::Link(LinkType::Inline | LinkType::Autolink, destination, _title)) => {
|
|
||||||
self.links.push(destination.clone().into_string());
|
|
||||||
},
|
|
||||||
|
|
||||||
//TODO: better way of generating a summary
|
|
||||||
Event::Text(text) => {
|
|
||||||
if self.summary.is_empty() {
|
|
||||||
self.summary = text.clone().into_string();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
event
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(knuffel::Decode)]
|
|
||||||
struct HeaderNode {
|
|
||||||
#[knuffel(child, unwrap(argument))]
|
|
||||||
title: String,
|
|
||||||
#[knuffel(child, unwrap(argument))]
|
|
||||||
subtitle: Option<String>,
|
|
||||||
#[knuffel(child, unwrap(argument))]
|
|
||||||
author: String,
|
|
||||||
#[knuffel(children(name="tag"))]
|
|
||||||
tags: Vec<TagNode>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(knuffel::Decode)]
|
|
||||||
struct TagNode {
|
|
||||||
#[knuffel(argument)]
|
|
||||||
tag: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct MdPost {
|
|
||||||
markdown: String,
|
|
||||||
title: String,
|
|
||||||
subtitle: Option<String>,
|
|
||||||
author: ShString22,
|
|
||||||
tags: Vec<ShString22>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MdPost {
|
|
||||||
fn parse(file_name: &str, source: &str) -> Result<Self, ParseError> {
|
|
||||||
const END_OF_HEADER_DELIM: &str = "\n---\n";
|
|
||||||
|
|
||||||
let (header, md) = source.split_once(END_OF_HEADER_DELIM)
|
|
||||||
.ok_or(ParseError::MissingHeader)?;
|
|
||||||
|
|
||||||
let header = knuffel::parse::<HeaderNode>(file_name, header)
|
|
||||||
.map_err(|err| ParseError::InvalidHeader(Box::new(err)))?;
|
|
||||||
|
|
||||||
let md = md.trim_start();
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
markdown: md.to_owned(),
|
|
||||||
title: header.title,
|
|
||||||
subtitle: header.subtitle,
|
|
||||||
author: header.author.into(),
|
|
||||||
tags: header.tags.into_iter().map(|tag| tag.tag.into()).collect(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ParseError {
|
|
||||||
MissingHeader,
|
|
||||||
InvalidHeader(Box<knuffel::Error>),
|
|
||||||
IdTooLong(usize),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ParseError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::MissingHeader => write!(f, "post file has no header"),
|
|
||||||
Self::InvalidHeader(err) => fmt::Display::fmt(err, f),
|
|
||||||
Self::IdTooLong(len) => write!(f, "post id too long ({} bytes)", len),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl error::Error for ParseError {}
|
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
use maud::{html, Markup, PreEscaped};
|
||||||
|
use pulldown_cmark::{
|
||||||
|
CodeBlockKind,
|
||||||
|
CowStr,
|
||||||
|
Event,
|
||||||
|
Options,
|
||||||
|
Parser,
|
||||||
|
Tag,
|
||||||
|
html::push_html,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::codeblock::CodeBlockRenderer;
|
||||||
|
|
||||||
|
pub(super) fn render_markdown(code_renderer: &CodeBlockRenderer, markdown: &str) -> Markup {
|
||||||
|
const PARSER_OPTIONS: Options = Options::ENABLE_TABLES
|
||||||
|
.union(Options::ENABLE_FOOTNOTES)
|
||||||
|
.union(Options::ENABLE_STRIKETHROUGH);
|
||||||
|
|
||||||
|
let mut parser = {
|
||||||
|
let parser = Parser::new_ext(markdown, PARSER_OPTIONS);
|
||||||
|
PostMdParser::new(parser, code_renderer)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut html_buf = String::new();
|
||||||
|
push_html(&mut html_buf, parser.by_ref());
|
||||||
|
|
||||||
|
PreEscaped(html_buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Iterator struct which wraps another event iterator in order to render code blocks, collect the links
|
||||||
|
/// encountered and generate a summary of the text content.
|
||||||
|
struct PostMdParser<'p, I> {
|
||||||
|
iter: I,
|
||||||
|
code_renderer: &'p CodeBlockRenderer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'p, I> PostMdParser<'p, I> {
|
||||||
|
fn new(iter: I, code_renderer: &'p CodeBlockRenderer) -> Self {
|
||||||
|
Self {
|
||||||
|
iter,
|
||||||
|
code_renderer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'e, 'p, I> Iterator for PostMdParser<'p, I> where I: Iterator<Item = pulldown_cmark::Event<'e>> {
|
||||||
|
type Item = pulldown_cmark::Event<'e>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.iter.next().map(|event| match event {
|
||||||
|
// When we reach a code block, we want to collect the text content until the code block finishes
|
||||||
|
// and have the `CodeBlockRenderer` render it
|
||||||
|
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(lang))) => {
|
||||||
|
let mut code_buf = String::new();
|
||||||
|
|
||||||
|
for event in self.iter.by_ref() {
|
||||||
|
match event {
|
||||||
|
// The code block has finished, so break out of the loop
|
||||||
|
Event::End(Tag::CodeBlock(_)) => break,
|
||||||
|
// All text events until the end of the code block should be considered as code, so
|
||||||
|
// add the text to the `code_buf` to be rendered later
|
||||||
|
Event::Text(text) => code_buf.push_str(&text),
|
||||||
|
// Ignore all other events
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let highlighted = self.code_renderer.render(&lang, &code_buf);
|
||||||
|
Event::Html(CowStr::Boxed(highlighted.into_string().into_boxed_str()))
|
||||||
|
},
|
||||||
|
|
||||||
|
Event::Code(code) => {
|
||||||
|
Event::Html(CowStr::Boxed(html! {
|
||||||
|
code .inline_code { (code) }
|
||||||
|
}.into_string().into_boxed_str()))
|
||||||
|
},
|
||||||
|
|
||||||
|
event => event,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,121 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use libshire::{strings::ShString22, uuid::{Uuid, UuidV5Error}};
|
||||||
|
use maud::{Markup, PreEscaped};
|
||||||
|
|
||||||
|
use crate::{codeblock::CodeBlockRenderer, time::unix_epoch};
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
error::Error,
|
||||||
|
header::Header,
|
||||||
|
id::Id,
|
||||||
|
markdown_post::MarkdownPost,
|
||||||
|
render::render_markdown,
|
||||||
|
source::PostSource,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct RenderedPost {
|
||||||
|
uuid: Uuid,
|
||||||
|
id: Id,
|
||||||
|
header: Header,
|
||||||
|
updated: DateTime<Utc>,
|
||||||
|
html: Markup,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderedPost {
|
||||||
|
pub fn new_from_str(
|
||||||
|
code_renderer: &CodeBlockRenderer,
|
||||||
|
namespace: Uuid,
|
||||||
|
id: Id,
|
||||||
|
updated: Option<DateTime<Utc>>,
|
||||||
|
source: &str
|
||||||
|
) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let markdown_post = source
|
||||||
|
.parse::<PostSource>()
|
||||||
|
.and_then(MarkdownPost::try_from)?;
|
||||||
|
|
||||||
|
Self::new_from_markdown_post(code_renderer, namespace, id, updated, markdown_post)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_from_markdown_post(
|
||||||
|
code_renderer: &CodeBlockRenderer,
|
||||||
|
namespace: Uuid,
|
||||||
|
id: Id,
|
||||||
|
updated: Option<DateTime<Utc>>,
|
||||||
|
markdown_post: MarkdownPost
|
||||||
|
) -> Result<Self, Error>
|
||||||
|
{
|
||||||
|
let uuid = Uuid::new_v5(namespace, &*id)
|
||||||
|
.map_err(|err| match err {
|
||||||
|
UuidV5Error::NameTooLong(len) => Error::IdTooLong(len),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
uuid,
|
||||||
|
id,
|
||||||
|
header: markdown_post.header,
|
||||||
|
updated: updated.unwrap_or_else(unix_epoch),
|
||||||
|
html: render_markdown(code_renderer, &markdown_post.markdown),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn uuid(&self) -> Uuid {
|
||||||
|
self.uuid
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn id(&self) -> &Id {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn header(&self) -> &Header {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn title(&self) -> &str {
|
||||||
|
self.header.title()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn subtitle(&self) -> Option<&str> {
|
||||||
|
self.header.subtitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn author(&self) -> &str {
|
||||||
|
self.header.author()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn tags(&self) -> &[ShString22] {
|
||||||
|
self.header.tags()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn published(&self) -> DateTime<Utc> {
|
||||||
|
self.header.published()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn updated(&self) -> DateTime<Utc> {
|
||||||
|
self.updated
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn html(&self) -> PreEscaped<&str> {
|
||||||
|
PreEscaped(&self.html.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
use std::{fmt, str};
|
||||||
|
|
||||||
|
use kdl::KdlDocument;
|
||||||
|
|
||||||
|
use super::error::Error;
|
||||||
|
|
||||||
|
const DELIM: &str = "\n---\n";
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct PostSource {
|
||||||
|
pub(super) header: KdlDocument,
|
||||||
|
pub(super) markdown: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PostSource {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn header(&self) -> &KdlDocument {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn header_mut(&mut self) -> &mut KdlDocument {
|
||||||
|
&mut self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn markdown(&self) -> &str {
|
||||||
|
&self.markdown
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn markdown_mut(&mut self) -> &mut String {
|
||||||
|
&mut self.markdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl str::FromStr for PostSource {
|
||||||
|
type Err = Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let (header, markdown) = s.split_once(DELIM)
|
||||||
|
.ok_or(Error::NoDelim)?;
|
||||||
|
|
||||||
|
Ok(PostSource {
|
||||||
|
header: header.parse()?,
|
||||||
|
markdown: markdown.to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for PostSource {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{}{}{}", self.header, DELIM, self.markdown)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,19 @@
|
|||||||
use chrono::{DateTime, NaiveDateTime, Utc};
|
use chrono::{DateTime, NaiveDateTime, Utc};
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn datetime_unix(seconds: i64, nanoseconds: u32) -> DateTime<Utc> {
|
||||||
|
DateTime::from_utc(NaiveDateTime::from_timestamp(seconds, nanoseconds), Utc)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn datetime_unix_seconds(seconds: i64) -> DateTime<Utc> {
|
||||||
|
datetime_unix(seconds, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
pub fn unix_epoch() -> DateTime<Utc> {
|
pub fn unix_epoch() -> DateTime<Utc> {
|
||||||
DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc)
|
datetime_unix(0, 0)
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue