diff --git a/src/configuration.rs b/src/configuration.rs index eab2dbf..f080e22 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1068,6 +1068,8 @@ pub struct ParseOptions { /// However, single dollars can interfere with “normal” dollars in text. /// Pass `false`, to only allow math (text) to form when two or more /// dollars are used. + /// If you pass `false`, you can still use two or more dollars for text + /// math. /// /// ## Examples /// diff --git a/src/construct/mdx_expression_flow.rs b/src/construct/mdx_expression_flow.rs index bfb73de..32403c9 100644 --- a/src/construct/mdx_expression_flow.rs +++ b/src/construct/mdx_expression_flow.rs @@ -107,12 +107,83 @@ pub fn after(tokenizer: &mut Tokenizer) -> State { /// ^ /// ``` pub fn end(tokenizer: &mut Tokenizer) -> State { - tokenizer.concrete = false; - tokenizer.tokenize_state.token_1 = Name::Data; + // We want to allow tags directly after expressions. + // + // This case is useful: + // + // ```mdx + // {b} + // ``` + // + // This case is not (very?) useful: + // + // ```mdx + // {a} + // ``` + // + // …but it would be tougher than needed to disallow. + // + // To allow that, here we call the MDX JSX flow construct, and there we + // call this one. + // + // It would introduce a cyclical interdependency if we test JSX and + // expressions here. + // Because the JSX extension already uses parts of this monorepo, we + // instead test it there. + // + // Note: in the JS version of micromark, arbitrary extensions could be + // loaded. + // Here we know that only our own construct `mdx_expression_flow` can be + // enabled. - if matches!(tokenizer.current, None | Some(b'\n')) { - State::Ok - } else { - State::Nok + // if matches!(tokenizer.current, None | Some(b'\n')) { + // State::Ok + // } else { + // State::Nok + // } + match tokenizer.current { + None | Some(b'\n') => { + reset(tokenizer); + State::Ok + } + // Tag. + Some(b'<') if tokenizer.parse_state.options.constructs.mdx_jsx_flow => { + // We can’t just say: fine. + // Lines of blocks have to be parsed until an eol/eof. + tokenizer.attempt( + State::Next(StateName::MdxExpressionFlowAfter), + State::Next(StateName::MdxExpressionFlowNok), + ); + State::Retry(StateName::MdxJsxStart) + } + // // An expression. + // Some(b'{') if tokenizer.parse_state.options.constructs.mdx_expression_flow => { + // tokenizer.attempt( + // State::Next(StateName::MdxExpressionFlowAfter), + // State::Next(StateName::MdxExpressionFlowNok), + // ); + // State::Retry(StateName::MdxExpressionFlowStart) + // } + _ => { + reset(tokenizer); + State::Nok + } } } + +/// At something that wasn’t an MDX expression (flow). +/// +/// ```markdown +/// > | {A} x +/// ^ +/// ``` +pub fn nok(tokenizer: &mut Tokenizer) -> State { + reset(tokenizer); + State::Nok +} + +/// Reset state. +fn reset(tokenizer: &mut Tokenizer) { + tokenizer.concrete = false; + tokenizer.tokenize_state.token_1 = Name::Data; +} diff --git a/src/construct/mdx_jsx_flow.rs b/src/construct/mdx_jsx_flow.rs index 4c3dd23..dafe70e 100644 --- a/src/construct/mdx_jsx_flow.rs +++ b/src/construct/mdx_jsx_flow.rs @@ -112,19 +112,37 @@ pub fn after(tokenizer: &mut Tokenizer) -> State { /// ^ /// ``` pub fn end(tokenizer: &mut Tokenizer) -> State { + // We want to allow expressions directly after tags. + // See + // for more info. + // + // Note: in the JS version of micromark, arbitrary extensions could be + // loaded. + // Here we know that only our own construct `mdx_expression_flow` can be + // enabled. match tokenizer.current { None | Some(b'\n') => { reset(tokenizer); State::Ok } - // Another? + // Another tag. Some(b'<') => { + // We can’t just say: fine. + // Lines of blocks have to be parsed until an eol/eof. tokenizer.attempt( State::Next(StateName::MdxJsxFlowAfter), State::Next(StateName::MdxJsxFlowNok), ); State::Retry(StateName::MdxJsxStart) } + // An expression. + Some(b'{') if tokenizer.parse_state.options.constructs.mdx_expression_flow => { + tokenizer.attempt( + State::Next(StateName::MdxJsxFlowAfter), + State::Next(StateName::MdxJsxFlowNok), + ); + State::Retry(StateName::MdxExpressionFlowStart) + } _ => { reset(tokenizer); State::Nok diff --git a/src/state.rs b/src/state.rs index c3f98e5..e7387e6 100644 --- a/src/state.rs +++ b/src/state.rs @@ -357,6 +357,7 @@ pub enum Name { MdxExpressionFlowBefore, MdxExpressionFlowAfter, MdxExpressionFlowEnd, + MdxExpressionFlowNok, MdxExpressionStart, MdxExpressionBefore, @@ -843,6 +844,7 @@ pub fn call(tokenizer: &mut Tokenizer, name: Name) -> State { Name::MdxExpressionFlowBefore => construct::mdx_expression_flow::before, Name::MdxExpressionFlowAfter => construct::mdx_expression_flow::after, Name::MdxExpressionFlowEnd => construct::mdx_expression_flow::end, + Name::MdxExpressionFlowNok => construct::mdx_expression_flow::nok, Name::MdxExpressionTextStart => construct::mdx_expression_text::start, Name::MdxExpressionTextAfter => construct::mdx_expression_text::after, diff --git a/tests/mdx_jsx_flow.rs b/tests/mdx_jsx_flow.rs index 3b2c79d..5adf41b 100644 --- a/tests/mdx_jsx_flow.rs +++ b/tests/mdx_jsx_flow.rs @@ -1,3 +1,4 @@ +mod test_utils; use markdown::{ mdast::{List, ListItem, MdxJsxFlowElement, Node, Paragraph, Root, Text}, to_html_with_options, to_mdast, @@ -5,6 +6,7 @@ use markdown::{ Constructs, Options, ParseOptions, }; use pretty_assertions::assert_eq; +use test_utils::swc::{parse_esm, parse_expression}; #[test] fn mdx_jsx_flow_agnostic() -> Result<(), String> { @@ -226,3 +228,116 @@ fn mdx_jsx_flow_essence() -> Result<(), String> { Ok(()) } + +// Flow is mostly the same as `text`, so we only test the relevant +// differences. +#[test] +fn mdx_jsx_flow_interleaving_with_expressions() -> Result<(), String> { + let mdx = Options { + parse: ParseOptions::mdx(), + ..Default::default() + }; + let swc = Options { + parse: ParseOptions { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Default::default() + }, + ..Default::default() + }; + + assert_eq!( + to_html_with_options("
\n{1}\n
", &mdx)?, + "", + "should support tags and expressions (unaware)" + ); + + assert_eq!( + to_html_with_options("
\n{'}'}\n
", &swc)?, + "", + "should support tags and expressions (aware)" + ); + + assert_eq!( + to_html_with_options("x{1}", &swc)?, + "

x

", + "should support tags and expressions with text before (text)" + ); + + assert_eq!( + to_html_with_options("x{1}", &swc)?, + "

x

", + "should support tags and expressions with text between, early (text)" + ); + + assert_eq!( + to_html_with_options("{1}x", &swc)?, + "

x

", + "should support tags and expressions with text between, late (text)" + ); + + assert_eq!( + to_html_with_options("{1}x", &swc)?, + "

x

", + "should support tags and expressions with text after (text)" + ); + + assert_eq!( + to_html_with_options("{1}", &swc)?, + "", + "should support a tag and then an expression (flow)" + ); + + assert_eq!( + to_html_with_options("{1}x", &swc)?, + "

x

", + "should support a tag, an expression, then text (text)" + ); + + assert_eq!( + to_html_with_options("x{1}", &swc)?, + "

x

", + "should support text, a tag, then an expression (text)" + ); + + assert_eq!( + to_html_with_options("{1}", &swc)?, + "", + "should support an expression and then a tag (flow)" + ); + + assert_eq!( + to_html_with_options("{1}x", &swc)?, + "

x

", + "should support an expression, a tag, then text (text)" + ); + + assert_eq!( + to_html_with_options("x{1}", &swc)?, + "

x

", + "should support text, an expression, then a tag (text)" + ); + + assert_eq!( + to_html_with_options("{[\n'',\n{c:''}\n]}", &swc)?, + "", + "should nicely interleaf (micromark/micromark-extension-mdx-jsx#9)" + ); + + assert_eq!( + to_html_with_options( + " + + ", + &swc + )?, + "", + "should nicely interleaf (mdx-js/mdx#1945)" + ); + + Ok(()) +}