Switch over to WebAssembly, Rust and Yew #35
37
Cargo.lock
generated
37
Cargo.lock
generated
@ -327,6 +327,15 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gimli"
|
name = "gimli"
|
||||||
version = "0.27.3"
|
version = "0.27.3"
|
||||||
@ -1032,6 +1041,18 @@ dependencies = [
|
|||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.9.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77a1a2f1f0a7ecff9c31abbe177637be0e97a0aef46cf8738ece09327985d998"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.33"
|
version = "1.0.33"
|
||||||
@ -1276,6 +1297,7 @@ dependencies = [
|
|||||||
"gray_matter",
|
"gray_matter",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
"log",
|
"log",
|
||||||
|
"pulldown-cmark",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
@ -1546,6 +1568,15 @@ version = "0.2.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89"
|
||||||
|
dependencies = [
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-bidi"
|
name = "unicode-bidi"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
@ -1573,6 +1604,12 @@ dependencies = [
|
|||||||
"tinyvec",
|
"tinyvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unsafe-libyaml"
|
name = "unsafe-libyaml"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
|
@ -34,6 +34,7 @@ yew = { version = "0.20" }
|
|||||||
yew-hooks = { version = "0.2" }
|
yew-hooks = { version = "0.2" }
|
||||||
yew-router = { version = "0.17" }
|
yew-router = { version = "0.17" }
|
||||||
log = { version = "0.4" }
|
log = { version = "0.4" }
|
||||||
|
pulldown-cmark = { version = "0.9" }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
|
@ -1,2 +1,4 @@
|
|||||||
pub mod blog;
|
pub mod blog;
|
||||||
|
pub mod content;
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
|
pub mod markdown;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use time::{format_description::FormatItem, macros::format_description};
|
use time::{format_description::FormatItem, macros::format_description};
|
||||||
use yew::{function_component, html, use_context, Html, Properties};
|
use yew::{classes, function_component, html, use_context, Html, Properties};
|
||||||
use yew_icons::{Icon, IconId};
|
use yew_icons::{Icon, IconId};
|
||||||
use yew_router::prelude::Link;
|
use yew_router::prelude::Link;
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ fn post_card_image(slug: &str, image: &Option<String>) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn post_card_details(info: &PostInfo, tags: &TagsContext) -> Html {
|
pub fn post_card_details(horizontal: bool, info: &PostInfo, tags: &TagsContext) -> Html {
|
||||||
let mut facts =
|
let mut facts =
|
||||||
Intersperse::new(html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> });
|
Intersperse::new(html! { <Icon class="text-gray-500" icon_id={IconId::BootstrapDot} /> });
|
||||||
|
|
||||||
@ -57,7 +57,12 @@ fn post_card_details(info: &PostInfo, tags: &TagsContext) -> Html {
|
|||||||
);
|
);
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="flex flex-col uppercase text-sm">
|
<div class={classes!("flex", "uppercase", "text-sm",
|
||||||
|
if horizontal {
|
||||||
|
"flex-row justify-between"
|
||||||
|
} else {
|
||||||
|
"flex-col"
|
||||||
|
})}>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
{facts.finish()}
|
{facts.finish()}
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +84,7 @@ fn post_card_description(info: &PostInfo, tags: &TagsContext) -> Html {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Link<Route>>
|
</Link<Route>>
|
||||||
{post_card_details(info, tags)}
|
{post_card_details(false, info, tags)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,14 @@ pub fn post_card_list() -> Html {
|
|||||||
let posts = use_context::<PostsContext>().expect("PostsContext to be provided");
|
let posts = use_context::<PostsContext>().expect("PostsContext to be provided");
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-x-10 lg:gap-y-20 my-10">
|
<div class="container mx-auto">
|
||||||
{for posts.iter().enumerate().map(|(index, post)| {
|
<div class="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-x-10 lg:gap-y-20 my-10">
|
||||||
html! {
|
{for posts.iter().enumerate().map(|(index, post)| {
|
||||||
<PostCard post={post.clone()} first={index == 0} />
|
html! {
|
||||||
}
|
<PostCard post={post.clone()} first={index == 0} />
|
||||||
})}
|
}
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
46
src/components/content.rs
Normal file
46
src/components/content.rs
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
use yew::{function_component, html, use_context, Html, Properties};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{blog::post_card::post_card_details, markdown::markdown},
|
||||||
|
model::{source::TagsContext, Post},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct PostContentProps {}
|
||||||
|
|
||||||
|
#[function_component(PostContent)]
|
||||||
|
pub fn post_content(_: &PostContentProps) -> Html {
|
||||||
|
let tags = use_context::<TagsContext>().expect("TagsContext to be provided");
|
||||||
|
let post = use_context::<Post>().expect("Post to be provided");
|
||||||
|
let style = if let Some(cover_image) = &post.info.cover_image {
|
||||||
|
format!(
|
||||||
|
"background-image: linear-gradient(rgba(0, 0, 0, 0.6), rgba(0, 0, 0, 0.6)), url({})",
|
||||||
|
cover_image
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"background-color: #000".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<article>
|
||||||
|
<header class="bg-[50%] bg-no-repeat bg-cover bg-fixed pt-20" {style}>
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<h1 class="text-5xl font-bold text-center text-white">
|
||||||
|
{ &post.info.doc_info.title }
|
||||||
|
</h1>
|
||||||
|
if let Some(excerpt) = &post.info.doc_info.excerpt {
|
||||||
|
<p class="font-text text-2xl text-white text-center mt-5">
|
||||||
|
{ excerpt }
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
<div class="mt-12 pt-8 px-16 bg-white dark:bg-zinc-900 rounded-t">
|
||||||
|
{post_card_details(true, &post.info, &tags)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="container mx-auto">
|
||||||
|
{markdown(&post.content)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
}
|
@ -15,9 +15,7 @@ pub fn layout(props: &LayoutProps) -> Html {
|
|||||||
html! {
|
html! {
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<navigation::Navigation />
|
<navigation::Navigation />
|
||||||
<div class="container mx-auto">
|
{props.children.clone()}
|
||||||
{props.children.clone()}
|
|
||||||
</div>
|
|
||||||
<footer::Footer />
|
<footer::Footer />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
194
src/components/markdown.rs
Normal file
194
src/components/markdown.rs
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
|
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
|
||||||
|
use yew::{
|
||||||
|
virtual_dom::{VList, VNode, VTag, VText},
|
||||||
|
Html,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Writer<'a, I> {
|
||||||
|
tokens: I,
|
||||||
|
output: Vec<VNode>,
|
||||||
|
stack: Vec<VTag>,
|
||||||
|
footnotes: HashMap<CowStr<'a>, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, I> Writer<'a, I>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = Event<'a>>,
|
||||||
|
{
|
||||||
|
fn new(tokens: I) -> Self {
|
||||||
|
Self {
|
||||||
|
tokens,
|
||||||
|
output: Vec::new(),
|
||||||
|
stack: Vec::new(),
|
||||||
|
footnotes: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_tag(&mut self, tag: Tag) {
|
||||||
|
match tag {
|
||||||
|
Tag::Paragraph => self.stack.push(VTag::new("p")),
|
||||||
|
|
||||||
|
Tag::Heading(level, ident, classes) => {
|
||||||
|
let mut tag = VTag::new(match level {
|
||||||
|
HeadingLevel::H1 => "h1",
|
||||||
|
HeadingLevel::H2 => "h2",
|
||||||
|
HeadingLevel::H3 => "h3",
|
||||||
|
HeadingLevel::H4 => "h4",
|
||||||
|
HeadingLevel::H5 => "h5",
|
||||||
|
HeadingLevel::H6 => "h6",
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(ident) = ident {
|
||||||
|
tag.add_attribute("id", ident.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
if !classes.is_empty() {
|
||||||
|
tag.add_attribute("class", classes.join(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stack.push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag::BlockQuote => self.stack.push(VTag::new("blockquote")),
|
||||||
|
|
||||||
|
Tag::CodeBlock(kind) => {
|
||||||
|
let mut pre = VTag::new("pre");
|
||||||
|
let mut code = VTag::new("code");
|
||||||
|
if let CodeBlockKind::Fenced(language) = kind {
|
||||||
|
if !language.is_empty() {
|
||||||
|
code.add_attribute("class", format!("lang-{language}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pre.add_child(code.into());
|
||||||
|
self.stack.push(pre);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag::List(ordered) => {
|
||||||
|
let mut tag = VTag::new(if ordered.is_some() { "ol" } else { "ul" });
|
||||||
|
if let Some(start) = ordered {
|
||||||
|
tag.add_attribute("start", start.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
self.stack.push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag::Item => self.stack.push(VTag::new("li")),
|
||||||
|
Tag::FootnoteDefinition(_) => todo!(),
|
||||||
|
Tag::Table(_) => todo!(),
|
||||||
|
Tag::TableHead => todo!(),
|
||||||
|
Tag::TableRow => todo!(),
|
||||||
|
Tag::TableCell => todo!(),
|
||||||
|
Tag::Emphasis => self.stack.push(VTag::new("em")),
|
||||||
|
Tag::Strong => self.stack.push(VTag::new("strong")),
|
||||||
|
Tag::Strikethrough => self.stack.push(VTag::new("s")),
|
||||||
|
|
||||||
|
Tag::Link(_, href, title) => {
|
||||||
|
let mut tag = VTag::new("a");
|
||||||
|
tag.add_attribute("href", href.to_string());
|
||||||
|
tag.add_attribute("title", title.to_string());
|
||||||
|
self.stack.push(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag::Image(_, href, title) => {
|
||||||
|
let mut tag = VTag::new("img");
|
||||||
|
tag.add_attribute("src", href.to_string());
|
||||||
|
tag.add_attribute("title", title.to_string());
|
||||||
|
if let Ok(alt) = self.raw_text() {
|
||||||
|
tag.add_attribute("alt", alt);
|
||||||
|
}
|
||||||
|
self.stack.push(tag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn end_tag(&mut self, tag: Tag) {
|
||||||
|
let top = self.stack.pop().unwrap_or_else(|| {
|
||||||
|
panic!("Expected stack to have an element at end of tag: {tag:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
self.output.push(top.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw_text(&mut self) -> Result<String, std::fmt::Error> {
|
||||||
|
let mut output = String::new();
|
||||||
|
let mut nest = 0;
|
||||||
|
|
||||||
|
for event in self.tokens.by_ref() {
|
||||||
|
match event {
|
||||||
|
Event::Start(_) => nest += 1,
|
||||||
|
Event::End(tag) => {
|
||||||
|
log::info!("raw_text event.end: {tag:?}");
|
||||||
|
if nest == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
nest -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::Html(text) | Event::Code(text) | Event::Text(text) => {
|
||||||
|
write!(&mut output, "{text}")?
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::SoftBreak | Event::HardBreak | Event::Rule => write!(&mut output, " ")?,
|
||||||
|
|
||||||
|
Event::FootnoteReference(name) => {
|
||||||
|
let next = self.footnotes.len() + 1;
|
||||||
|
let footnote = *self.footnotes.entry(name).or_insert(next);
|
||||||
|
write!(&mut output, "[{footnote}]")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Event::TaskListMarker(true) => write!(&mut output, "[x]")?,
|
||||||
|
Event::TaskListMarker(false) => write!(&mut output, "[ ]")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(mut self) -> Html {
|
||||||
|
while let Some(event) = self.tokens.next() {
|
||||||
|
match event {
|
||||||
|
Event::Start(tag) => self.start_tag(tag),
|
||||||
|
Event::End(tag) => self.end_tag(tag),
|
||||||
|
Event::Text(text) => {
|
||||||
|
if let Some(top) = self.stack.last_mut() {
|
||||||
|
let text = VText::new(text.to_string());
|
||||||
|
top.add_child(text.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Code(text) => {
|
||||||
|
if let Some(top) = self.stack.last_mut() {
|
||||||
|
let text = VText::new(text.to_string());
|
||||||
|
let mut code = VTag::new("code");
|
||||||
|
code.add_child(text.into());
|
||||||
|
top.add_child(code.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::Html(_) => {
|
||||||
|
log::info!("Ignoring html: {event:?}")
|
||||||
|
}
|
||||||
|
Event::FootnoteReference(_) => todo!(),
|
||||||
|
Event::SoftBreak => {}
|
||||||
|
Event::HardBreak => {}
|
||||||
|
Event::Rule => {}
|
||||||
|
Event::TaskListMarker(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// debug_assert!(
|
||||||
|
// self.stack.is_empty(),
|
||||||
|
// "Stack is not empty: {:?}",
|
||||||
|
// self.stack
|
||||||
|
// );
|
||||||
|
|
||||||
|
VList::with_children(self.output, None).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn markdown(content: &str) -> Html {
|
||||||
|
let parser = Parser::new_ext(content, Options::all());
|
||||||
|
Writer::new(parser).run()
|
||||||
|
}
|
@ -2,7 +2,7 @@ use include_dir::{include_dir, Dir, File};
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use yew::{function_component, html, Children, ContextProvider, Html, Properties};
|
use yew::{function_component, html, Children, ContextProvider, Html, Properties};
|
||||||
|
|
||||||
use super::{frontmatter::parse_front_matter, PostInfo, Tag};
|
use super::{frontmatter::parse_front_matter, Post, PostInfo, Tag};
|
||||||
|
|
||||||
pub type TagsContext = HashMap<String, Tag>;
|
pub type TagsContext = HashMap<String, Tag>;
|
||||||
pub type PostsContext = Vec<PostInfo>;
|
pub type PostsContext = Vec<PostInfo>;
|
||||||
@ -24,22 +24,6 @@ pub fn provide_tags(props: &ProvideTagsProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tags() -> TagsContext {
|
|
||||||
let file = CONTENT_DIR.get_file("tags.yaml").expect("tags.yaml");
|
|
||||||
let tags = match serde_yaml::from_slice::<Vec<Tag>>(file.contents()) {
|
|
||||||
Ok(tags) => tags
|
|
||||||
.into_iter()
|
|
||||||
.map(|tag| (tag.slug.clone(), tag))
|
|
||||||
.collect::<TagsContext>(),
|
|
||||||
Err(err) => {
|
|
||||||
panic!("Failed to parse tags.yaml: {err}");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
log::info!("Loaded {} tags", tags.len());
|
|
||||||
tags
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct ProvidePostsProps {
|
pub struct ProvidePostsProps {
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
@ -57,6 +41,41 @@ pub fn provide_posts(props: &ProvidePostsProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Properties, PartialEq)]
|
||||||
|
pub struct ProvidePostProps {
|
||||||
|
pub slug: String,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Children,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(ProvidePost)]
|
||||||
|
pub fn provide_post(props: &ProvidePostProps) -> Html {
|
||||||
|
let post = get_post(&props.slug)
|
||||||
|
.unwrap_or_else(|| panic!("Failed to find post with slug: '{}'", props.slug));
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<ContextProvider<Post> context={post}>
|
||||||
|
{props.children.clone()}
|
||||||
|
</ContextProvider<Post>>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_tags() -> TagsContext {
|
||||||
|
let file = CONTENT_DIR.get_file("tags.yaml").expect("tags.yaml");
|
||||||
|
let tags = match serde_yaml::from_slice::<Vec<Tag>>(file.contents()) {
|
||||||
|
Ok(tags) => tags
|
||||||
|
.into_iter()
|
||||||
|
.map(|tag| (tag.slug.clone(), tag))
|
||||||
|
.collect::<TagsContext>(),
|
||||||
|
Err(err) => {
|
||||||
|
panic!("Failed to parse tags.yaml: {err}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
log::info!("Loaded {} tags", tags.len());
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_posts() -> PostsContext {
|
pub fn get_posts() -> PostsContext {
|
||||||
let files = CONTENT_DIR.get_dir("posts").expect("posts dir").files();
|
let files = CONTENT_DIR.get_dir("posts").expect("posts dir").files();
|
||||||
let mut posts = files.filter_map(load_post_info).collect::<Vec<_>>();
|
let mut posts = files.filter_map(load_post_info).collect::<Vec<_>>();
|
||||||
@ -65,7 +84,16 @@ pub fn get_posts() -> PostsContext {
|
|||||||
posts
|
posts
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_post(slug: &str) -> Option<Post> {
|
||||||
|
let file = CONTENT_DIR.get_file(format!("posts/{slug}.md"))?;
|
||||||
|
load_post(file)
|
||||||
|
}
|
||||||
|
|
||||||
fn load_post_info(file: &File) -> Option<PostInfo> {
|
fn load_post_info(file: &File) -> Option<PostInfo> {
|
||||||
|
load_post(file).map(|post| post.info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_post(file: &File) -> Option<Post> {
|
||||||
let (Some(front_matter), matter) = parse_front_matter(file.contents()) else {
|
let (Some(front_matter), matter) = parse_front_matter(file.contents()) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
@ -80,12 +108,10 @@ fn load_post_info(file: &File) -> Option<PostInfo> {
|
|||||||
|
|
||||||
let reading_time = words_count::count(&matter.content).words / 200;
|
let reading_time = words_count::count(&matter.content).words / 200;
|
||||||
|
|
||||||
Some(PostInfo::from_front_matter(
|
Some(Post {
|
||||||
slug,
|
info: PostInfo::from_front_matter(slug, Some(reading_time), matter.excerpt, front_matter),
|
||||||
Some(reading_time),
|
content: matter.content,
|
||||||
matter.excerpt,
|
})
|
||||||
front_matter,
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static CONTENT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/content");
|
static CONTENT_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/content");
|
||||||
|
@ -15,13 +15,13 @@ pub enum Route {
|
|||||||
About,
|
About,
|
||||||
#[at("/blog")]
|
#[at("/blog")]
|
||||||
Blog,
|
Blog,
|
||||||
#[at("/blog/{slug}")]
|
#[at("/blog/:slug")]
|
||||||
BlogPost { slug: String },
|
BlogPost { slug: String },
|
||||||
#[at("/disclaimer")]
|
#[at("/disclaimer")]
|
||||||
Disclaimer,
|
Disclaimer,
|
||||||
#[at("/tags")]
|
#[at("/tags")]
|
||||||
Tags,
|
Tags,
|
||||||
#[at("/tags/{slug}")]
|
#[at("/tags/:slug")]
|
||||||
Tag { slug: String },
|
Tag { slug: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
use yew::{function_component, html, Html, Properties};
|
use yew::{function_component, html, Html, Properties};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::content::PostContent,
|
||||||
|
model::source::{ProvidePost, ProvideTags},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
pub struct PageProps {
|
pub struct PageProps {
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
@ -7,5 +12,11 @@ pub struct PageProps {
|
|||||||
|
|
||||||
#[function_component(Page)]
|
#[function_component(Page)]
|
||||||
pub fn page(props: &PageProps) -> Html {
|
pub fn page(props: &PageProps) -> Html {
|
||||||
html! { <h1>{format!("Blog post '{}'", props.slug)}</h1> }
|
html! {
|
||||||
|
<ProvideTags>
|
||||||
|
<ProvidePost slug={props.slug.clone()}>
|
||||||
|
<PostContent />
|
||||||
|
</ProvidePost>
|
||||||
|
</ProvideTags>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user