From d3e5f3a8a55d57b55dd873b4cf347b35ec34486a Mon Sep 17 00:00:00 2001 From: Bnchi Date: Sun, 20 Oct 2024 08:37:08 +0300 Subject: [PATCH] Add mdx serialization support --- .../src/handle/mdx_expression.rs | 45 +++++ .../src/handle/mdx_js_esm.rs | 20 +++ mdast_util_to_markdown/src/handle/mdx_jsx.rs | 74 +++++++++ mdast_util_to_markdown/src/handle/mod.rs | 3 + mdast_util_to_markdown/src/handle/root.rs | 1 + mdast_util_to_markdown/src/state.rs | 10 +- mdast_util_to_markdown/src/unsafe.rs | 12 +- mdast_util_to_markdown/tests/mdx.rs | 154 ++++++++++++++++++ 8 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 mdast_util_to_markdown/src/handle/mdx_expression.rs create mode 100644 mdast_util_to_markdown/src/handle/mdx_js_esm.rs create mode 100644 mdast_util_to_markdown/src/handle/mdx_jsx.rs create mode 100644 mdast_util_to_markdown/tests/mdx.rs diff --git a/mdast_util_to_markdown/src/handle/mdx_expression.rs b/mdast_util_to_markdown/src/handle/mdx_expression.rs new file mode 100644 index 00000000..448ac56c --- /dev/null +++ b/mdast_util_to_markdown/src/handle/mdx_expression.rs @@ -0,0 +1,45 @@ +//! JS equivalent: https://github.com/syntax-tree/mdast-util-mdx-expression/blob/main/lib/index.js#L42 + +use super::Handle; +use crate::state::{Info, State}; +use alloc::{format, string::String}; +use markdown::{ + mdast::{MdxFlowExpression, MdxTextExpression, Node}, + message::Message, +}; + +impl Handle for MdxFlowExpression { + fn handle( + &self, + state: &mut State, + _info: &Info, + _parent: Option<&Node>, + _node: &Node, + ) -> Result { + Ok(handle_mdx_expression(&self.value, state)) + } +} + +impl Handle for MdxTextExpression { + fn handle( + &self, + state: &mut State, + _info: &Info, + _parent: Option<&Node>, + _node: &Node, + ) -> Result { + Ok(handle_mdx_expression(&self.value, state)) + } +} + +fn handle_mdx_expression(value: &str, state: &State) -> String { + let result = state.indent_lines(value, |line, index, blank| { + let space = if index == 0 || blank { "" } else { " " }; + let mut results = String::with_capacity(space.len() + line.len()); + results.push_str(space); + results.push_str(line); + results + }); + + format!("{{{}}}", result) +} diff --git a/mdast_util_to_markdown/src/handle/mdx_js_esm.rs b/mdast_util_to_markdown/src/handle/mdx_js_esm.rs new file mode 100644 index 00000000..6d0c3cb2 --- /dev/null +++ b/mdast_util_to_markdown/src/handle/mdx_js_esm.rs @@ -0,0 +1,20 @@ +//! JS equivalent: https://github.com/syntax-tree/mdast-util-mdxjs-esm/blob/main/lib/index.js#L79 + +use super::Handle; +use crate::state::{Info, State}; +use markdown::{ + mdast::{MdxjsEsm, Node}, + message::Message, +}; + +impl Handle for MdxjsEsm { + fn handle( + &self, + _state: &mut State, + _info: &Info, + _parent: Option<&Node>, + _node: &Node, + ) -> Result { + Ok(self.value.clone()) + } +} diff --git a/mdast_util_to_markdown/src/handle/mdx_jsx.rs b/mdast_util_to_markdown/src/handle/mdx_jsx.rs new file mode 100644 index 00000000..8d31e6da --- /dev/null +++ b/mdast_util_to_markdown/src/handle/mdx_jsx.rs @@ -0,0 +1,74 @@ +//! JS equivalent: https://github.com/syntax-tree/mdast-util-to-markdown/blob/main/lib/handle/text.js + +use super::Handle; +use crate::{ + construct_name::ConstructName, + state::{Info, State}, +}; +use alloc::{string::String, vec::Vec}; +use markdown::{ + mdast::{AttributeContent, MdxJsxFlowElement, Node}, + message::Message, + unist::Position, +}; + +#[allow(dead_code)] +trait MdxJsx { + fn children(&self) -> &Vec; + fn name(&self) -> &Option; + fn attributes(&self) -> &Vec; + fn position(&self) -> &Option; +} + +impl MdxJsx for MdxJsxFlowElement { + fn children(&self) -> &Vec { + &self.children + } + + fn name(&self) -> &Option { + &self.name + } + + fn attributes(&self) -> &Vec { + &self.attributes + } + + fn position(&self) -> &Option { + &self.position + } +} + +impl Handle for MdxJsxFlowElement { + fn handle( + &self, + _state: &mut State, + _info: &Info, + _parent: Option<&Node>, + _node: &Node, + ) -> Result { + Ok(String::new()) + } +} + +#[allow(dead_code)] +fn create_indent(depth: usize) -> String { + " ".repeat(depth) +} + +#[allow(dead_code)] +fn infer_depth(state: &State) -> usize { + let mut depth: usize = 0; + + for construct_name in state.stack.iter().rev() { + if matches!( + construct_name, + ConstructName::Blockquote | ConstructName::ListItem + ) { + break; + } else { + depth += 1; + } + } + + depth +} diff --git a/mdast_util_to_markdown/src/handle/mod.rs b/mdast_util_to_markdown/src/handle/mod.rs index 8e5e0c01..499c1d81 100644 --- a/mdast_util_to_markdown/src/handle/mod.rs +++ b/mdast_util_to_markdown/src/handle/mod.rs @@ -18,6 +18,9 @@ pub mod link_reference; mod list; mod list_item; mod math; +mod mdx_expression; +mod mdx_js_esm; +mod mdx_jsx; mod paragraph; mod root; pub mod strong; diff --git a/mdast_util_to_markdown/src/handle/root.rs b/mdast_util_to_markdown/src/handle/root.rs index 6ab8a95e..0eecff90 100644 --- a/mdast_util_to_markdown/src/handle/root.rs +++ b/mdast_util_to_markdown/src/handle/root.rs @@ -39,6 +39,7 @@ fn phrasing(child: &Node) -> bool { | Node::InlineMath(_) | Node::Link(_) | Node::LinkReference(_) + | Node::MdxTextExpression(_) | Node::Strong(_) | Node::Text(_) ) diff --git a/mdast_util_to_markdown/src/state.rs b/mdast_util_to_markdown/src/state.rs index 891e82bc..a1804832 100644 --- a/mdast_util_to_markdown/src/state.rs +++ b/mdast_util_to_markdown/src/state.rs @@ -325,6 +325,13 @@ impl<'a> State<'a> { Node::ThematicBreak(thematic_break) => thematic_break.handle(self, info, parent, node), Node::Math(math) => math.handle(self, info, parent, node), Node::InlineMath(inline_math) => inline_math.handle(self, info, parent, node), + Node::MdxFlowExpression(mdx_flow_expression) => { + mdx_flow_expression.handle(self, info, parent, node) + } + Node::MdxTextExpression(mdx_text_expression) => { + mdx_text_expression.handle(self, info, parent, node) + } + Node::MdxjsEsm(mdx_js_esm) => mdx_js_esm.handle(self, info, parent, node), _ => Err(Message { place: None, reason: format!("Unexpected node type `{:?}`", node), @@ -350,7 +357,8 @@ impl<'a> State<'a> { line += 1; } - result.push_str(&map(&value[start..], line, value.is_empty())); + let value_slice = &value[start..]; + result.push_str(&map(value_slice, line, value_slice.is_empty())); result } diff --git a/mdast_util_to_markdown/src/unsafe.rs b/mdast_util_to_markdown/src/unsafe.rs index 062076ca..965778ba 100644 --- a/mdast_util_to_markdown/src/unsafe.rs +++ b/mdast_util_to_markdown/src/unsafe.rs @@ -233,10 +233,11 @@ impl<'a> Unsafe<'a> { '<', None, None, - vec![ConstructName::DestinationLiteral], + vec![ConstructName::DestinationLiteral, ConstructName::Phrasing], vec![], false, ), + Self::new('<', None, None, vec![], vec![], true), Self::new('=', None, None, vec![], vec![], true), Self::new('>', None, None, vec![], vec![], true), Self::new( @@ -331,6 +332,15 @@ impl<'a> Unsafe<'a> { false, ), Self::new('$', None, "\\$".into(), vec![], vec![], true), + Self::new( + '{', + None, + None, + vec![ConstructName::Phrasing], + vec![], + false, + ), + Self::new('{', None, None, vec![], vec![], true), ] } diff --git a/mdast_util_to_markdown/tests/mdx.rs b/mdast_util_to_markdown/tests/mdx.rs new file mode 100644 index 00000000..0ec7523a --- /dev/null +++ b/mdast_util_to_markdown/tests/mdx.rs @@ -0,0 +1,154 @@ +use markdown::{ + mdast::{ + Definition, MdxFlowExpression, MdxTextExpression, MdxjsEsm, Node, Paragraph, Root, Text, + }, + to_mdast as from, ParseOptions, +}; +use mdast_util_to_markdown::to_markdown as to; +use pretty_assertions::assert_eq; + +#[test] +fn mdx() { + assert_eq!( + to(&Node::Root(Root { + children: vec![ + Node::MdxFlowExpression(MdxFlowExpression { + value: "a + b".to_string(), + position: None, + stops: vec![] + }), + Node::MdxFlowExpression(MdxFlowExpression { + value: String::from("\nc +\n1\n"), + position: None, + stops: vec![] + }), + Node::MdxFlowExpression(MdxFlowExpression { + value: String::new(), + position: None, + stops: vec![] + }), + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "d".to_string(), + position: None + })], + position: None + }) + ], + position: None + })) + .unwrap(), + "{a + b}\n\n{\n c +\n 1\n}\n\n{}\n\nd\n", + "should serialize flow expressions" + ); + + assert_eq!( + to(&Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "a ".to_string(), + position: None + }), + Node::MdxTextExpression(MdxTextExpression { + value: "b + c".to_string(), + position: None, + stops: vec![] + }), + Node::Text(Text { + value: ", d ".to_string(), + position: None + }), + Node::MdxTextExpression(MdxTextExpression { + value: "e + 1".to_string(), + position: None, + stops: vec![] + }), + Node::Text(Text { + value: ", f ".to_string(), + position: None + }), + Node::MdxTextExpression(MdxTextExpression { + value: String::new(), + position: None, + stops: vec![] + }), + Node::Text(Text { + value: ".".to_string(), + position: None + }), + ], + position: None + })) + .unwrap(), + "a {b + c}, d {e + 1}, f {}.\n", + "should serialize text expressions" + ); + + assert_eq!( + to(&Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "a { b".to_string(), + position: None + })], + position: None + })) + .unwrap(), + "a \\{ b\n", + "should escape `{{` in text" + ); + + assert_eq!( + to(&Node::Definition(Definition { + position: None, + url: "x".to_string(), + title: "a\n{\nb".to_string().into(), + identifier: "a".to_string(), + label: None + })) + .unwrap(), + "[a]: x \"a\n\\{\nb\"\n", + "should escape `{{` at the start of a line" + ); + + assert_eq!( + to(&from(" {`\n a\n `}", &ParseOptions::mdx()).unwrap()).unwrap(), + "{`\n a\n `}\n", + "should strip superfluous whitespace depending on the opening prefix, when roundtripping expressions (flow)" + ); + + // This require changing to match the js tests when https://github.com/wooorm/markdown-rs/issues/150 is resolved + //assert_eq!( + // to(&from(" {`\n a\n `}", &ParseOptions::mdx()).unwrap()).unwrap(), + // "{`\n a\n `}\n", + // "should *not* strip superfluous whitespace (if there is more) when roundtripping expressions (flow)" + //); + + //// This require changing to match the js tests when https://github.com/wooorm/markdown-rs/issues/150 is resolved + //assert_eq!( + // to(&from("a {`\n b\n `} c", &ParseOptions::mdx()).unwrap()).unwrap(), + // "a {`\n b\n `} c\n", + // "should not strip consecutive lines in expressions (text)" + //); + + assert_eq!( + to(&Node::Root(Root { + children: vec![ + Node::MdxjsEsm(MdxjsEsm { + value: String::from("import a from \"b\"\nexport var c = \"\""), + position: None, + stops: Vec::new() + }), + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "d".to_string(), + position: None + })], + position: None + }) + ], + position: None + })) + .unwrap(), + "import a from \"b\"\nexport var c = \"\"\n\nd\n" + ) +}