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
10 changed files with 1172 additions and 22 deletions
Showing only changes of commit 265f299473 - Show all commits

94
Cargo.lock generated
View File

@ -43,6 +43,15 @@ dependencies = [
"syn 2.0.29",
]
[[package]]
name = "atomic-polyfill"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -109,6 +118,12 @@ version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "bytes"
version = "1.4.0"
@ -130,6 +145,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cobs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@ -165,6 +186,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]]
name = "deranged"
version = "0.3.8"
@ -574,12 +601,35 @@ dependencies = [
"tracing",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heapless"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "hermit-abi"
version = "0.3.2"
@ -863,6 +913,9 @@ dependencies = [
name = "model"
version = "2.0.0"
dependencies = [
"postcard",
"proc-macro2",
"quote",
"serde",
"thiserror",
"time",
@ -1088,6 +1141,17 @@ dependencies = [
"time",
]
[[package]]
name = "postcard"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
dependencies = [
"cobs",
"heapless",
"serde",
]
[[package]]
name = "prettyplease"
version = "0.1.25"
@ -1265,6 +1329,15 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.8"
@ -1343,6 +1416,12 @@ dependencies = [
"libc",
]
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.171"
@ -1468,6 +1547,21 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "1.0.109"

110
macros/Cargo.lock generated
View File

@ -8,6 +8,15 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "atomic-polyfill"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.1.0"
@ -35,6 +44,12 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cc"
version = "1.0.83"
@ -50,6 +65,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cobs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
[[package]]
name = "crc32fast"
version = "1.3.2"
@ -59,6 +80,12 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]]
name = "deranged"
version = "0.3.8"
@ -104,12 +131,35 @@ dependencies = [
"yaml-rust",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "heapless"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "indexmap"
version = "1.9.3"
@ -153,6 +203,16 @@ version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "macros"
version = "0.1.0"
@ -198,6 +258,9 @@ dependencies = [
name = "model"
version = "2.0.0"
dependencies = [
"postcard",
"proc-macro2",
"quote",
"serde",
"thiserror",
"time",
@ -261,6 +324,17 @@ dependencies = [
"time",
]
[[package]]
name = "postcard"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
dependencies = [
"cobs",
"heapless",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
@ -306,6 +380,15 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "ryu"
version = "1.0.15"
@ -327,6 +410,18 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.188"
@ -358,6 +453,21 @@ dependencies = [
"serde",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.29"

View File

@ -1,8 +1,7 @@
use std::path::PathBuf;
use model::document::{Details, Document};
use model::document::{encode_nodes, Details, Document};
use proc_macro::TokenStream;
use pulldown_cmark::{Options, Parser};
use quote::{quote, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
@ -15,10 +14,8 @@ use crate::{
slug::{slug_constr, slug_ident},
};
use self::writer::Writer;
mod highlight;
mod writer;
mod markdown;
fn load_documents(directory: &str) -> Result<Vec<Document<String>>, Error> {
let mut root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@ -144,20 +141,13 @@ fn generate_document(generator: &mut Generator, document: Document<String>) -> R
let render_ident =
parse_str::<Ident>(&format!("render_{ident}")).expect("document render identifier");
let html = {
let mut html = String::new();
Writer::new(
Parser::new_ext(&document.content, Options::all()),
&mut html,
)
.run()
.expect("HTML");
html
};
let html = markdown::render(&document.content);
let html = encode_nodes(html);
let html = proc_macro2::Literal::byte_string(&html);
generator.render_funcs.append_all(quote! {
fn #render_ident() -> yew::Html {
yew::Html::from_html_unchecked(yew::AttrValue::from(#html))
fn #render_ident() -> Vec<RenderNode> {
decode_nodes(#html)
}
});
@ -233,7 +223,7 @@ pub fn generate(input: DocumentsInput) -> Result<TokenStream, Error> {
#render_funcs
pub fn render(ident: DocId) -> Option<(Details<DocId>, yew::Html)> {
pub fn render(ident: DocId) -> Option<(Details<DocId>, Vec<RenderNode>)> {
match ident {
#render_matches
_ => None

View File

@ -0,0 +1,616 @@
use std::collections::HashMap;
use gray_matter::engine::Engine;
use model::{
document::{AttributeName, RenderElement, RenderNode, RenderText, TagName},
properties::Properties,
};
use pulldown_cmark::{Alignment, CodeBlockKind, CowStr, Event, HeadingLevel, Options, Parser, Tag};
use serde::Deserialize;
use syntect::{
easy::HighlightLines,
highlighting::{Color, FontStyle, Style},
util::LinesWithEndings,
};
use crate::parse::properties::{parse_language, parse_language_properties};
use super::highlight::{SYNTAX_SET, THEME_SET};
pub fn heading_for_level(level: HeadingLevel) -> TagName {
match level {
HeadingLevel::H1 => TagName::H1,
HeadingLevel::H2 => TagName::H2,
HeadingLevel::H3 => TagName::H3,
HeadingLevel::H4 => TagName::H4,
HeadingLevel::H5 => TagName::H5,
HeadingLevel::H6 => TagName::H6,
}
}
#[derive(Deserialize)]
struct Bookmark {
url: String,
title: String,
description: Option<String>,
author: Option<String>,
publisher: Option<String>,
thumbnail: Option<String>,
icon: Option<String>,
}
#[derive(Deserialize)]
pub struct Quote {
pub quote: String,
pub author: Option<String>,
pub url: Option<String>,
}
struct Highlighting {
language: String,
content: String,
}
impl Highlighting {
pub fn finish(self) -> RenderElement {
let syntax = SYNTAX_SET
.find_syntax_by_token(&self.language)
.unwrap_or_else(|| panic!("Unknown language: {}", self.language));
let theme = THEME_SET.themes.get("base16-ocean.dark").expect("theme");
let bg = theme.settings.background.unwrap_or(Color::WHITE);
let mut pre = RenderElement::new(TagName::Pre);
pre.add_attribute(
AttributeName::Style,
format!("background-color: #{:02x}{:02x}{:02x};", bg.r, bg.g, bg.b),
);
let mut highlighter = HighlightLines::new(syntax, theme);
for line in LinesWithEndings::from(&self.content) {
let regions = highlighter
.highlight_line(line, &SYNTAX_SET)
.expect("highlight");
let mut active: Option<(Style, RenderElement)> = None;
for (style, text) in regions {
let unify_style = if let Some((active, _)) = &active {
style == *active
|| (style.background == active.background && text.trim().is_empty())
} else {
false
};
if unify_style {
let text = RenderText::new(text.to_string());
active.as_mut().unwrap().1.add_child(text);
} else {
if let Some(active) = active.take() {
pre.add_child(active.1);
}
let mut style_attr = Vec::new();
if style.background != bg {
style_attr.push(format!(
"background-color:#{:02x}{:02x}{:02x}",
style.background.r, style.background.g, style.background.b
));
}
if style.font_style.contains(FontStyle::BOLD) {
style_attr.push("font-weight:bold".to_string());
}
if style.font_style.contains(FontStyle::ITALIC) {
style_attr.push("font-style:italic".to_string());
}
if style.font_style.contains(FontStyle::UNDERLINE) {
style_attr.push("text-decoration:underline".to_string());
}
style_attr.push(format!(
"color:#{:02x}{:02x}{:02x}",
style.foreground.r, style.foreground.g, style.foreground.b
));
let mut span = RenderElement::new(TagName::Span);
span.add_attribute(AttributeName::Style, style_attr.join(";"));
span.add_child(RenderText::new(text.to_string()));
active = Some((style, span));
}
}
if let Some(active) = active.take() {
pre.add_child(active.1)
}
}
pre
}
}
pub struct Renderer<'a, I> {
tokens: I,
output: Vec<RenderNode>,
stack: Vec<RenderElement>,
footnotes: HashMap<CowStr<'a>, usize>,
highlight: Option<Highlighting>,
table_align: Vec<Alignment>,
table_head: bool,
table_colidx: usize,
}
impl<'a, I> Renderer<'a, I>
where
I: Iterator<Item = Event<'a>>,
{
pub fn new(tokens: I) -> Self {
Self {
tokens,
output: vec![],
stack: vec![],
footnotes: HashMap::new(),
highlight: None,
table_align: vec![],
table_head: false,
table_colidx: 0,
}
}
fn output<N: Into<RenderNode>>(&mut self, node: N) {
if let Some(top) = self.stack.last_mut() {
top.add_child(node)
} else {
self.output.push(node.into());
}
}
fn enter(&mut self, element: RenderElement) {
self.stack.push(element);
}
fn leave(&mut self, tag: TagName) {
let Some(top) = self.stack.pop() else {
panic!("Stack underflow");
};
assert!(
top.tag == tag,
"Expected to pop <{}>, found <{}>",
tag.as_str(),
top.tag.as_str()
);
self.output(top)
}
fn generate_bookmark(&mut self, source: &str) {
let Bookmark {
url,
title,
description,
author,
publisher,
thumbnail,
icon,
} = gray_matter::engine::YAML::parse(source)
.deserialize()
.expect("Bookmark properties");
let mut figure = RenderElement::new(TagName::Figure);
figure.add_attribute(AttributeName::Class, "w-full text-base");
let mut link = RenderElement::new(TagName::A);
link.add_attribute(AttributeName::Href, url);
link.add_attribute(
AttributeName::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) = thumbnail {
let mut div = RenderElement::new(TagName::Div);
div.add_attribute(
AttributeName::Class,
"relative lg:order-2 min-w-[33%] min-h-[160px] lg:min-h-fit max-h-[100%]",
);
let mut img = RenderElement::new(TagName::Img);
img.add_attribute(AttributeName::Src, thumbnail);
img.add_attribute(AttributeName::Alt, title.clone());
img.add_attribute(AttributeName::Loading, "lazy");
img.add_attribute(AttributeName::Decoding, "async");
div.add_child(img);
link.add_child(div);
}
let mut container = RenderElement::new(TagName::Div);
container.add_attribute(
AttributeName::Class,
"font-sans lg:order-1 grow flex flex-col justify-start align-start p-5",
);
container.add_child({
let mut title_div = RenderElement::new(TagName::Div);
title_div.add_attribute(AttributeName::Class, "font-semibold");
title_div.add_child(RenderText::new(title));
title_div
});
if let Some(description) = description {
container.add_child({
let mut descr_div = RenderElement::new(TagName::Div);
descr_div
.add_attribute(AttributeName::Class, "grow overflow-y-hidden mt-3 max-h-12");
descr_div.add_child(RenderText::new(description));
descr_div
});
}
let mut details = RenderElement::new(TagName::Div);
details.add_attribute(
AttributeName::Class,
"flex flex-row flex-wrap align-center gap-1 mt-3.5",
);
if let Some(icon) = icon {
let mut img = RenderElement::new(TagName::Img);
img.add_attribute(
AttributeName::Class,
"w-[18px] h-[18px] lg:w-[22px] lg:h-[22px] mr-3",
);
img.add_attribute(AttributeName::Alt, publisher.clone().unwrap_or_default());
img.add_attribute(AttributeName::Src, icon);
details.add_child(img);
}
if let Some(publisher) = publisher {
let mut span = RenderElement::new(TagName::Span);
span.add_child(RenderText::new(publisher));
details.add_child(span);
if author.is_some() {
let mut dot = RenderElement::new(TagName::Span);
dot.add_child(RenderText::new(""));
details.add_child(dot);
}
}
if let Some(author) = author {
let mut span = RenderElement::new(TagName::Span);
span.add_child(RenderText::new(author));
details.add_child(span);
}
container.add_child(details);
link.add_child(container);
figure.add_child(link);
self.output(figure);
}
fn generate_quote(&mut self, source: &str) {
let Quote { quote, author, url } = gray_matter::engine::YAML::parse(source)
.deserialize()
.expect("Bookmark properties");
let mut figure = RenderElement::new(TagName::Figure);
figure.add_attribute(AttributeName::Class, "quote");
let mut p = RenderElement::new(TagName::P);
p.add_child(RenderText::new(quote));
figure.add_child(p);
if let Some(author) = author {
let mut cite = RenderElement::new(TagName::Cite);
if let Some(url) = url {
let mut link = RenderElement::new(TagName::A);
link.add_attribute(AttributeName::Href, url);
link.add_child(RenderText::new(author));
cite.add_child(link);
} else {
cite.add_child(RenderText::new(author));
}
figure.add_child(cite);
}
self.output(figure);
}
fn component(&mut self, name: &str) -> bool {
match name {
"bookmark" => {
let content = self.raw_text();
self.generate_bookmark(&content);
true
}
"quote" => {
let content = self.raw_text();
self.generate_quote(&content);
true
}
_ => false,
}
}
fn start(&mut self, tag: Tag) {
match tag {
Tag::Paragraph => self.enter(RenderElement::new(TagName::P)),
Tag::Heading(level, ident, classes) => {
let mut heading = RenderElement::new(heading_for_level(level));
if let Some(ident) = ident {
heading.add_attribute(AttributeName::Id, ident.to_string());
}
if !classes.is_empty() {
let classes = classes.join(" ");
heading.add_attribute(AttributeName::Class, classes);
}
self.enter(heading);
}
Tag::BlockQuote => self.enter(RenderElement::new(TagName::BlockQuote)),
Tag::CodeBlock(kind) => {
if let CodeBlockKind::Fenced(language) = &kind {
if self.component(language) {
return;
}
}
let language = if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
let language = parse_language(&language);
Some(language)
} else {
None
}
} else {
None
};
let mut figure = RenderElement::new(TagName::Figure);
figure.add_attribute(AttributeName::Class, "code");
self.enter(figure);
self.highlight = None;
if let Some(language) = &language {
if language != "plain" {
self.highlight = Some(Highlighting {
language: language.to_string(),
content: String::new(),
});
}
}
if self.highlight.is_none() {
self.enter(RenderElement::new(TagName::Pre));
}
}
Tag::List(ordered) => self.enter(if let Some(start) = ordered {
let mut ol = RenderElement::new(TagName::Ol);
ol.add_attribute(AttributeName::Start, start.to_string());
ol
} else {
RenderElement::new(TagName::Ul)
}),
Tag::Item => self.enter(RenderElement::new(TagName::Li)),
Tag::Table(align) => {
self.table_align = align;
self.enter(RenderElement::new(TagName::Table));
}
Tag::TableHead => {
self.table_head = true;
self.enter(RenderElement::new(TagName::THead));
self.enter(RenderElement::new(TagName::Tr));
}
Tag::TableRow => {
self.table_colidx = 0;
self.enter(RenderElement::new(TagName::Tr));
}
Tag::TableCell => {
let mut cell = RenderElement::new(if self.table_head {
TagName::Th
} else {
TagName::Td
});
if let Some(align) =
self.table_align
.get(self.table_colidx)
.and_then(|align| match align {
Alignment::None => None,
Alignment::Left => Some("left"),
Alignment::Right => Some("right"),
Alignment::Center => Some("center"),
})
{
cell.add_attribute(AttributeName::Class, align);
}
self.enter(cell);
}
Tag::Emphasis => self.enter(RenderElement::new(TagName::Em)),
Tag::Strong => self.enter(RenderElement::new(TagName::Strong)),
Tag::Strikethrough => self.enter(RenderElement::new(TagName::S)),
Tag::Link(_, href, title) => {
let mut a = RenderElement::new(TagName::A);
a.add_attribute(AttributeName::Title, title.to_string());
a.add_attribute(AttributeName::Href, href.to_string());
self.enter(a);
}
Tag::Image(_, src, title) => {
let mut figure = RenderElement::new(TagName::Figure);
let mut img = RenderElement::new(TagName::Img);
img.add_attribute(AttributeName::Src, src.to_string());
img.add_attribute(AttributeName::Title, title.to_string());
let alt = self.raw_text();
img.add_attribute(AttributeName::Alt, alt.clone());
figure.add_child(img);
let mut figcaption = RenderElement::new(TagName::FigCaption);
figcaption.add_child(RenderText::new(alt));
figure.add_child(figcaption);
self.output(figure);
}
_ => {}
}
}
fn end(&mut self, tag: Tag) {
match tag {
Tag::Paragraph => self.leave(TagName::P),
Tag::Heading(level, _, _) => self.leave(heading_for_level(level)),
Tag::BlockQuote => self.leave(TagName::BlockQuote),
Tag::CodeBlock(kind) => {
let mut highlight = None;
std::mem::swap(&mut highlight, &mut self.highlight);
if let Some(highlight) = highlight {
let pre = highlight.finish();
self.output(pre);
} else {
self.leave(TagName::Pre); // <pre>
}
let properties = if let CodeBlockKind::Fenced(language) = kind {
if !language.is_empty() {
let (_, properties) = parse_language_properties(&language)
.expect("valid language and properties");
properties
} else {
Properties::default()
}
} else {
Properties::default()
};
if let Some(caption) = properties.get("caption") {
let mut figcap = RenderElement::new(TagName::FigCaption);
figcap.add_child(RenderText::new(caption.to_string()));
self.output(figcap);
}
self.leave(TagName::Figure); // <figure>
}
Tag::List(ordered) => self.leave(if ordered.is_some() {
TagName::Ol
} else {
TagName::Ul
}),
Tag::Item => self.leave(TagName::Li),
Tag::Table(_) => {
self.leave(TagName::TBody);
self.leave(TagName::Table);
}
Tag::TableRow => self.leave(TagName::Tr),
Tag::TableHead => {
self.leave(TagName::Tr);
self.leave(TagName::THead);
self.enter(RenderElement::new(TagName::TBody));
self.table_head = false;
}
Tag::TableCell => {
self.leave(if self.table_head {
TagName::Th
} else {
TagName::Td
});
self.table_colidx += 1;
}
Tag::Emphasis => self.leave(TagName::Em),
Tag::Strong => self.leave(TagName::Strong),
Tag::Strikethrough => self.leave(TagName::S),
Tag::Link(_, _, _) => self.leave(TagName::A),
_ => {}
}
}
fn raw_text(&mut self) -> String {
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) => output.push_str(&text),
Event::SoftBreak | Event::HardBreak | Event::Rule => output.push(' '),
Event::FootnoteReference(name) => {
let next = self.footnotes.len() + 1;
let footnote = *self.footnotes.entry(name).or_insert(next);
output.push_str(&format!("[{footnote}]"));
}
Event::TaskListMarker(true) => output.push_str("[x]"),
Event::TaskListMarker(false) => output.push_str("[ ]"),
}
}
output
}
fn event(&mut self, event: Event) {
match event {
Event::Start(tag) => self.start(tag),
Event::End(tag) => self.end(tag),
Event::Text(text) => {
if let Some(highlight) = &mut self.highlight {
highlight.content.push_str(&text);
} else {
self.output(RenderText::new(text.to_string()))
}
}
_ => {}
}
}
pub fn run(mut self) -> Vec<RenderNode> {
while let Some(event) = self.tokens.next() {
self.event(event);
}
self.output
}
}
pub fn render(content: &str) -> Vec<RenderNode> {
Renderer::new(Parser::new_ext(content, Options::all())).run()
}

116
model/Cargo.lock generated
View File

@ -2,6 +2,39 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "atomic-polyfill"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ff7eb3f316534d83a8a2c3d1674ace8a5a71198eba31e2e2b597833f699b28"
dependencies = [
"critical-section",
]
[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
[[package]]
name = "cobs"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15"
[[package]]
name = "critical-section"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216"
[[package]]
name = "deranged"
version = "0.3.8"
@ -11,15 +44,62 @@ dependencies = [
"serde",
]
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "heapless"
version = "0.7.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db04bc24a18b9ea980628ecf00e6c0264f3c1426dac36c00cb49b6fbad8b0743"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "lock_api"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "model"
version = "2.0.0"
dependencies = [
"postcard",
"proc-macro2",
"quote",
"serde",
"thiserror",
"time",
]
[[package]]
name = "postcard"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9ee729232311d3cd113749948b689627618133b1c5012b77342c1950b25eaeb"
dependencies = [
"cobs",
"heapless",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.66"
@ -38,6 +118,27 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
dependencies = [
"semver",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "serde"
version = "1.0.188"
@ -58,6 +159,21 @@ dependencies = [
"syn",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "syn"
version = "2.0.29"

View File

@ -5,6 +5,9 @@ publish = false
edition = "2021"
[dependencies]
postcard = { version = "1.0", features = ["use-std"] }
proc-macro2 = { version = "1.0" }
quote = { version = "1.0" }
serde = { version = "1.0", features = ["derive"] }
thiserror = { version = "1.0" }
time = { version = "0.3", features = ["serde", "parsing"] }

View File

@ -1,3 +1,4 @@
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::frontmatter::FrontMatter;
@ -60,3 +61,174 @@ impl<S> Details<S> {
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum RenderNode {
Text(RenderText),
Element(RenderElement),
}
impl From<RenderText> for RenderNode {
fn from(value: RenderText) -> Self {
Self::Text(value)
}
}
impl From<RenderElement> for RenderNode {
fn from(value: RenderElement) -> Self {
Self::Element(value)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderText {
pub content: String,
}
impl RenderText {
pub fn new<S: Into<String>>(content: S) -> Self {
Self {
content: content.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderElement {
pub tag: TagName,
pub attributes: Vec<RenderAttribute>,
pub children: Vec<RenderNode>,
}
impl RenderElement {
pub fn new(tag: TagName) -> Self {
Self {
tag,
attributes: Vec::new(),
children: Vec::new(),
}
}
pub fn add_attribute<A: Into<String>>(&mut self, name: AttributeName, value: A) {
self.attributes.push(RenderAttribute {
name,
value: value.into(),
})
}
pub fn add_child<C: Into<RenderNode>>(&mut self, child: C) {
self.children.push(child.into());
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum TagName {
A,
BlockQuote,
Cite,
Div,
Em,
FigCaption,
Figure,
H1,
H2,
H3,
H4,
H5,
H6,
Img,
Li,
Ol,
P,
Pre,
S,
Span,
Strong,
TBody,
THead,
Table,
Td,
Th,
Tr,
Ul,
}
impl TagName {
pub fn as_str(&self) -> &'static str {
match self {
TagName::A => "a",
TagName::BlockQuote => "blockquote",
TagName::Cite => "cite",
TagName::Div => "div",
TagName::Em => "em",
TagName::FigCaption => "figcaption",
TagName::Figure => "figure",
TagName::H1 => "h1",
TagName::H2 => "h2",
TagName::H3 => "h3",
TagName::H4 => "h4",
TagName::H5 => "h5",
TagName::H6 => "h6",
TagName::Img => "img",
TagName::Li => "li",
TagName::Ol => "ol",
TagName::P => "p",
TagName::Pre => "pre",
TagName::S => "s",
TagName::Span => "span",
TagName::Strong => "strong",
TagName::TBody => "tbody",
TagName::THead => "thead",
TagName::Table => "table",
TagName::Td => "td",
TagName::Th => "th",
TagName::Tr => "tr",
TagName::Ul => "ul",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RenderAttribute {
pub name: AttributeName,
pub value: String,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum AttributeName {
Alt,
Class,
Decoding,
Href,
Id,
Loading,
Src,
Start,
Style,
Title,
}
impl AttributeName {
pub fn as_str(&self) -> &'static str {
match self {
AttributeName::Alt => "alt",
AttributeName::Class => "class",
AttributeName::Decoding => "decoding",
AttributeName::Href => "href",
AttributeName::Id => "id",
AttributeName::Loading => "loading",
AttributeName::Src => "src",
AttributeName::Start => "start",
AttributeName::Style => "style",
AttributeName::Title => "title",
}
}
}
pub fn encode_nodes(nodes: Vec<RenderNode>) -> Vec<u8> {
postcard::to_stdvec(&nodes).expect("encoded nodes")
}
pub fn decode_nodes(encoded: &[u8]) -> Vec<RenderNode> {
postcard::from_bytes(encoded).expect("decoded nodes")
}

View File

@ -1,3 +1,4 @@
pub mod blog;
pub mod content;
pub mod layout;
pub mod render;

View File

@ -1,12 +1,15 @@
use model::document::Details;
use model::document::{Details, RenderNode};
use yew::{function_component, html, use_context, Html, Properties};
use crate::{components::blog::post_card::post_card_details, model::TagsContext};
use crate::{
components::{blog::post_card::post_card_details, render::Render},
model::TagsContext,
};
#[derive(Properties, PartialEq)]
pub struct PostContentProps<S: PartialEq> {
pub details: Details<S>,
pub content: Html,
pub content: Vec<RenderNode>,
}
#[function_component(PostContent)]
@ -39,7 +42,12 @@ pub fn post_content<S: PartialEq>(props: &PostContentProps<S>) -> Html {
</div>
</header>
<div class="container mx-auto my-12 px-16 markdown">
{props.content.clone()}
{
props.content.iter().map(|node| html! {
<Render node={node.clone()} />
})
.collect::<Html>()
}
</div>
</article>
}

40
src/components/render.rs Normal file
View File

@ -0,0 +1,40 @@
use model::document::{RenderElement, RenderNode, RenderText};
use yew::{
function_component,
virtual_dom::{VTag, VText},
Html, Properties,
};
fn render_node(node: &RenderNode) -> Html {
match node {
RenderNode::Text(RenderText { content }) => VText::new(content.to_string()).into(),
RenderNode::Element(RenderElement {
tag,
attributes,
children,
}) => {
let mut tag = VTag::new(tag.as_str());
for attribute in attributes {
tag.add_attribute(attribute.name.as_str(), attribute.value.to_string());
}
for child in children {
let child = render_node(child);
tag.add_child(child);
}
tag.into()
}
}
}
#[derive(Properties, PartialEq)]
pub struct RenderProps {
pub node: RenderNode,
}
#[function_component(Render)]
pub fn render(props: &RenderProps) -> Html {
render_node(&props.node)
}