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
5 changed files with 0 additions and 595 deletions
Showing only changes of commit 87823f8375 - Show all commits

View File

@ -1,6 +1,3 @@
pub mod blog;
// pub mod content;
pub mod content;
pub mod document;
pub mod layout;
// pub mod markdown;

View File

@ -1,2 +0,0 @@
pub mod bookmark;
pub mod quote;

View File

@ -1,59 +0,0 @@
use yew::{function_component, html, Html, Properties};
use yew_icons::{Icon, IconId};
#[derive(Properties, PartialEq)]
pub struct BookmarkProps {
pub url: String,
pub title: String,
pub author: Option<String>,
pub description: Option<String>,
pub publisher: Option<String>,
pub thumbnail: Option<String>,
pub icon: Option<String>,
}
#[function_component(Bookmark)]
pub fn bookmark(props: &BookmarkProps) -> Html {
html! {
<figure class="w-full text-base">
<a href={props.url.clone()}
class="plain w-full flex flex-col lg:flex-row rounded-md shadow-md min-h-[148px] border border-neutral-300 dark:border-neutral-700">
if let Some(thumbnail) = &props.thumbnail {
<div class="relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]">
<img
class="absolute top-0 left-0 w-full h-full rounded-r-md object-cover"
src={thumbnail.clone()}
alt={props.title.clone()}
loading="lazy"
decoding="async" />
</div>
}
<div class="font-sans ld:order-1 grow flex flex-col justify-start align-start p-5">
<div class="font-semibold">{props.title.clone()}</div>
if let Some(description) = &props.description {
<div class="grow overflow-y-hidden mt-3 max-h-12">{description.clone()}</div>
}
<div class="flex flex-row flex-wrap align-center gap-1 mt-3.5">
if let Some(icon) = &props.icon {
<img
class="w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-2"
alt={props.publisher.clone()}
src={icon.clone()} />
}
if let Some(publisher) = &props.publisher {
<span>{publisher}</span>
if props.author.is_some() {
<Icon icon_id={IconId::BootstrapDot} />
}
}
if let Some(author) = &props.author {
<span>{author.clone()}</span>
}
</div>
</div>
</a>
</figure>
}
}

View File

@ -1,33 +0,0 @@
use yew::{function_component, html, Children, Html, Properties};
#[derive(Properties, PartialEq)]
pub struct QuoteProps {
pub author: Option<String>,
pub url: Option<String>,
#[prop_or_default]
pub children: Children,
}
#[function_component(Quote)]
pub fn quote(props: &QuoteProps) -> Html {
let cite = props.author.clone().map(|author| {
props
.url
.clone()
.map(|url| {
html! {
<cite>
<a href={url} target="_blank" rel="noreferrer">{author.clone()}</a>
</cite>
}
})
.unwrap_or_else(|| html! { <cite>{author.clone()}</cite> })
});
html! {
<figure class="quote">
{props.children.clone()}
{cite}
</figure>
}
}

View File

