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

View File

@ -38,7 +38,6 @@ pulldown-cmark = { version = "0.9" }
reqwest = { version = "0.11", features = ["json"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0" }
serde_yaml = { version = "0.9" }
thiserror = { version = "1.0" }
words-count = { version = "0.1" }

View File

@ -38,7 +38,7 @@ pub fn post_content(_: &PostContentProps) -> Html {
</div>
</div>
</header>
<div class="container mx-auto">
<div class="container mx-auto my-12 px-16 markdown">
{markdown(&post.content)}
</div>
</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 serde::Deserialize;
use yew::{
html,
virtual_dom::{VList, VNode, VTag, VText},
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> {
tokens: I,
output: Vec<VNode>,
@ -54,6 +115,17 @@ where
Tag::BlockQuote => self.stack.push(VTag::new("blockquote")),
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 code = VTag::new("code");
if let CodeBlockKind::Fenced(language) = kind {
@ -86,30 +158,68 @@ where
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);
let mut anchor = VTag::new("a");
anchor.add_attribute("href", href.to_string());
anchor.add_attribute("title", title.to_string());
self.stack.push(anchor);
}
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);
// Note that we do not get an `Event::End` for the image tag.
let mut img = VTag::new("img");
img.add_attribute("loading", "lazy");
img.add_attribute("src", href.to_string());
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) {
let top = self.stack.pop().unwrap_or_else(|| {
panic!("Expected stack to have an element at end of tag: {tag:?}");
});
let element = self
.stack
.pop()
.unwrap_or_else(|| {
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> {
@ -150,15 +260,19 @@ where
fn run(mut self) -> Html {
while let Some(event) = self.tokens.next() {
log::info!("{event:?}");
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());
@ -167,22 +281,30 @@ where
top.add_child(code.into());
}
}
Event::Html(_) => {
log::info!("Ignoring html: {event:?}")
}
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::Rule => {}
Event::TaskListMarker(_) => todo!(),
}
}
// debug_assert!(
// self.stack.is_empty(),
// "Stack is not empty: {:?}",
// self.stack
// );
debug_assert!(
self.stack.is_empty(),
"Stack is not empty: {:?}",
self.stack
);
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 std::collections::HashMap;
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 {
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
.into_iter()
.map(|tag| (tag.slug.clone(), tag))

View File

@ -5,4 +5,27 @@
@apply bg-white text-neutral-800;
@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;
}
}
}