More modular rendering of posts, post publish time now lives in the header
This commit is contained in:
1 parent
14726ca7d4
commit
341ccde5d5
21 files changed
+562
-326
No files matched your search
Generated
+28
@@ -164,6 +164,7 @@ dependencies = [
|
|||||||
"atom_syndication",
|
"atom_syndication",
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"kdl",
|
||||||
"knuffel",
|
"knuffel",
|
||||||
"libshire",
|
"libshire",
|
||||||
"maud",
|
"maud",
|
||||||
@@ -584,6 +585,17 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
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]]
|
[[package]]
|
||||||
name = "kernel32-sys"
|
name = "kernel32-sys"
|
||||||
version = "0.2.2"
|
version = "0.2.2"
|
||||||
@@ -764,6 +776,12 @@ dependencies = [
|
|||||||
"unicase",
|
"unicase",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "minimal-lexical"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -845,6 +863,16 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c96aba5aa877601bb3f6dd6a63a969e1f82e60646e81e71b14496995e9853c91"
|
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]]
|
[[package]]
|
||||||
name = "notify"
|
name = "notify"
|
||||||
version = "4.0.17"
|
version = "4.0.17"
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ mime = "0.3"
|
|||||||
maud = "0.23"
|
maud = "0.23"
|
||||||
atom_syndication = "0.11"
|
atom_syndication = "0.11"
|
||||||
rss = "2"
|
rss = "2"
|
||||||
|
kdl = "4"
|
||||||
knuffel = "2"
|
knuffel = "2"
|
||||||
pulldown-cmark = "0.9"
|
pulldown-cmark = "0.9"
|
||||||
syntect = "4"
|
syntect = "4"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod config;
|
||||||
mod fs_watcher;
|
mod fs_watcher;
|
||||||
mod render;
|
mod render;
|
||||||
mod service;
|
mod service;
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ use tracing::{info, warn, error};
|
|||||||
|
|
||||||
use blog::{
|
use blog::{
|
||||||
codeblock::CodeBlockRenderer,
|
codeblock::CodeBlockRenderer,
|
||||||
post::{parse as parse_post, ParseError, Post, PostId},
|
post::{Error as ParseError, Post, Id},
|
||||||
db::ConcurrentPostsStore,
|
db::ConcurrentPostsStore,
|
||||||
time::unix_epoch,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Config;
|
use crate::Config;
|
||||||
@@ -189,14 +188,10 @@ impl Renderer {
|
|||||||
return Err(Error::NotAFile);
|
return Err(Error::NotAFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
let (created, updated) = metadata.created()
|
let updated = metadata
|
||||||
.and_then(|created| metadata.modified()
|
.modified()
|
||||||
.map(|modified| (DateTime::from(created), DateTime::from(modified))))
|
.ok()
|
||||||
// If created / modified metadata is not available, default to the UNIX epoch.
|
.map(DateTime::from);
|
||||||
.unwrap_or_else(|_| {
|
|
||||||
let epoch = unix_epoch();
|
|
||||||
(epoch, epoch)
|
|
||||||
});
|
|
||||||
|
|
||||||
let contents = {
|
let contents = {
|
||||||
let mut buf = String::new();
|
let mut buf = String::new();
|
||||||
@@ -207,12 +202,10 @@ impl Renderer {
|
|||||||
|
|
||||||
drop(fd);
|
drop(fd);
|
||||||
|
|
||||||
parse_post(
|
Post::new_from_str(
|
||||||
&self.code_renderer,
|
&self.code_renderer,
|
||||||
*self.config.namespace_uuid,
|
*self.config.namespace_uuid,
|
||||||
target.id.clone(),
|
target.id.clone(),
|
||||||
&target.path.to_string_lossy(),
|
|
||||||
created,
|
|
||||||
updated,
|
updated,
|
||||||
&contents
|
&contents
|
||||||
).map_err(|err| Error::Parsing(Box::new(err)))
|
).map_err(|err| Error::Parsing(Box::new(err)))
|
||||||
@@ -229,7 +222,7 @@ enum Event {
|
|||||||
|
|
||||||
struct EventTarget {
|
struct EventTarget {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
pub id: PostId,
|
pub id: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Debug for EventTarget {
|
impl fmt::Debug for EventTarget {
|
||||||
@@ -242,7 +235,7 @@ impl EventTarget {
|
|||||||
pub fn from_path(path: PathBuf) -> Option<Self> {
|
pub fn from_path(path: PathBuf) -> Option<Self> {
|
||||||
path.file_name()
|
path.file_name()
|
||||||
.and_then(|file_name| file_name.to_str())
|
.and_then(|file_name| file_name.to_str())
|
||||||
.and_then(PostId::from_file_name)
|
.and_then(Id::from_file_name)
|
||||||
.map(|id| Self {
|
.map(|id| Self {
|
||||||
path,
|
path,
|
||||||
id,
|
id,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ pub async fn handle(
|
|||||||
let guard = posts.read().await;
|
let guard = posts.read().await;
|
||||||
|
|
||||||
let atom_entries = guard
|
let atom_entries = guard
|
||||||
.iter_by_created()
|
.iter_by_published()
|
||||||
.take(config.atom.num_posts)
|
.take(config.atom.num_posts)
|
||||||
.map(|post| {
|
.map(|post| {
|
||||||
atom::EntryBuilder::default()
|
atom::EntryBuilder::default()
|
||||||
|
|||||||
@@ -49,14 +49,14 @@ pub async fn handle(Extension(posts): Extension<ConcurrentPostsStore>) -> Html {
|
|||||||
h2 { "Articles" }
|
h2 { "Articles" }
|
||||||
p { "Some recent ones:" }
|
p { "Some recent ones:" }
|
||||||
ul .articles_list {
|
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 {
|
li {
|
||||||
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
|
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
|
||||||
@if let Some(subtitle) = post.subtitle() {
|
@if let Some(subtitle) = post.subtitle() {
|
||||||
p .article_list_subtitle { (subtitle) }
|
p .article_list_subtitle { (subtitle) }
|
||||||
}
|
}
|
||||||
p .article_list_published_date {
|
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() {
|
@if let Some(subtitle) = post.subtitle() {
|
||||||
p .article_subtitle { (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 {
|
article .article_content {
|
||||||
(post.html())
|
(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."
|
"A collection of words I have written, against my better judgement."
|
||||||
}
|
}
|
||||||
ul .articles_list {
|
ul .articles_list {
|
||||||
@for post in posts.read().await.iter_by_created().rev() {
|
@for post in posts.read().await.iter_by_published().rev() {
|
||||||
li {
|
li {
|
||||||
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
|
h3 { a href={"/articles/" (post.id())} { (post.title()) } }
|
||||||
@if let Some(subtitle) = post.subtitle() {
|
@if let Some(subtitle) = post.subtitle() {
|
||||||
p .article_list_subtitle { (subtitle) }
|
p .article_list_subtitle { (subtitle) }
|
||||||
}
|
}
|
||||||
p .article_list_published_date {
|
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 (rss_items, updated) = {
|
||||||
let guard = posts.read().await;
|
let guard = posts.read().await;
|
||||||
|
|
||||||
let rss_items = guard.iter_by_created()
|
let rss_items = guard.iter_by_published()
|
||||||
.take(config.rss.num_posts)
|
.take(config.rss.num_posts)
|
||||||
.map(|post| {
|
.map(|post| {
|
||||||
rss::ItemBuilder::default()
|
rss::ItemBuilder::default()
|
||||||
@@ -36,7 +36,7 @@ pub async fn handle(
|
|||||||
config.self_ref.domain,
|
config.self_ref.domain,
|
||||||
post.id()
|
post.id()
|
||||||
)))
|
)))
|
||||||
.pub_date(Some(post.created().to_rfc2822()))
|
.pub_date(Some(post.published().to_rfc2822()))
|
||||||
.build()
|
.build()
|
||||||
})
|
})
|
||||||
.collect::<Vec<rss::Item>>();
|
.collect::<Vec<rss::Item>>();
|
||||||
|
|||||||
+22
-22
@@ -8,7 +8,7 @@ use chrono::{DateTime, Utc};
|
|||||||
use libshire::strings::ShString22;
|
use libshire::strings::ShString22;
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||||
|
|
||||||
use crate::post::{Post, PostId};
|
use crate::post::{Post, Id};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConcurrentPostsStore {
|
pub struct ConcurrentPostsStore {
|
||||||
@@ -40,9 +40,9 @@ impl Default for ConcurrentPostsStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct PostsStore {
|
pub struct PostsStore {
|
||||||
posts: HashMap<PostId, Arc<Post>>,
|
posts: HashMap<Id, Arc<Post>>,
|
||||||
created_ix: BTreeSet<CreatedIxEntry>,
|
published_ix: BTreeSet<PublishedIxEntry>,
|
||||||
tags_ix: HashMap<ShString22, HashSet<PostId>>,
|
tags_ix: HashMap<ShString22, HashSet<Id>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: shrink the various collections on removal to deallocate unneeded space
|
// TODO: shrink the various collections on removal to deallocate unneeded space
|
||||||
@@ -51,7 +51,7 @@ impl PostsStore {
|
|||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
posts: HashMap::new(),
|
posts: HashMap::new(),
|
||||||
created_ix: BTreeSet::new(),
|
published_ix: BTreeSet::new(),
|
||||||
tags_ix: HashMap::new(),
|
tags_ix: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -73,8 +73,8 @@ impl PostsStore {
|
|||||||
}.insert(post.id().clone());
|
}.insert(post.id().clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert the post into the correct position of the created BTree index.
|
// Insert the post into the correct position of the published BTree index.
|
||||||
self.created_ix.insert(CreatedIxEntry::new(&post));
|
self.published_ix.insert(PublishedIxEntry::new(&post));
|
||||||
|
|
||||||
// Wrap the post with an atomic reference counter and insert it into the main posts
|
// Wrap the post with an atomic reference counter and insert it into the main posts
|
||||||
// `HashMap`.
|
// `HashMap`.
|
||||||
@@ -86,9 +86,9 @@ impl PostsStore {
|
|||||||
pub fn remove(&mut self, id: &str) -> Option<Arc<Post>> {
|
pub fn remove(&mut self, id: &str) -> Option<Arc<Post>> {
|
||||||
match self.posts.remove(id) {
|
match self.posts.remove(id) {
|
||||||
Some(post) => {
|
Some(post) => {
|
||||||
// Remove the post's entry in the created index.
|
// Remove the post's entry in the published index.
|
||||||
self.created_ix
|
self.published_ix
|
||||||
.remove(&CreatedIxEntry::new(&post));
|
.remove(&PublishedIxEntry::new(&post));
|
||||||
|
|
||||||
// Remove every occurence of the post from the tags index.
|
// Remove every occurence of the post from the tags index.
|
||||||
for tag in post.tags() {
|
for tag in post.tags() {
|
||||||
@@ -105,7 +105,7 @@ impl PostsStore {
|
|||||||
|
|
||||||
pub fn clear(&mut self) {
|
pub fn clear(&mut self) {
|
||||||
self.tags_ix.clear();
|
self.tags_ix.clear();
|
||||||
self.created_ix.clear();
|
self.published_ix.clear();
|
||||||
self.posts.clear();
|
self.posts.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ impl PostsStore {
|
|||||||
self.posts.values()
|
self.posts.values()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn iter_by_created(&self)
|
pub fn iter_by_published(&self)
|
||||||
-> impl '_
|
-> impl '_
|
||||||
+ Iterator<Item = &Arc<Post>>
|
+ Iterator<Item = &Arc<Post>>
|
||||||
+ DoubleEndedIterator
|
+ DoubleEndedIterator
|
||||||
@@ -131,13 +131,13 @@ impl PostsStore {
|
|||||||
+ FusedIterator
|
+ FusedIterator
|
||||||
+ Clone
|
+ Clone
|
||||||
{
|
{
|
||||||
// For each entry of the created index, look up the corresponding post in the posts map and
|
// For each entry of the published index, look up the corresponding post in the posts map
|
||||||
// return the post. Every entry in the created index should contain the ID of a post in the
|
// and return the post. Every entry in the published index should contain the ID of a post
|
||||||
// posts map, so the `expect` should never fail.
|
// in the posts map, so the `expect` should never fail.
|
||||||
self.created_ix
|
self.published_ix
|
||||||
.iter()
|
.iter()
|
||||||
.map(|entry| self.get(&entry.id)
|
.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)]
|
#[derive(PartialEq, Eq, PartialOrd, Ord)]
|
||||||
struct CreatedIxEntry {
|
struct PublishedIxEntry {
|
||||||
created: DateTime<Utc>,
|
published: DateTime<Utc>,
|
||||||
id: PostId,
|
id: Id,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CreatedIxEntry {
|
impl PublishedIxEntry {
|
||||||
fn new(post: &Post) -> Self {
|
fn new(post: &Post) -> Self {
|
||||||
Self {
|
Self {
|
||||||
created: post.created(),
|
published: post.published(),
|
||||||
id: post.id().clone(),
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-9
@@ -3,9 +3,9 @@ use std::{borrow, fmt, ops};
|
|||||||
use libshire::strings::ShString22;
|
use libshire::strings::ShString22;
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
|
||||||
pub struct PostId(ShString22);
|
pub struct Id(ShString22);
|
||||||
|
|
||||||
impl PostId {
|
impl Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn from_file_name(file_name: &str) -> Option<Self> {
|
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;
|
type Target = str;
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
@@ -45,42 +45,42 @@ impl ops::Deref for PostId {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ops::DerefMut for PostId {
|
impl ops::DerefMut for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
&mut *self.0
|
&mut *self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsRef<str> for PostId {
|
impl AsRef<str> for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn as_ref(&self) -> &str {
|
fn as_ref(&self) -> &str {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsMut<str> for PostId {
|
impl AsMut<str> for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn as_mut(&mut self) -> &mut str {
|
fn as_mut(&mut self) -> &mut str {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl borrow::Borrow<str> for PostId {
|
impl borrow::Borrow<str> for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn borrow(&self) -> &str {
|
fn borrow(&self) -> &str {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl borrow::BorrowMut<str> for PostId {
|
impl borrow::BorrowMut<str> for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn borrow_mut(&mut self) -> &mut str {
|
fn borrow_mut(&mut self) -> &mut str {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for PostId {
|
impl fmt::Display for Id {
|
||||||
#[inline]
|
#[inline]
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt::Display::fmt(&**self, f)
|
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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-56
@@ -1,61 +1,18 @@
|
|||||||
|
mod error;
|
||||||
mod id;
|
mod id;
|
||||||
mod parse;
|
mod header;
|
||||||
|
mod markdown_post;
|
||||||
|
mod render;
|
||||||
|
mod rendered_post;
|
||||||
|
mod source;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
pub use error::Error;
|
||||||
use libshire::{strings::ShString22, uuid::Uuid};
|
pub use header::Header;
|
||||||
use maud::{Markup, PreEscaped};
|
pub use id::Id;
|
||||||
|
pub use markdown_post::MarkdownPost;
|
||||||
pub use id::PostId;
|
pub use rendered_post::RenderedPost;
|
||||||
pub use parse::{parse, ParseError};
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-1
@@ -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)
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user