Skip to content
Closed

Mdx #3971

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/yew-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ mod derive_props;
mod function_component;
mod hook;
mod html_tree;
mod mdx;
mod props;
mod stringify;
mod use_prepared_state;
Expand Down Expand Up @@ -186,3 +187,19 @@ pub fn use_transitive_state_without_closure(input: TokenStream) -> TokenStream {
let transitive_state = parse_macro_input!(input as TransitiveState);
transitive_state.to_token_stream_without_closure().into()
}

#[proc_macro]
pub fn mdx(input: TokenStream) -> TokenStream {
let mdx_tokens = mdx::mdx(input);
html(mdx_tokens)
}

#[proc_macro]
pub fn mdx_style(input: TokenStream) -> TokenStream {
mdx::mdx_style(input)
}

#[proc_macro]
pub fn include_mdx(input: TokenStream) -> TokenStream {
html(mdx::include_mdx(input))
}
173 changes: 173 additions & 0 deletions packages/yew-macro/src/mdx/cmark.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
use proc_macro::TokenStream;
use pulldown_cmark::{Event, Options, Parser, Tag};
use quote::quote;

use super::GLOBAL_STYLE;

// styling idea:
// caller passes in mapping of tag to yew component name
// caller can implement component however they want, it can take children
// instead of rendering <p> we render <SpecialP>
// problem: has to be specified at every call-site, and it's verbose because it has to be the input
// to the proc macro
//
// take 2: use yew dynamic tags to lookup in a named or global style array,
// including fallbacks to defaults. how to control whether style is applied at
// all? could have 2 different mdx macros, mdx/mdxs (global style) / mdxss
// (user-specified name of style map)
// Disadvantage: yew component name validation done at runtime
// instead of mapping to component name, could just map to Fn(children)-> Html
// dealbreaker?: do dynamic tags work with components anyways??
//
// take2.5: just dont use dynamic tag?? -- but then we need the map specified at compile-time
//
// take 3: separate mdx_style! macro to define styles

// map static tag to dynamic tag, falling back to the given tag
#[derive(PartialEq)]
enum Side {
Start,
End,
}

fn dyn_tag_name_opt(tag: &str) -> Option<String> {
GLOBAL_STYLE.lock().unwrap().get(tag).map(Into::into)
}
fn dyn_tag_name(tag: &str) -> String {
dyn_tag_name_opt(tag).unwrap_or(tag.into())
}

fn dyn_tag(tag: &str, side: Side) -> TokenStream {
let tag = dyn_tag_name_opt(tag).unwrap_or(tag.into());
(match side {
Side::Start => "<",
Side::End => "</",
}
.to_string()
+ &tag
+ ">")
.parse()
.unwrap()
}

fn dyn_tag_opt(tag: &str, side: Side) -> Option<TokenStream> {
GLOBAL_STYLE.lock().unwrap().get(tag.into()).map(|tag| {
(match side {
Side::Start => "<",
Side::End => "</",
}
.to_string()
+ &tag
+ ">")
.parse()
.unwrap()
})
}

