Switch over to WebAssembly, Rust and Yew #35

Merged
BlakeRain merged 87 commits from yew-static into main 2023-08-30 18:01:40 +00:00
6 changed files with 177 additions and 70 deletions
Showing only changes of commit f7e983d583 - Show all commits

50
Cargo.lock generated
View File

@ -187,12 +187,6 @@ dependencies = [
"termcolor", "termcolor",
] ]
[[package]]
name = "equivalent"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]] [[package]]
name = "errno" name = "errno"
version = "0.3.2" version = "0.3.2"
@ -534,7 +528,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"http", "http",
"indexmap 1.9.3", "indexmap",
"slab", "slab",
"tokio", "tokio",
"tokio-util", "tokio-util",
@ -547,12 +541,6 @@ version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.3.2" version = "0.3.2"
@ -661,7 +649,7 @@ version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f" checksum = "7c6ecbd987bb94f1f3c76c6787879756cf4b6f73bfff48d79308e8c56b46f65f"
dependencies = [ dependencies = [
"indexmap 1.9.3", "indexmap",
] ]
[[package]] [[package]]
@ -690,17 +678,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"hashbrown 0.12.3", "hashbrown",
]
[[package]]
name = "indexmap"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.0",
] ]
[[package]] [[package]]
@ -1266,19 +1244,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_yaml"
version = "0.9.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574"
dependencies = [
"indexmap 2.0.0",
"itoa",
"ryu",
"serde",
"unsafe-libyaml",
]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.1" version = "1.4.1"
@ -1301,7 +1266,6 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml",
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
@ -1610,12 +1574,6 @@ version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]]
name = "unsafe-libyaml"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa"
[[package]] [[package]]
name = "url" name = "url"
version = "2.4.0" version = "2.4.0"
@ -1885,7 +1843,7 @@ dependencies = [
"gloo", "gloo",
"html-escape", "html-escape",
"implicit-clone", "implicit-clone",
"indexmap 1.9.3", "indexmap",
"js-sys", "js-sys",
"prokio", "prokio",
"rustversion", "rustversion",

View File

@ -38,7 +38,6 @@ 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" }
serde_yaml = { version = "0.9" }
thiserror = { version = "1.0" } thiserror = { version = "1.0" }
words-count = { version = "0.1" } words-count = { version = "0.1" }

View File

@ -38,7 +38,7 @@ pub fn post_content(_: &PostContentProps) -> Html {
</div> </div>
</div> </div>
</header> </header>
<div class="container mx-auto"> <div class="container mx-auto my-12 px-16 markdown">
{markdown(&post.content)} {markdown(&post.content)}
</div> </div>
</article> </article>

View File

@ -1,11 +1,72 @@
use std::{collections::HashMap, fmt::Write}; use std::{collections::HashMap, fmt::Write, str::FromStr};
use gray_matter::engine::Engine;
use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag}; use pulldown_cmark::{CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
use serde::Deserialize;
use yew::{ use yew::{
html,
virtual_dom::{VList, VNode, VTag, VText}, virtual_dom::{VList, VNode, VTag, VText},
Html, Html,
}; };
#[derive(Deserialize)]
struct BookmarkDecl {
url: String,
title: String,
description: String,
author: String,
publisher: Option<String>,
thumbnail: Option<String>,
icon: Option<String>,
}
impl BookmarkDecl {
fn generate(self) -> VNode {
html! {
<figure>
<a href={self.url}>
<div>
<h1>{self.title}</h1>
</div>
</a>
</figure>
}
}
}
enum GeneratorBlock {
Bookmark(BookmarkDecl),
}
impl GeneratorBlock {
fn new_bookmark(content: String) -> Self {
let decl = gray_matter::engine::YAML::parse(&content)
.deserialize()
.expect("BookmarkDecl");
Self::Bookmark(decl)
}
fn generate(self) -> VNode {
match self {
Self::Bookmark(decl) => decl.generate(),
}
}
}
struct Generator(pub Box<dyn Fn(String) -> GeneratorBlock>);
impl FromStr for Generator {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "bookmark" {
Ok(Generator(Box::new(GeneratorBlock::new_bookmark)))
} else {
Err(())
}
}
}
struct Writer<'a, I> { struct Writer<'a, I> {
tokens: I, tokens: I,
output: Vec<VNode>, output: Vec<VNode>,
@ -54,6 +115,17 @@ where
Tag::BlockQuote => self.stack.push(VTag::new("blockquote")), Tag::BlockQuote => self.stack.push(VTag::new("blockquote")),
Tag::CodeBlock(kind) => { Tag::CodeBlock(kind) => {
if let CodeBlockKind::Fenced(language) = &kind {
if let Ok(generator) = language.parse::<Generator>() {
if let Ok(content) = self.raw_text() {
let block = (generator.0)(content);
let tag = block.generate();
self.stack.push(tag);
return;
}
}
}
let mut pre = VTag::new("pre"); let mut pre = VTag::new("pre");
let mut code = VTag::new("code"); let mut code = VTag::new("code");
if let CodeBlockKind::Fenced(language) = kind { if let CodeBlockKind::Fenced(language) = kind {
@ -86,30 +158,68 @@ where
Tag::Strikethrough => self.stack.push(VTag::new("s")), Tag::Strikethrough => self.stack.push(VTag::new("s")),
Tag::Link(_, href, title) => { Tag::Link(_, href, title) => {
let mut tag = VTag::new("a"); let mut anchor = VTag::new("a");
tag.add_attribute("href", href.to_string()); anchor.add_attribute("href", href.to_string());
tag.add_attribute("title", title.to_string()); anchor.add_attribute("title", title.to_string());
self.stack.push(tag); self.stack.push(anchor);
} }
Tag::Image(_, href, title) => { Tag::Image(_, href, title) => {
let mut tag = VTag::new("img"); // Note that we do not get an `Event::End` for the image tag.
tag.add_attribute("src", href.to_string()); let mut img = VTag::new("img");
tag.add_attribute("title", title.to_string()); img.add_attribute("loading", "lazy");
if let Ok(alt) = self.raw_text() { img.add_attribute("src", href.to_string());
tag.add_attribute("alt", alt); img.add_attribute("title", title.to_string());
let alt = if let Ok(alt) = self.raw_text() {
img.add_attribute("alt", alt.clone());
Some(alt)
} else {
None
};
// If the currently open tag is a <p> tag, then we want to replace it with a
// <figure>. If not, we'll add our own <figure> tag.
let top_p = self
.stack
.last()
.map(|top| top.tag() == "p")
.unwrap_or_default();
if top_p {
self.stack.pop();
} }
self.stack.push(tag);
let mut figure = VTag::new("figure");
figure.add_child(img.into());
if let Some(alt) = alt {
let mut caption = VTag::new("figcaption");
caption.add_child(VText::new(alt).into());
figure.add_child(caption.into());
}
// Put the <figure> onto the stack. This will be popped by the `Event::End` for the
// `<p>` tag that Markdown likes to wrap around images.
self.stack.push(figure);
} }
} }
} }
fn end_tag(&mut self, tag: Tag) { fn end_tag(&mut self, tag: Tag) {
let top = self.stack.pop().unwrap_or_else(|| { let element = self
.stack
.pop()
.unwrap_or_else(|| {
panic!("Expected stack to have an element at end of tag: {tag:?}"); panic!("Expected stack to have an element at end of tag: {tag:?}");
}); })
.into();
self.output.push(top.into()); if let Some(top) = self.stack.last_mut() {
top.add_child(element);
} else {
self.output.push(element);
}
} }
fn raw_text(&mut self) -> Result<String, std::fmt::Error> { fn raw_text(&mut self) -> Result<String, std::fmt::Error> {
@ -150,15 +260,19 @@ where
fn run(mut self) -> Html { fn run(mut self) -> Html {
while let Some(event) = self.tokens.next() { while let Some(event) = self.tokens.next() {
log::info!("{event:?}");
match event { match event {
Event::Start(tag) => self.start_tag(tag), Event::Start(tag) => self.start_tag(tag),
Event::End(tag) => self.end_tag(tag), Event::End(tag) => self.end_tag(tag),
Event::Text(text) => { Event::Text(text) => {
if let Some(top) = self.stack.last_mut() { if let Some(top) = self.stack.last_mut() {
let text = VText::new(text.to_string()); let text = VText::new(text.to_string());
top.add_child(text.into()); top.add_child(text.into());
} }
} }
Event::Code(text) => { Event::Code(text) => {
if let Some(top) = self.stack.last_mut() { if let Some(top) = self.stack.last_mut() {
let text = VText::new(text.to_string()); let text = VText::new(text.to_string());
@ -167,22 +281,30 @@ where
top.add_child(code.into()); top.add_child(code.into());
} }
} }
Event::Html(_) => { Event::Html(_) => {
log::info!("Ignoring html: {event:?}") log::info!("Ignoring html: {event:?}")
} }
Event::FootnoteReference(_) => todo!(), Event::FootnoteReference(_) => todo!(),
Event::SoftBreak => {}
Event::SoftBreak => {
if let Some(top) = self.stack.last_mut() {
let text = VText::new("\n");
top.add_child(text.into());
}
}
Event::HardBreak => {} Event::HardBreak => {}
Event::Rule => {} Event::Rule => {}
Event::TaskListMarker(_) => todo!(), Event::TaskListMarker(_) => todo!(),
} }
} }
// debug_assert!( debug_assert!(
// self.stack.is_empty(), self.stack.is_empty(),
// "Stack is not empty: {:?}", "Stack is not empty: {:?}",
// self.stack self.stack
// ); );
VList::with_children(self.output, None).into() VList::with_children(self.output, None).into()
} }

View File

@ -1,3 +1,4 @@
use gray_matter::engine::Engine;
use include_dir::{include_dir, Dir, File}; 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};
@ -62,7 +63,11 @@ pub fn provide_post(props: &ProvidePostProps) -> Html {
pub fn get_tags() -> TagsContext { pub fn get_tags() -> TagsContext {
let file = CONTENT_DIR.get_file("tags.yaml").expect("tags.yaml"); let file = CONTENT_DIR.get_file("tags.yaml").expect("tags.yaml");
let tags = match serde_yaml::from_slice::<Vec<Tag>>(file.contents()) { let tags = match gray_matter::engine::YAML::parse(
file.contents_utf8().expect("tags.yaml to be utf-8"),
)
.deserialize::<Vec<Tag>>()
{
Ok(tags) => tags Ok(tags) => tags
.into_iter() .into_iter()
.map(|tag| (tag.slug.clone(), tag)) .map(|tag| (tag.slug.clone(), tag))

View File

@ -5,4 +5,27 @@
@apply bg-white text-neutral-800; @apply bg-white text-neutral-800;
@apply dark:bg-zinc-900 dark:text-neutral-200; @apply dark:bg-zinc-900 dark:text-neutral-200;
} }
.markdown {
@apply font-text text-xl;
@apply text-neutral-800 dark:text-neutral-300;
p {
@apply mb-6 leading-relaxed;
}
a {
@apply underline text-blue-500 dark:text-blue-200;
@apply hover:text-blue-600 dark:hover:text-blue-300;
}
figure {
@apply mb-6 flex flex-col items-center;
}
figcaption {
@apply mt-4 font-sans text-sm text-center;
@apply text-neutral-600 dark:text-neutral-500;
}
}
} }