Switch over to WebAssembly, Rust and Yew #35
@ -1,6 +1,3 @@
|
|||||||
pub mod blog;
|
pub mod blog;
|
||||||
// pub mod content;
|
|
||||||
pub mod content;
|
pub mod content;
|
||||||
pub mod document;
|
|
||||||
pub mod layout;
|
pub mod layout;
|
||||||
// pub mod markdown;
|
|
||||||
|
@ -1,2 +0,0 @@
|
|||||||
pub mod bookmark;
|
|
||||||
pub mod quote;
|
|
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
@ -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>
|
|
||||||
}
|
|
||||||
}
|
|
@ -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()
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user