pub fn parse_commonmark(input: &str) -> TokenStream {
let parser = Parser::new_ext(input, Options::all());

let mut toks = TokenStream::new();
toks.extend::<TokenStream>("<>".parse().unwrap());

parser.for_each(|evt| {
// dbg!(&evt);
let new_toks: TokenStream = match evt {
Event::Start(tag) => match tag {
Tag::Paragraph => dyn_tag("p", Side::Start),
Tag::Heading(lvl, ..) => dyn_tag(&lvl.to_string(), Side::Start),
Tag::BlockQuote => dyn_tag("blockquote", Side::Start),
Tag::CodeBlock(kind) => match kind {
pulldown_cmark::CodeBlockKind::Indented => FromIterator::from_iter(
[
dyn_tag("pre", Side::Start),
"<code>".parse::<TokenStream>().unwrap(),
]
.into_iter(),
),
pulldown_cmark::CodeBlockKind::Fenced(lang) => {
let tags = FromIterator::from_iter(
[
dyn_tag("pre", Side::Start),
format!("<code class=\"language-{}\">", lang)
.parse::<TokenStream>()
.unwrap(),
]
.into_iter(),
);

tags
}
},
Tag::List(None) => dyn_tag("ul", Side::Start),
Tag::List(Some(0)) => dyn_tag("ol", Side::Start),
Tag::List(Some(0..=u64::MAX)) => dyn_tag("ol", Side::Start),
Tag::Item => dyn_tag("li", Side::Start),
Tag::FootnoteDefinition(_) => todo!(),
Tag::Table(_) => dyn_tag("table", Side::Start),
Tag::TableHead => dyn_tag("thead", Side::Start),
Tag::TableRow => dyn_tag("tr", Side::Start),
Tag::TableCell => dyn_tag("td", Side::Start),
Tag::Emphasis => dyn_tag("em", Side::Start),
Tag::Strong => dyn_tag("strong", Side::Start),
Tag::Strikethrough => dyn_tag("s", Side::Start),
Tag::Link(_type, url, title) => {
format!("<{} href=\"{}\">{}", dyn_tag_name("a").to_string(), url, title)
.parse()
.unwrap()
}
Tag::Image(_type, url, title) => {
let tag = dyn_tag_name("url");
format!(r#"<{tag} src="{url}" title="{title}"/>"#)
.parse()
.unwrap()
}
},
Event::End(tag) => match tag {
Tag::Paragraph => dyn_tag("p", Side::End),
Tag::Heading(lvl, ..) => dyn_tag(&lvl.to_string(), Side::End),
Tag::BlockQuote => dyn_tag("blockquote", Side::End),
Tag::CodeBlock(_) => {
FromIterator::from_iter(["</code>".parse().unwrap(), dyn_tag("pre", Side::End)])
}
Tag::List(None) => dyn_tag("ul", Side::End),
Tag::List(Some(0)) => dyn_tag("ol", Side::End),
Tag::List(Some(0..=u64::MAX)) => dyn_tag("ol", Side::End),
Tag::Item => dyn_tag("li", Side::End),
Tag::FootnoteDefinition(_) => todo!(),
Tag::Table(_) => dyn_tag("table", Side::End),
Tag::TableHead => dyn_tag("thead", Side::End),
Tag::TableRow => dyn_tag("tr", Side::End),
Tag::TableCell => dyn_tag("td", Side::End),
Tag::Emphasis => dyn_tag("em", Side::End),
Tag::Strong => dyn_tag("strong", Side::End),
Tag::Strikethrough => dyn_tag("s", Side::End),
Tag::Link(_type, _url, _title) => dyn_tag("a", Side::End),
Tag::Image(_type, _url, _title) => "".parse().unwrap(),
},
Event::Text(txt) => format!("{{r###\"{}\"###}}", txt).parse().unwrap(),
Event::Code(code) => {
let tag = dyn_tag_name("code");
format!("<{tag}>{{r###\"{}\"###}}</{tag}>", code)
.parse()
.unwrap()
}
Event::Rule => {
let tag = dyn_tag_name("hr");
format!("<{tag} />").parse().unwrap()
}
Event::SoftBreak => "{{\" \"}}".parse().unwrap(),
Event::HardBreak => {
let tag = dyn_tag_name("br");
format!("<{tag} />").parse().unwrap()
},
Event::Html(html) => html.parse().unwrap(),
_ => quote! {}.into(),
};
toks.extend(new_toks);
});

toks.extend::<TokenStream>("</>".parse().unwrap());

toks
}
75 changes: 75 additions & 0 deletions packages/yew-macro/src/mdx/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
mod cmark;

use std::collections::HashMap;
use std::sync::{Arc, Mutex};

use proc_macro::{TokenStream, TokenTree};

use self::cmark::parse_commonmark;

lazy_static::lazy_static! {
static ref GLOBAL_STYLE : Arc<Mutex<HashMap<String, String>>> = {
Default::default()
};
}

pub fn mdx_style(input: TokenStream) -> TokenStream {
let input = input.into_iter().collect::<Vec<_>>();
for chunk in input.chunks(4) {
let (from, to) = (chunk.get(0), chunk.get(2));
match from.zip(to) {
Some((TokenTree::Ident(from), TokenTree::Ident(to))) => {
GLOBAL_STYLE
.lock()
.unwrap()
.insert(from.to_string(), to.to_string());
}
_ => {}
}
}
// GLOBAL_STYLE
// .lock()
// .unwrap()
// .insert("h3".into(), "MyHeading3".into());
quote::quote! {}.into()
}

pub fn mdx(input: TokenStream) -> TokenStream {
let parsed = input
.into_iter()
.map(|token| match token {
lit @ TokenTree::Literal(_) => {
let mdx_str = lit.to_string();
let mdx_str = mdx_str
.strip_prefix("r#\"")
.unwrap()
.strip_suffix("\"#")
.unwrap();
parse_commonmark(&mdx_str)
}
_ => panic!("mdx! expected literal"),
})
.collect::<TokenStream>();

parsed
}

pub fn include_mdx(input: TokenStream) -> TokenStream {
let file_path: std::path::PathBuf = input
.to_string()
.trim_start_matches('"')
.trim_end_matches('"')
.parse()
.unwrap();

let full_path = std::env::var("CARGO_MANIFEST_DIR")
.unwrap()
.parse::<std::path::PathBuf>()
.unwrap()
.join(file_path);
let contents = std::fs::read_to_string(full_path)
.unwrap()
.replace("\r", "");

parse_commonmark(&contents)
}
Loading
Loading