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 parse;
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use libshire::{strings::ShString22, uuid::Uuid};
|
||||
use maud::{Markup, PreEscaped};
|
||||
|
||||
pub use id::PostId;
|
||||
pub use parse::{parse, ParseError};
|
||||
mod header;
|
||||
mod markdown_post;
|
||||
mod render;
|
||||
mod rendered_post;
|
||||
mod source;
|
||||
|
||||
pub use error::Error;
|
||||
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";
|
||||
|
||||
pub struct Post {
|
||||
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
|
||||
}
|
||||
}
|
||||
pub type Post = RenderedPost;
|
||||
|
||||
@ -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};
|
||||
|
||||
#[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> {
|
||||
DateTime::from_utc(NaiveDateTime::from_timestamp(0, 0), Utc)
|
||||
datetime_unix(0, 0)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue