Switch over to WebAssembly, Rust and Yew #35
50
Cargo.lock
generated
50
Cargo.lock
generated
@ -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",
|
||||||
|
@ -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" }
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user