@ -1,498 +0,0 @@
use std::{collections::HashMap, fmt::Write, str::FromStr};
use gray_matter::engine::Engine;
use pulldown_cmark::{Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
use serde::Deserialize;
use yew::{
html,
virtual_dom::{VList, VNode, VTag, VText},
Html,
};
use yew_icons::{Icon, IconId};
use crate::model::properties::{Properties, PropertiesParseError};
fn parse_language_properties(input: &str) -> Result<(String, Properties), PropertiesParseError> {
let input = input.trim();
if let Some((language, rest)) = input.split_once(' ') {
let rest = rest.trim();
let properties = if rest.is_empty() {
Properties::default()
} else {
Properties::from_str(rest)?
};
Ok((language.to_string(), properties))
} else {
Ok((input.to_string(), Properties::default()))
}
}
#[derive(Deserialize)]
struct BookmarkDecl {
url: String,
title: String,
description: Option<String>,
author: String,
publisher: Option<String>,
thumbnail: Option<String>,
icon: Option<String>,
}
impl BookmarkDecl {
fn generate(self) -> VNode {
html! {
<figure class="w-full text-base">
<a href={self.url}
class="plain w-full flex flex-col lg:flex-row rounded-md shadow-md min-h-[148px] border border-neutral-300 dark:border-neutral-700">
if let Some(thumbnail) = self.thumbnail {
<div class="relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]">
<img
class="absolute top-0 left-0 w-full h-full rounded-r-md object-cover"
src={thumbnail}
alt={self.title.clone()}
loading="lazy"
decoding="async" />
</div>
}
<div class="font-sans ld:order-1 grow flex flex-col justify-start align-start p-5">
<div class="font-semibold">{self.title}</div>
if let Some(description) = self.description {
<div class="grow overflow-y-hidden mt-3 max-h-12">{description}</div>
}
<div class="flex flex-row flex-wrap align-center gap-1 mt-3.5">
if let Some(icon) = self.icon {
<img
class="w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-2"
alt={self.publisher.clone()}
src={icon} />
}
if let Some(publisher) = self.publisher {
<span>{publisher}</span>
if !self.author.is_empty() {
<Icon icon_id={IconId::BootstrapDot} />
}
}
if !self.author.is_empty() {
<span>{self.author}</span>
}
</div>
</div>
</a>
</figure>
}
}
}
#[derive(Deserialize)]
struct QuoteDecl {
quote: String,
author: Option<String>,
url: Option<String>,
}
impl QuoteDecl {
fn generate(self) -> VNode {
let cite = self.author.map(|author| {
self.url
.map(|url| {
html! {
<cite>
<a href={url} target="_blank" rel="noreferrer">{author.clone()}</a>
</cite>
}
})
.unwrap_or_else(|| html! { <cite>{author.clone()}</cite> })
});
html! {
<figure class="quote">
<p>{self.quote}</p>
{cite}
</figure>
}
}
}
enum GeneratorBlock {
Bookmark(BookmarkDecl),
Quote(QuoteDecl),
}
impl GeneratorBlock {
fn new_bookmark(content: String) -> Self {
let decl = gray_matter::engine::YAML::parse(&content)
.deserialize()
.expect("BookmarkDecl");
Self::Bookmark(decl)
}
fn new_quote(content: String) -> Self {
let decl = gray_matter::engine::YAML::parse(&content)
.deserialize()
.expect("QuoteDecl");
Self::Quote(decl)
}
fn generate(self) -> VNode {
match self {
Self::Bookmark(decl) => decl.generate(),
Self::Quote(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 if s == "quote" {
Ok(Generator(Box::new(GeneratorBlock::new_quote)))
} else {
Err(())
}
}
}
struct Writer<'a, I> {
tokens: I,
output: Vec<VNode>,
stack: Vec<VNode>,
footnotes: HashMap<CowStr<'a>, usize>,
table_align: Vec<Alignment>,
table_head: bool,
table_colidx: 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(),
table_align: Vec::new(),
table_head: false,
table_colidx: 0,
}
}
fn pop(&mut self) -> VNode {
self.stack.pop().unwrap_or_else(|| {
panic!("Stack underflow");
})
}
fn output(&mut self, node: VNode) {
if let Some(VNode::VTag(top)) = self.stack.last_mut() {
top.add_child(node);
} else {
self.output.push(node);
}
}
fn start_tag(&mut self, tag: Tag) {
match tag {
Tag::Paragraph => self.stack.push(VTag::new("p").into()),
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.into());
}
Tag::BlockQuote => self.stack.push(VTag::new("blockquote").into()),
Tag::CodeBlock(kind) => {
if let CodeBlockKind::Fenced(language) = &kind {
if let Ok(generator) = language.parse::<Generator>() {
if let Ok(content) = self.raw_text() {
self.output((generator.0)(content).generate());
return;
}
}
}
if let Ok(content) = self.raw_text() {
let mut figure = VTag::new("figure");
figure.add_attribute("class", "code");
let mut pre = VTag::new("pre");
let mut code = VTag::new("code");
let properties = if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
let (language, properties) = parse_language_properties(&language)
.expect("valid language and properties");
code.add_attribute("class", format!("lang-{language}"));
Some(properties)
} else {
None
}
} else {
None
}
.unwrap_or_default();
code.add_child(VText::new(content).into());
pre.add_child(code.into());
figure.add_child(pre.into());
if properties.has("caption") {
let mut figcap = VTag::new("figcaption");
figcap.add_child(
VText::new(
properties
.get("caption")
.map(ToString::to_string)
.unwrap_or_default(),
)
.into(),
);
figure.add_child(figcap.into());
}
self.output(figure.into());
} else {
self.stack.push(VTag::new("pre").into());
}
}
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.into());
}
Tag::Item => self.stack.push(VTag::new("li").into()),
Tag::FootnoteDefinition(_) => todo!(),
Tag::Table(align) => {
self.table_align = align;
self.stack.push(VTag::new("table").into());
}
Tag::TableHead => {
self.table_head = true;
self.stack.push(VTag::new("thead").into());
self.stack.push(VTag::new("tr").into());
}
Tag::TableRow => {
self.table_colidx = 0;
self.stack.push(VTag::new("tr").into());
}
Tag::TableCell => {
let mut cell = if self.table_head {
VTag::new("th")
} else {
VTag::new("td")
};
match self.table_align.get(self.table_colidx) {
Some(&Alignment::Left) => {
cell.add_attribute("class", "left");
}
Some(&Alignment::Right) => {
cell.add_attribute("class", "right");
}
Some(&Alignment::Center) => cell.add_attribute("class", "center"),
_ => {}
}
self.stack.push(cell.into());
}
Tag::Emphasis => self.stack.push(VTag::new("em").into()),
Tag::Strong => self.stack.push(VTag::new("strong").into()),
Tag::Strikethrough => self.stack.push(VTag::new("s").into()),
Tag::Link(_, href, title) => {
let mut anchor = VTag::new("a");
anchor.add_attribute("href", href.to_string());
anchor.add_attribute("title", title.to_string());
self.stack.push(anchor.into());
}
Tag::Image(_, href, title) => {
// 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| match top {
VNode::VTag(tag) => tag.tag() == "p",
_ => false,
})
.unwrap_or_default();
if top_p {
self.stack.pop();
}
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.into());
}
}
}
fn end_tag(&mut self, tag: Tag) {
match tag {
Tag::Table(_) => {
// Pop the <tbody> and then the <table>
let tbody = self.pop();
self.output(tbody);
let table = self.pop();
self.output(table);
}
Tag::TableHead => {
// Pop the <tr>, the <thead>, and then enter the <tbody>.
let row = self.pop();
self.output(row);
let thead = self.pop();
self.output(thead);
self.table_head = false;
self.stack.push(VTag::new("tbody").into());
}
Tag::TableCell => {
let cell = self.pop();
self.output(cell);
self.table_colidx += 1;
}
_ => {
let element = self.pop();
self.output(element);
}
}
}
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(_) => {
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) => self.output(VText::new(text.to_string()).into()),
Event::Code(text) => {
let text = VText::new(text.to_string());
let mut code = VTag::new("code");
code.add_child(text.into());
self.output(code.into());
}
Event::Html(_) => {
log::info!("Ignoring html: {event:?}")
}
Event::FootnoteReference(_) => todo!(),
Event::SoftBreak => self.output(VText::new("\n").into()),
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()
}