More modular rendering of posts, post publish time now lives in the header

main
Pantonshire 4 years ago
parent 14726ca7d4
commit 341ccde5d5

28
Cargo.lock generated

@ -164,6 +164,7 @@ dependencies = [
"atom_syndication",
"axum",
"chrono",
"kdl",
"knuffel",
"libshire",
"maud",
@ -584,6 +585,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]]
name = "kdl"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7ba123fe3f30838b649efd5606531e8326623c5f44491c7e631f3b970e20cdb"
dependencies = [
"miette",
"nom",
"thiserror",
]
[[package]]
name = "kernel32-sys"
version = "0.2.2"
@ -764,6 +776,12 @@ dependencies = [
"unicase",
]
[[package]]
name = "minimal-lexical"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
@ -845,6 +863,16 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
[[package]]
name = "nom"
version = "7.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
dependencies = [
"memchr",
"minimal-lexical",
]
[[package]]
name = "notify"
version = "4.0.17"

@ -25,6 +25,7 @@ mime = "0.3"
maud = "0.23"
atom_syndication = "0.11"
rss = "2"
kdl = "4"
knuffel = "2"
pulldown-cmark = "0.9"
syntect = "4"

@ -1,3 +1,4 @@
mod config;
mod fs_watcher;
mod render;
mod service;

