Switch over to WebAssembly, Rust and Yew #35
94
Cargo.lock
generated
94
Cargo.lock
generated
@ -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
110
macros/Cargo.lock
generated
@ -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"
|
||||
|
@ -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
|
||||
|
616
macros/src/documents/markdown.rs
Normal file
616
macros/src/documents/markdown.rs
Normal 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
116
model/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"] }
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
pub mod blog;
|
||||
pub mod content;
|
||||
pub mod layout;
|
||||
pub mod render;
|
||||
|
@ -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
40
src/components/render.rs
Normal 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user