diff --git a/packages/yew-macro/src/lib.rs b/packages/yew-macro/src/lib.rs
index f7950538ffb..85cb73bf7c2 100644
--- a/packages/yew-macro/src/lib.rs
+++ b/packages/yew-macro/src/lib.rs
@@ -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;
@@ -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))
+}
diff --git a/packages/yew-macro/src/mdx/cmark.rs b/packages/yew-macro/src/mdx/cmark.rs
new file mode 100644
index 00000000000..0049fcf66ee
--- /dev/null
+++ b/packages/yew-macro/src/mdx/cmark.rs
@@ -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
we render
+// 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 {
+ 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 {
+ 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::("<>".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),
+ "".parse::().unwrap(),
+ ]
+ .into_iter(),
+ ),
+ pulldown_cmark::CodeBlockKind::Fenced(lang) => {
+ let tags = FromIterator::from_iter(
+ [
+ dyn_tag("pre", Side::Start),
+ format!("", lang)
+ .parse::()
+ .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(["".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::(">".parse().unwrap());
+
+ toks
+}
diff --git a/packages/yew-macro/src/mdx/mod.rs b/packages/yew-macro/src/mdx/mod.rs
new file mode 100644
index 00000000000..8d4ca9da074
--- /dev/null
+++ b/packages/yew-macro/src/mdx/mod.rs
@@ -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>> = {
+ Default::default()
+};
+}
+
+pub fn mdx_style(input: TokenStream) -> TokenStream {
+ let input = input.into_iter().collect::>();
+ 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::();
+
+ 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::()
+ .unwrap()
+ .join(file_path);
+ let contents = std::fs::read_to_string(full_path)
+ .unwrap()
+ .replace("\r", "");
+
+ parse_commonmark(&contents)
+}
diff --git a/packages/yew-macro/tests/mdx_macro_test.rs b/packages/yew-macro/tests/mdx_macro_test.rs
new file mode 100644
index 00000000000..2d1a23e33f7
--- /dev/null
+++ b/packages/yew-macro/tests/mdx_macro_test.rs
@@ -0,0 +1,221 @@
+use yew::{Children, Html};
+use yew_macro::{function_component, html, mdx, mdx_style, Properties};
+
+// h3 used for testing styling
+#[cfg(test)]
+mdx_style!(h3: MyHeading3,);
+
+#[derive(Properties, PartialEq)]
+struct MyPProps {
+ children: Children,
+}
+
+#[function_component]
+fn MyP(props: &MyPProps) -> Html {
+ html! {
+ {props.children.clone()}
+ }
+}
+
+#[test]
+fn text() {
+ assert_eq!(
+ mdx! {
+ r#"hi"#
+ },
+ html! {<>{"hi"}
>}
+ );
+}
+
+#[test]
+fn h1() {
+ dbg_eq(
+ mdx! {
+ r#"# hi"#
+ },
+ html! {
+ <>{"hi"}
>
+ },
+ );
+}
+
+#[test]
+fn a() {
+ dbg_eq(
+ mdx! {
+ r#"[this is a link](google.com)"#
+ },
+ html! {
+ <>{"this is a link"}
>
+ },
+ )
+}
+
+#[test]
+fn nested() {
+ dbg_eq(
+ mdx! {
+ r#"# Wow a [link](google.com) in a title"#
+ },
+ html! {
+ <>{"Wow a "}{"link"}{" in a title"}
>
+ },
+ )
+}
+
+#[test]
+fn multiple() {
+ dbg_eq(
+ mdx! {
+ r#"Some text [link](google.com)"#
+ },
+ html! {
+ <>
+
+ {"Some text "}
+ {"link"}
+
+ >
+ },
+ )
+}
+
+#[test]
+fn multiline_text() {
+ dbg_eq(
+ mdx! { r#"this is some text that
+ spans multiple lines"#
+ },
+ html! {<>{"this is some text that"}{" "}{"spans multiple lines"}
>},
+ )
+}
+
+#[test]
+fn multiline_link() {
+ dbg_eq(
+ mdx! { r#"[this is a
+ multiline link wow](google.com)"#},
+ html! {
+ <>
+
+ {"this is a"} {" "} {"multiline link wow"}
+
+ >
+ },
+ )
+}
+
+#[test]
+fn basic_code() {
+ dbg_eq(
+ mdx! {r#"here is some `inline code` ooo"#},
+ html! {
+ <>
+
+ {"here is some "}{"inline code"}{" ooo"}
+
+ >
+ },
+ );
+ dbg_eq(
+ mdx! {r#"# header `inline code` ooo"#},
+ html! {
+ <>
+
+ {"header "}{"inline code"}{" ooo"}
+
+ >
+ },
+ );
+ dbg_eq(
+ mdx! {r#"# header [link `inline code`](google.com) ooo"#},
+ html! {
+ <>
+
+ >
+ },
+ );
+}
+
+#[test]
+fn list() {
+ dbg_eq(
+ mdx! {r#"
+- one
+- two
+- three
+"#},
+ html! {
+ <>
+
+ - {"one"}
+ - {"two"}
+ - {"three"}
+
+ >
+
+ },
+ )
+}
+
+#[test]
+fn component() {
+ dbg_eq(
+ mdx! {r#"
+#
+"#},
+ html! {
+ <>
+
+
+
+ >
+ },
+ );
+}
+
+#[test]
+fn quotes_escaped() {
+ // NOTE: if quotes aren't correctly esacped, this will panic as invalid
+ // syntax because of
+ // https://doc.rust-lang.org/edition-guide/rust-2021/reserving-syntax.html#summary
+ mdx!(r#"i "like"quotes"" """"#);
+}
+
+fn dbg_eq(a: T, b: T) {
+ assert_eq!(format!("{a:?}"), format!("{b:?}"));
+}
+
+#[function_component]
+fn TestComponent() -> Html {
+ html! {
+
+ }
+}
+
+#[derive(PartialEq, Properties)]
+struct MyHeading3Props {
+ pub children: Children,
+}
+#[function_component]
+fn MyHeading3(c: &MyHeading3Props) -> Html {
+ html! {
+
+
+ {c.children.clone()}
+
+
+ }
+}
+
+#[test]
+fn style_h3() {
+ dbg_eq(
+ mdx! {r#"### 123"#},
+ html! {
+ <>{"123"}>
+ },
+ )
+}