@ -12,9 +12,8 @@ use tracing::{info, warn, error};
use blog::{
codeblock::CodeBlockRenderer,
post::{parse as parse_post, ParseError, Post, PostId},
post::{Error as ParseError, Post, Id},
db::ConcurrentPostsStore,
time::unix_epoch,
};
use crate::Config;
@ -189,14 +188,10 @@ impl Renderer {
return Err(Error::NotAFile);
}
let (created, updated) = metadata.created()
.and_then(|created| metadata.modified()
.map(|modified| (DateTime::from(created), DateTime::from(modified))))
// If created / modified metadata is not available, default to the UNIX epoch.
.unwrap_or_else(|_| {
let epoch = unix_epoch();
(epoch, epoch)
});
let updated = metadata
.modified()
.ok()
.map(DateTime::from);
let contents = {
let mut buf = String::new();
@ -207,12 +202,10 @@ impl Renderer {
drop(fd);
parse_post(
Post::new_from_str(
&self.code_renderer,
*self.config.namespace_uuid,
target.id.clone(),
&target.path.to_string_lossy(),
created,
updated,
&contents
).map_err(|err| Error::Parsing(Box::new(err)))
@ -229,7 +222,7 @@ enum Event {
struct EventTarget {
pub path: PathBuf,
pub id: PostId,
pub id: Id,
}
impl fmt::Debug for EventTarget {
@ -242,7 +235,7 @@ impl EventTarget {
pub fn from_path(path: PathBuf) -> Option<Self> {
path.file_name()
.and_then(|file_name| file_name.to_str())
.and_then(PostId::from_file_name)
.and_then(Id::from_file_name)
.map(|id| Self {
path,
id,

@ -17,7 +17,7 @@ pub async fn handle(
let guard = posts.read().await;
let atom_entries = guard
.iter_by_created()
.iter_by_published()
.take(config.atom.num_posts)
.map(|post| {
atom::EntryBuilder::default()

@ -49,14 +49,14 @@ pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
h2 { "Articles" }
p { "Some recent ones:" }
ul .articles_list {
@for post in posts.read().await.iter_by_created().rev().take(3) {
@for post in posts.read().await.iter_by_published().rev().take(3) {
li {
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
@if let Some(subtitle) = post.subtitle() {
p .article_list_subtitle { (subtitle) }
}
p .article_list_published_date {
"Published " (post.created().format("%Y/%m/%d"))
"Published " (post.published().format("%Y/%m/%d"))
}
}
}

@ -31,7 +31,7 @@ pub async fn handle(
@if let Some(subtitle) = post.subtitle() {
p .article_subtitle { (subtitle) }
}
p .article_published_date { "Published " (post.created().format("%Y/%m/%d")) }
p .article_published_date { "Published " (post.published().format("%Y/%m/%d")) }
}
article .article_content {
(post.html())

@ -23,14 +23,14 @@ pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
"A collection of words I have written, against my better judgement."
}
ul .articles_list {
@for post in posts.read().await.iter_by_created().rev() {
@for post in posts.read().await.iter_by_published().rev() {
li {
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
@if let Some(subtitle) = post.subtitle() {
p .article_list_subtitle { (subtitle) }
}
p .article_list_published_date {
"Published " (post.created().format("%Y/%m/%d"))
"Published " (post.published().format("%Y/%m/%d"))
}
}
}

@ -21,7 +21,7 @@ pub async fn handle(
let (rss_items, updated) = {
let guard = posts.read().await;
let rss_items = guard.iter_by_created()
let rss_items = guard.iter_by_published()
.take(config.rss.num_posts)
.map(|post| {
rss::ItemBuilder::default()
@ -36,7 +36,7 @@ pub async fn handle(
config.self_ref.domain,
post.id()
)))
.pub_date(Some(post.created().to_rfc2822()))
.pub_date(Some(post.published().to_rfc2822()))
.build()
})
.collect::<Vec<rss::Item>>();

@ -8,7 +8,7 @@ use chrono::{DateTime, Utc};
use libshire::strings::ShString22;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::post::{Post, PostId};
use crate::post::{Post, Id};
#[derive(Clone)]
pub struct ConcurrentPostsStore {
@ -40,9 +40,9 @@ impl Default for ConcurrentPostsStore {
}
pub struct PostsStore {
posts: HashMap<PostId, Arc<Post>>,
created_ix: BTreeSet<CreatedIxEntry>,
tags_ix: HashMap<ShString22, HashSet<PostId>>,
posts: HashMap<Id, Arc<Post>>,
published_ix: BTreeSet<PublishedIxEntry>,
tags_ix: HashMap<ShString22, HashSet<Id>>,
}
// TODO: shrink the various collections on removal to deallocate unneeded space
@ -51,7 +51,7 @@ impl PostsStore {
pub fn new() -> Self {
Self {
posts: HashMap::new(),
created_ix: BTreeSet::new(),
published_ix: BTreeSet::new(),
tags_ix: HashMap::new(),
}
}
@ -73,8 +73,8 @@ impl PostsStore {
}.insert(post.id().clone());
}
// Insert the post into the correct position of the created BTree index.
self.created_ix.insert(CreatedIxEntry::new(&post));
// Insert the post into the correct position of the published BTree index.
self.published_ix.insert(PublishedIxEntry::new(&post));
// Wrap the post with an atomic reference counter and insert it into the main posts
// `HashMap`.
@ -86,9 +86,9 @@ impl PostsStore {
pub fn remove(&mut self, id: &str) -> Option<Arc<Post>> {
match self.posts.remove(id) {
Some(post) => {
// Remove the post's entry in the created index.
self.created_ix
.remove(&CreatedIxEntry::new(&post));
// Remove the post's entry in the published index.
self.published_ix
.remove(&PublishedIxEntry::new(&post));
// Remove every occurence of the post from the tags index.
for tag in post.tags() {
@ -105,7 +105,7 @@ impl PostsStore {
pub fn clear(&mut self) {
self.tags_ix.clear();
self.created_ix.clear();
self.published_ix.clear();
self.posts.clear();
}
@ -123,7 +123,7 @@ impl PostsStore {
self.posts.values()
}
pub fn iter_by_created(&self)
pub fn iter_by_published(&self)
-> impl '_
+ Iterator<Item = &Arc<Post>>
+ DoubleEndedIterator
@ -131,13 +131,13 @@ impl PostsStore {
+ FusedIterator
+ Clone
{
// For each entry of the created index, look up the corresponding post in the posts map and
// return the post. Every entry in the created index should contain the ID of a post in the
// posts map, so the `expect` should never fail.
self.created_ix
// For each entry of the published index, look up the corresponding post in the posts map
// and return the post. Every entry in the published index should contain the ID of a post
// in the posts map, so the `expect` should never fail.
self.published_ix
.iter()
.map(|entry| self.get(&entry.id)
.expect("invalid entry in `created_ix` pointing to a post that does not exist"))
.expect("invalid entry in `published_ix` pointing to a post that does not exist"))
}
}
@ -148,15 +148,15 @@ impl Default for PostsStore {
}
#[derive(PartialEq, Eq, PartialOrd, Ord)]
struct CreatedIxEntry {
created: DateTime<Utc>,
id: PostId,
struct PublishedIxEntry {
published: DateTime<Utc>,
id: Id,
}
impl CreatedIxEntry {
impl PublishedIxEntry {
fn new(post: &Post) -> Self {
Self {
created: post.created(),
published: post.published(),
id: post.id().clone(),
}
}

@ -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)
}
}

@ -3,9 +3,9 @@ use std::{borrow, fmt, ops};
use libshire::strings::ShString22;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
pub struct PostId(ShString22);
pub struct Id(ShString22);
impl PostId {
impl Id {
#[inline]
#[must_use]
pub fn from_file_name(file_name: &str) -> Option<Self> {
@ -36,7 +36,7 @@ impl PostId {
}
}
impl ops::Deref for PostId {
impl ops::Deref for Id {
type Target = str;
#[inline]
@ -45,42 +45,42 @@ impl ops::Deref for PostId {
}
}
impl ops::DerefMut for PostId {
impl ops::DerefMut for Id {
#[inline]
fn deref_mut(&mut self) -> &mut Self::Target {
&mut *self.0
}
}
impl AsRef<str> for PostId {
impl AsRef<str> for Id {
#[inline]
fn as_ref(&self) -> &str {
self
}
}
impl AsMut<str> for PostId {
impl AsMut<str> for Id {
#[inline]
fn as_mut(&mut self) -> &mut str {
self
}
}
impl borrow::Borrow<str> for PostId {
impl borrow::Borrow<str> for Id {
#[inline]
fn borrow(&self) -> &str {
self
}
}
impl borrow::BorrowMut<str> for PostId {
impl borrow::BorrowMut<str> for Id {
#[inline]
fn borrow_mut(&mut self) -> &mut str {
self
}
}
impl fmt::Display for PostId {
impl fmt::Display for Id {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&**self, f)

@ -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…
Cancel
Save