diff options
Diffstat (limited to '')
-rw-r--r-- | tests/code_indented.rs | 6 | ||||
-rw-r--r-- | tests/gfm_table.rs | 6 | ||||
-rw-r--r-- | tests/math_text.rs | 14 | ||||
-rw-r--r-- | tests/mdx_esm.rs | 241 | ||||
-rw-r--r-- | tests/mdx_expression_flow.rs | 281 | ||||
-rw-r--r-- | tests/mdx_expression_text.rs | 422 | ||||
-rw-r--r-- | tests/mdx_jsx_text.rs | 96 | ||||
-rw-r--r-- | tests/mdx_swc.rs | 47 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 245 |
9 files changed, 941 insertions, 417 deletions
diff --git a/tests/code_indented.rs b/tests/code_indented.rs index 29d8909..bf39fa3 100644 --- a/tests/code_indented.rs +++ b/tests/code_indented.rs @@ -167,7 +167,11 @@ fn code_indented() -> Result<(), String> { "a <?\n ?>", &Options { allow_dangerous_html: true, - ..off.clone() + constructs: Constructs { + code_indented: false, + ..Constructs::default() + }, + ..Options::default() } )?, "<p>a <?\n?></p>", diff --git a/tests/gfm_table.rs b/tests/gfm_table.rs index b7f884a..8c46a30 100644 --- a/tests/gfm_table.rs +++ b/tests/gfm_table.rs @@ -1037,7 +1037,8 @@ bar "###, &Options { allow_dangerous_html: true, - ..gfm.clone() + constructs: Constructs::gfm(), + ..Options::default() } )?, r###"<h1>Grave accents</h1> @@ -1345,7 +1346,8 @@ b "###, &Options { allow_dangerous_html: true, - ..gfm.clone() + constructs: Constructs::gfm(), + ..Options::default() } )?, r###"<h2>Blank line</h2> diff --git a/tests/math_text.rs b/tests/math_text.rs index dced393..7b53268 100644 --- a/tests/math_text.rs +++ b/tests/math_text.rs @@ -30,7 +30,12 @@ fn math_text() -> Result<(), String> { "$foo$ $$bar$$", &Options { math_text_single_dollar: false, - ..math.clone() + constructs: Constructs { + math_text: true, + math_flow: true, + ..Constructs::default() + }, + ..Options::default() } )?, "<p>$foo$ <code class=\"language-math math-inline\">bar</code></p>", @@ -133,7 +138,12 @@ fn math_text() -> Result<(), String> { &Options { allow_dangerous_html: true, allow_dangerous_protocol: true, - ..math.clone() + constructs: Constructs { + math_text: true, + math_flow: true, + ..Constructs::default() + }, + ..Options::default() } )?, "<p><a href=\"$\">$</p>", diff --git a/tests/mdx_esm.rs b/tests/mdx_esm.rs new file mode 100644 index 0000000..f1ea122 --- /dev/null +++ b/tests/mdx_esm.rs @@ -0,0 +1,241 @@ +extern crate micromark; +mod test_utils; +use micromark::{micromark_with_options, Constructs, Options}; +use pretty_assertions::assert_eq; +use test_utils::{parse_esm, parse_expression}; + +#[test] +fn mdx_esm() -> Result<(), String> { + let swc = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("import a from 'b'\n\nc", &swc)?, + "<p>c</p>", + "should support an import" + ); + + assert_eq!( + micromark_with_options("export default a\n\nb", &swc)?, + "<p>b</p>", + "should support an export" + ); + + assert_eq!( + micromark_with_options("impossible", &swc)?, + "<p>impossible</p>", + "should not support other keywords (`impossible`)" + ); + + assert_eq!( + micromark_with_options("exporting", &swc)?, + "<p>exporting</p>", + "should not support other keywords (`exporting`)" + ); + + assert_eq!( + micromark_with_options("import.", &swc)?, + "<p>import.</p>", + "should not support a non-whitespace after the keyword" + ); + + assert_eq!( + micromark_with_options("import('a')", &swc)?, + "<p>import('a')</p>", + "should not support a non-whitespace after the keyword (import-as-a-function)" + ); + + assert_eq!( + micromark_with_options(" import a from 'b'\n export default c", &swc)?, + "<p>import a from 'b'\nexport default c</p>", + "should not support an indent" + ); + + assert_eq!( + micromark_with_options("- import a from 'b'\n> export default c", &swc)?, + "<ul>\n<li>import a from 'b'</li>\n</ul>\n<blockquote>\n<p>export default c</p>\n</blockquote>", + "should not support keywords in containers" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\nexport default c", &swc)?, + "", + "should support imports and exports in the same “block”" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\n\nexport default c", &swc)?, + "", + "should support imports and exports in separate “blocks”" + ); + + assert_eq!( + micromark_with_options("a\n\nimport a from 'b'\n\nb\n\nexport default c", &swc)?, + "<p>a</p>\n<p>b</p>\n", + "should support imports and exports in between other constructs" + ); + + assert_eq!( + micromark_with_options("a\nimport a from 'b'\n\nb\nexport default c", &swc)?, + "<p>a\nimport a from 'b'</p>\n<p>b\nexport default c</p>", + "should not support import/exports when interrupting paragraphs" + ); + + assert_eq!( + micromark_with_options("import a", &swc).err().unwrap(), + "1:9: Could not parse esm with swc: Expected ',', got '<eof>'", + "should crash on invalid import/exports (1)" + ); + + assert_eq!( + micromark_with_options("import 1/1", &swc).err().unwrap(), + "1:9: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'", + "should crash on invalid import/exports (2)" + ); + + assert_eq!( + micromark_with_options("export {\n a\n} from 'b'\n\nc", &swc)?, + "<p>c</p>", + "should support line endings in import/exports" + ); + + assert_eq!( + micromark_with_options("export {\n\n a\n\n} from 'b'\n\nc", &swc)?, + "<p>c</p>", + "should support blank lines in import/exports" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\n*md*?", &swc) + .err() + .unwrap(), + "2:6: Could not parse esm with swc: Unexpected token `?`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "should crash on markdown after import/export w/o blank line" + ); + + assert_eq!( + micromark_with_options("export var a = 1\n// b\n/* c */\n\nd", &swc)?, + "<p>d</p>", + "should support comments in “blocks”" + ); + + assert_eq!( + micromark_with_options("export var a = 1\nvar b\n\nc", &swc) + .err() + .unwrap(), + "2:1: Unexpected statement in code: only import/exports are supported", + "should crash on other statements in “blocks”" + ); + + assert_eq!( + micromark_with_options("import ('a')\n\nb", &swc) + .err() + .unwrap(), + "1:1: Unexpected statement in code: only import/exports are supported", + "should crash on import-as-a-function with a space `import (x)`" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\nexport {a}\n\nc", &swc)?, + "<p>c</p>", + "should support a reexport from another import" + ); + + assert_eq!( + micromark_with_options("import a from 'b';\nexport {a};\n\nc", &swc)?, + "<p>c</p>", + "should support a reexport from another import w/ semicolons" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\nexport {a as default}\n\nc", &swc)?, + "<p>c</p>", + "should support a reexport default from another import" + ); + + assert_eq!( + micromark_with_options("export var a = () => <b />", &swc)?, + "", + "should support JSX by default" + ); + + assert_eq!( + micromark_with_options("export {a}\n", &swc)?, + "", + "should support EOF after EOL" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\n\nexport {a}\n\nc", &swc)?, + "<p>c</p>", + "should support a reexport from another esm block (1)" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\n\nexport {a}\n\n# c", &swc)?, + "<h1>c</h1>", + "should support a reexport from another esm block (2)" + ); + + let cases = vec![ + ("default", "import a from \"b\""), + ("whole", "import * as a from \"b\""), + ("destructuring", "import {a} from \"b\""), + ("destructuring and rename", "import {a as b} from \"c\""), + ("default and destructuring", "import a, {b as c} from \"d\""), + ("default and whole", "import a, * as b from \"c\""), + ("side-effects", "import \"a\""), + ]; + + for case in cases { + assert_eq!( + micromark_with_options(case.1, &swc)?, + "", + "should support imports: {}", + case.0 + ); + } + + let cases = vec![ + ("var", "export var a = \"\""), + ("const", "export const a = \"\""), + ("let", "export let a = \"\""), + ("multiple", "export var a, b"), + ("multiple w/ assignment", "export var a = \"a\", b = \"b\""), + ("function", "export function a() {}"), + ("class", "export class a {}"), + ("destructuring", "export var {a} = {}"), + ("rename destructuring", "export var {a: b} = {}"), + ("array destructuring", "export var [a] = []"), + ("default", "export default a = 1"), + ("default function", "export default function a() {}"), + ("default class", "export default class a {}"), + ("aggregate", "export * from \"a\""), + ("whole reexport", "export * as a from \"b\""), + ("reexport destructuring", "export {a} from \"b\""), + ( + "reexport destructuring w rename", + "export {a as b} from \"c\"", + ), + ("reexport as a default whole", "export {default} from \"b\""), + ( + "reexport default and non-default", + "export {default as a, b} from \"c\"", + ), + ]; + + for case in cases { + assert_eq!( + micromark_with_options(case.1, &swc)?, + "", + "should support exports: {}", + case.0 + ); + } + + Ok(()) +} diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index 2a66a9d..81a31a7 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -1,6 +1,8 @@ extern crate micromark; +mod test_utils; use micromark::{micromark_with_options, Constructs, Options}; use pretty_assertions::assert_eq; +use test_utils::{parse_esm, parse_expression}; #[test] fn mdx_expression_flow_agnostic() -> Result<(), String> { @@ -82,153 +84,132 @@ fn mdx_expression_flow_agnostic() -> Result<(), String> { Ok(()) } -// To do: swc. -// #[test] -// fn mdx_expression_flow_gnostic() -> Result<(), String> { -// assert_eq!( -// micromark_with_options("{a}", &swc), -// "", -// "should support an expression" -// ); - -// assert_eq!( -// micromark_with_options("{}", &swc)?, -// "", -// "should support an empty expression" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("{a", &swc); -// // }, -// // /Unexpected end of file in expression, expected a corresponding closing brace for `{`/, -// // "should crash if no closing brace is found (1)" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("{b { c }", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected content after expression/, -// // "should crash if no closing brace is found (2)" -// // ); - -// assert_eq!( -// micromark_with_options("{\n}\na", &swc)?, -// "<p>a</p>", -// "should support a line ending in an expression" -// ); - -// assert_eq!( -// micromark_with_options("{ a } \t\nb", &swc)?, -// "<p>b</p>", -// "should support expressions followed by spaces" -// ); - -// assert_eq!( -// micromark_with_options(" { a }\nb", &swc)?, -// "<p>b</p>", -// "should support expressions preceded by spaces" -// ); - -// assert_eq!( -// micromark_with_options(" {`\n a\n `}", &swc)?, -// "", -// "should support indented expressions" -// ); - -// assert_eq!( -// micromark_with_options("a{(b)}c", &swc)?, -// "<p>ac</p>", -// "should support expressions padded w/ parens" -// ); - -// assert_eq!( -// micromark_with_options("a{/* b */ ( (c) /* d */ + (e) )}f", &swc)?, -// "<p>af</p>", -// "should support expressions padded w/ parens and comments" -// ); - -// Ok(()) -// } - -// To do: move to JSX, actually test spread in expressions? -// To do: swc. -// #[test] -// fn mdx_expression_spread() -> Result<(), String> { -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {b} c", &swc); -// // }, -// // /Unexpected `Property` in code: only spread elements are supported/, -// // "should crash if not a spread" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {...?} c", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on an incorrect spread" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {...b,c} d", &swc); -// // }, -// // /Unexpected extra content in spread: only a single spread is supported/, -// // "should crash if a spread and other things" -// // ); - -// assert_eq!( -// micromark_with_options("a {} b", &swc)?, -// "<p>a b</p>", -// "should support an empty spread" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {} b", &swc); -// // }, -// // /Unexpected empty expression/, -// // "should crash on an empty spread w/ `allowEmpty: false`" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("{a=b}", &swc); -// // }, -// // /Could not parse expression with swc: Shorthand property assignments are valid only in destructuring patterns/, -// // "should crash if not a spread w/ `allowEmpty`" -// // ); - -// assert_eq!( -// micromark_with_options("a {/* b */} c", &swc)?, -// "<p>a c</p>", -// "should support a comment spread" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {/* b */} c", &swc); -// // }, -// // /Unexpected empty expression/, -// // "should crash on a comment spread w/ `allowEmpty: false`" -// // ); - -// assert_eq!( -// micromark_with_options("a {...b} c", &swc)?, -// "<p>a c</p>", -// "should support a spread" -// ); - -// Ok(()) -// } +#[test] +fn mdx_expression_flow_gnostic() -> Result<(), String> { + let swc = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("{a}", &swc)?, + "", + "should support an expression" + ); + + assert_eq!( + micromark_with_options("{}", &swc)?, + "", + "should support an empty expression" + ); + + assert_eq!( + micromark_with_options("{a", &swc).err().unwrap(), + "1:3: Unexpected end of file in expression, expected a corresponding closing brace for `{`", + "should crash if no closing brace is found (1)" + ); + + assert_eq!( + micromark_with_options("{b { c }", &swc).err().unwrap(), + "1:4: Could not parse expression with swc: Unexpected content after expression", + "should crash if no closing brace is found (2)" + ); + + assert_eq!( + micromark_with_options("{\n}\na", &swc)?, + "<p>a</p>", + "should support a line ending in an expression" + ); + + assert_eq!( + micromark_with_options("{ a } \t\nb", &swc)?, + "<p>b</p>", + "should support expressions followed by spaces" + ); + + assert_eq!( + micromark_with_options(" { a }\nb", &swc)?, + "<p>b</p>", + "should support expressions preceded by spaces" + ); + + assert_eq!( + micromark_with_options(" {`\n a\n `}", &swc)?, + "", + "should support indented expressions" + ); + + assert_eq!( + micromark_with_options("a{(b)}c", &swc)?, + "<p>ac</p>", + "should support expressions padded w/ parens" + ); + + assert_eq!( + micromark_with_options("a{/* b */ ( (c) /* d */ + (e) )}f", &swc)?, + "<p>af</p>", + "should support expressions padded w/ parens and comments" + ); + + Ok(()) +} + +#[test] +fn mdx_expression_spread() -> Result<(), String> { + let swc = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("<a {...b} />", &swc)?, + "", + "should support spreads for attribute expression" + ); + + assert_eq!( + micromark_with_options("<a {b} />", &swc).err().unwrap(), + "1:5: Expected a single spread value, such as `...x`", + "should crash if not a spread" + ); + + assert_eq!( + micromark_with_options("<a {...?} />", &swc).err().unwrap(), + "1:13: Could not parse expression with swc: Unexpected token `?`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "should crash on an incorrect spread" + ); + + assert_eq!( + micromark_with_options("<a {...b,c} d>", &swc) + .err() + .unwrap(), + "1:5: Expected a single spread value, such as `...x`", + "should crash if a spread and other things" + ); + + assert_eq!( + micromark_with_options("<a {} />", &swc).err().unwrap(), + "1:5: Expected a single spread value, such as `...x`", + "should crash on an empty spread" + ); + + assert_eq!( + micromark_with_options("<a {a=b} />", &swc).err().unwrap(), + "1:12: Could not parse expression with swc: assignment property is invalid syntax", + "should crash if not an identifier" + ); + + assert_eq!( + micromark_with_options("<a {/* b */} />", &swc) + .err() + .unwrap(), + "1:5: Expected a single spread value, such as `...x`", + "should crash on a comment spread" + ); + + Ok(()) +} diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index b42faf2..3a48965 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -1,147 +1,144 @@ extern crate micromark; +mod test_utils; use micromark::{micromark_with_options, Constructs, Options}; use pretty_assertions::assert_eq; +use test_utils::{parse_esm, parse_expression}; -// To do: swc. -// #[test] -// fn mdx_expression_text_gnostic_core() -> Result<(), String> { -// assert_eq!( -// micromark_with_options("a {} b", &swc)?, -// "<p>a b</p>", -// "should support an empty expression (1)" -// ); - -// assert_eq!( -// micromark_with_options("a { \t\r\n} b", &swc)?, -// "<p>a b</p>", -// "should support an empty expression (2)" -// ); - -// assert_eq!( -// micromark_with_options("a {/**/} b", &swc)?, -// "<p>a b</p>", -// "should support a multiline comment (1)" -// ); - -// assert_eq!( -// micromark_with_options("a { /*\n*/\t} b", &swc)?, -// "<p>a b</p>", -// "should support a multiline comment (2)" -// ); - -// assert_eq!( -// micromark_with_options("a {/*b*//*c*/} d", &swc)?, -// "<p>a d</p>", -// "should support a multiline comment (3)" -// ); - -// assert_eq!( -// micromark_with_options("a {b/*c*/} d", &swc)?, -// "<p>a d</p>", -// "should support a multiline comment (4)" -// ); - -// assert_eq!( -// micromark_with_options("a {/*b*/c} d", &swc)?, -// "<p>a d</p>", -// "should support a multiline comment (4)" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {//} b", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on an incorrect line comment (1)" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a { // b } c", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on an incorrect line comment (2)" -// // ); - -// assert_eq!( -// micromark_with_options("a {//\n} b", &swc)?, -// "<p>a b</p>", -// "should support a line comment followed by a line ending" -// ); - -// assert_eq!( -// micromark_with_options("a {// b\nd} d", &swc)?, -// "<p>a d</p>", -// "should support a line comment followed by a line ending and an expression" -// ); - -// assert_eq!( -// micromark_with_options("a {b// c\n} d", &swc)?, -// "<p>a d</p>", -// "should support an expression followed by a line comment and a line ending" -// ); - -// assert_eq!( -// micromark_with_options("a {/*b*/ // c\n} d", &swc)?, -// "<p>a d</p>", -// "should support comments (1)" -// ); - -// assert_eq!( -// micromark_with_options("a {b.c} d", &swc)?, -// "<p>a d</p>", -// "should support expression statements (1)" -// ); - -// assert_eq!( -// micromark_with_options("a {1 + 1} b", &swc)?, -// "<p>a b</p>", -// "should support expression statements (2)" -// ); - -// assert_eq!( -// micromark_with_options("a {function () {}} b", &swc)?, -// "<p>a b</p>", -// "should support expression statements (3)" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {var b = \"c\"} d", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on non-expressions" -// // ); - -// assert_eq!( -// micromark_with_options("> a {\n> b} c", &swc)?, -// "<blockquote>\n<p>a c</p>\n</blockquote>", -// "should support expressions in containers" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("> a {\n> b<} c", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on incorrect expressions in containers (1)" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("> a {\n> b\n> c} d", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected content after expression/, -// // "should crash on incorrect expressions in containers (2)" -// // ); - -// Ok(()) -// } +#[test] +fn mdx_expression_text_gnostic_core() -> Result<(), String> { + let swc = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("a {} b", &swc)?, + "<p>a b</p>", + "should support an empty expression (1)" + ); + + assert_eq!( + micromark_with_options("a { \t\r\n} b", &swc)?, + "<p>a b</p>", + "should support an empty expression (2)" + ); + + assert_eq!( + micromark_with_options("a {/**/} b", &swc)?, + "<p>a b</p>", + "should support a multiline comment (1)" + ); + + assert_eq!( + micromark_with_options("a { /*\n*/\t} b", &swc)?, + "<p>a b</p>", + "should support a multiline comment (2)" + ); + + assert_eq!( + micromark_with_options("a {/*b*//*c*/} d", &swc)?, + "<p>a d</p>", + "should support a multiline comment (3)" + ); + + assert_eq!( + micromark_with_options("a {b/*c*/} d", &swc)?, + "<p>a d</p>", + "should support a multiline comment (4)" + ); + + assert_eq!( + micromark_with_options("a {/*b*/c} d", &swc)?, + "<p>a d</p>", + "should support a multiline comment (4)" + ); + + assert_eq!( + micromark_with_options("a {//} b", &swc).err().unwrap(), + "1:4: Could not parse expression with swc: Unexpected eof", + "should crash on an incorrect line comment (1)" + ); + + assert_eq!( + micromark_with_options("a { // b } c", &swc).err().unwrap(), + "1:4: Could not parse expression with swc: Unexpected eof", + "should crash on an incorrect line comment (2)" + ); + + assert_eq!( + micromark_with_options("a {//\n} b", &swc)?, + "<p>a b</p>", + "should support a line comment followed by a line ending" + ); + + assert_eq!( + micromark_with_options("a {// b\nd} d", &swc)?, + "<p>a d</p>", + "should support a line comment followed by a line ending and an expression" + ); + + assert_eq!( + micromark_with_options("a {b// c\n} d", &swc)?, + "<p>a d</p>", + "should support an expression followed by a line comment and a line ending" + ); + + assert_eq!( + micromark_with_options("a {/*b*/ // c\n} d", &swc)?, + "<p>a d</p>", + "should support comments (1)" + ); + + assert_eq!( + micromark_with_options("a {b.c} d", &swc)?, + "<p>a d</p>", + "should support expression statements (1)" + ); + + assert_eq!( + micromark_with_options("a {1 + 1} b", &swc)?, + "<p>a b</p>", + "should support expression statements (2)" + ); + + assert_eq!( + micromark_with_options("a {function () {}} b", &swc)?, + "<p>a b</p>", + "should support expression statements (3)" + ); + + assert_eq!( + micromark_with_options("a {var b = \"c\"} d", &swc).err().unwrap(), + "1:7: Could not parse expression with swc: Unexpected token `var`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "should crash on non-expressions" + ); + + assert_eq!( + micromark_with_options("> a {\n> b} c", &swc)?, + "<blockquote>\n<p>a c</p>\n</blockquote>", + "should support expressions in containers" + ); + + assert_eq!( + micromark_with_options("> a {\n> b<} c", &swc) + .err() + .unwrap(), + "2:8: Could not parse expression with swc: Unexpected eof", + "should crash on incorrect expressions in containers (1)" + ); + + assert_eq!( + micromark_with_options("> a {\n> b\n> c} d", &swc) + .err() + .unwrap(), + "3:3: Could not parse expression with swc: Unexpected content after expression", + "should crash on incorrect expressions in containers (2)" + ); + + Ok(()) +} #[test] fn mdx_expression_text_agnostic() -> Result<(), String> { @@ -197,77 +194,74 @@ fn mdx_expression_text_agnostic() -> Result<(), String> { Ok(()) } -// // To do: swc. -// #[test] -// fn mdx_expression_text_gnostic() -> Result<(), String> { -// assert_eq!( -// micromark_with_options("a {b} c", &swc)?, -// "<p>a c</p>", -// "should support an expression" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {??} b", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected token/, -// // "should crash on an incorrect expression" -// // ); - -// assert_eq!( -// micromark_with_options("a {} b", &swc)?, -// "<p>a b</p>", -// "should support an empty expression" -// ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {b c", &swc); -// // }, -// // /Unexpected end of file in expression, expected a corresponding closing brace for `{`/, -// // "should crash if no closing brace is found (1)" -// // ); - -// // To do: errors. -// // t.throws( -// // () => { -// // micromark_with_options("a {b { c } d", &swc); -// // }, -// // /Could not parse expression with swc: Unexpected content after expression/, -// // "should crash if no closing brace is found (2)" -// // ); - -// assert_eq!( -// micromark_with_options("a {\n} b", &swc)?, -// "<p>a b</p>", -// "should support a line ending in an expression" -// ); - -// assert_eq!( -// micromark_with_options("a } b", &swc)?, -// "<p>a } b</p>", -// "should support just a closing brace" -// ); - -// assert_eq!( -// micromark_with_options("{ a } b", &swc)?, -// "<p> b</p>", -// "should support expressions as the first thing when following by other things" -// ); - -// assert_eq!( -// micromark_with_options("a { /* { */ } b", &swc)?, -// "<p>a b</p>", -// "should support an unbalanced opening brace (if JS permits)" -// ); - -// assert_eq!( -// micromark_with_options("a { /* } */ } b", &swc)?, -// "<p>a b</p>", -// "should support an unbalanced closing brace (if JS permits)" -// ); - -// Ok(()) -// } +#[test] +fn mdx_expression_text_gnostic() -> Result<(), String> { + let swc = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("a {b} c", &swc)?, + "<p>a c</p>", + "should support an expression" + ); + + assert_eq!( + micromark_with_options("a {??} b", &swc).err().unwrap(), + "1:9: Could not parse expression with swc: Unexpected eof", + "should crash on an incorrect expression" + ); + + assert_eq!( + micromark_with_options("a {} b", &swc)?, + "<p>a b</p>", + "should support an empty expression" + ); + + assert_eq!( + micromark_with_options("a {b c", &swc).err().unwrap(), + "1:7: Unexpected end of file in expression, expected a corresponding closing brace for `{`", + "should crash if no closing brace is found (1)" + ); + + assert_eq!( + micromark_with_options("a {b { c } d", &swc).err().unwrap(), + "1:6: Could not parse expression with swc: Unexpected content after expression", + "should crash if no closing brace is found (2)" + ); + + assert_eq!( + micromark_with_options("a {\n} b", &swc)?, + "<p>a b</p>", + "should support a line ending in an expression" + ); + + assert_eq!( + micromark_with_options("a } b", &swc)?, + "<p>a } b</p>", + "should support just a closing brace" + ); + + assert_eq!( + micromark_with_options("{ a } b", &swc)?, + "<p> b</p>", + "should support expressions as the first thing when following by other things" + ); + + assert_eq!( + micromark_with_options("a { /* { */ } b", &swc)?, + "<p>a b</p>", + "should support an unbalanced opening brace (if JS permits)" + ); + + assert_eq!( + micromark_with_options("a { /* } */ } b", &swc)?, + "<p>a b</p>", + "should support an unbalanced closing brace (if JS permits)" + ); + + Ok(()) +} diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index cf507ee..be76d6f 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -1,6 +1,8 @@ extern crate micromark; +mod test_utils; use micromark::{micromark_with_options, Constructs, Options}; use pretty_assertions::assert_eq; +use test_utils::{parse_esm, parse_expression}; #[test] fn mdx_jsx_text_core() -> Result<(), String> { @@ -84,99 +86,97 @@ fn mdx_jsx_text_agnosic() -> Result<(), String> { #[test] fn mdx_jsx_text_gnostic() -> Result<(), String> { - let mdx = Options { + let swc = Options { constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), ..Options::default() }; assert_eq!( - micromark_with_options("a <b /> c", &mdx)?, + micromark_with_options("a <b /> c", &swc)?, "<p>a c</p>", "should support a self-closing element" ); assert_eq!( - micromark_with_options("a <b> c </b> d", &mdx)?, + micromark_with_options("a <b> c </b> d", &swc)?, "<p>a c d</p>", "should support a closed element" ); assert_eq!( - micromark_with_options("a <b> c", &mdx)?, + micromark_with_options("a <b> c", &swc)?, "<p>a c</p>", "should support an unclosed element" ); assert_eq!( - micromark_with_options("a <b {...c} /> d", &mdx)?, + micromark_with_options("a <b {...c} /> d", &swc)?, "<p>a d</p>", "should support an attribute expression" ); assert_eq!( - micromark_with_options("a <b {...{c: 1, d: Infinity, e: false}} /> f", &mdx)?, + micromark_with_options("a <b {...{c: 1, d: Infinity, e: false}} /> f", &swc)?, "<p>a f</p>", "should support more complex attribute expression (1)" ); assert_eq!( - micromark_with_options("a <b {...[1, Infinity, false]} /> d", &mdx)?, + micromark_with_options("a <b {...[1, Infinity, false]} /> d", &swc)?, "<p>a d</p>", "should support more complex attribute expression (2)" ); assert_eq!( - micromark_with_options("a <b c={1 + 1} /> d", &mdx)?, + micromark_with_options("a <b c={1 + 1} /> d", &swc)?, "<p>a d</p>", "should support an attribute value expression" ); assert_eq!( - micromark_with_options("a <b c={} /> d", &mdx) + micromark_with_options("a <b c={} /> d", &swc) .err() .unwrap(), - "1:9: Unexpected empty expression, expected a value between braces", + "1:15: Could not parse expression with swc: Unexpected eof", "should crash on an empty attribute value expression" ); - // To do: swc. - // assert_eq!( - // micromark_with_options("a <b {1 + 1} /> c", &swc) - // .err() - // .unwrap(), - // "Could not parse expression with acorn: Unexpected token", - // "should crash on a non-spread attribute expression" - // ); - - // To do: swc. - // assert_eq!( - // micromark_with_options("a <b c={?} /> d", &swc) - // .err() - // .unwrap(), - // "Could not parse expression with acorn: Unexpected token", - // "should crash on invalid JS in an attribute value expression" - // ); - - // To do: swc. - // assert_eq!( - // micromark_with_options("a <b {?} /> c", &swc) - // .err() - // .unwrap(), - // "Could not parse expression with acorn: Unexpected token", - // "should crash on invalid JS in an attribute expression" - // ); - - // To do: swc. - // assert_eq!( - // micromark_with_options("a <b{c=d}={}/> f", &swc) - // .err() - // .unwrap(), - // "Unexpected `ExpressionStatement` in code: expected an object spread", - // "should crash on invalid JS in an attribute expression (2)" - // ); - - assert_eq!( - micromark_with_options("a <b c={(2)} d={<e />} /> f", &mdx)?, + assert_eq!( + micromark_with_options("a <b {1 + 1} /> c", &swc) + .err() + .unwrap(), + "1:18: Could not parse expression with swc: Expected ',', got '}'", + "should crash on a non-spread attribute expression" + ); + + assert_eq!( + micromark_with_options("a <b c={?} /> d", &swc) + .err() + .unwrap(), + "1:16: Could not parse expression with swc: Unexpected token `?`. Expected this, import, async, function, [ for array literal, { for object literal, @ for decorator, function, class, null, true, false, number, bigint, string, regexp, ` for template literal, (, or an identifier", + "should crash on invalid JS in an attribute value expression" + ); + + assert_eq!( + micromark_with_options("a <b {?} /> c", &swc) + .err() + .unwrap(), + "1:14: Could not parse expression with swc: Unexpected token `?`. Expected identifier, string literal, numeric literal or [ for the computed key", + "should crash on invalid JS in an attribute expression" + ); + + assert_eq!( + micromark_with_options("a <b{c=d}={}/> f", &swc) + .err() + .unwrap(), + "1:6: Expected a single spread value, such as `...x`", + "should crash on invalid JS in an attribute expression (2)" + ); + + assert_eq!( + micromark_with_options("a <b c={(2)} d={<e />} /> f", &swc)?, "<p>a f</p>", "should support parenthesized expressions" ); diff --git a/tests/mdx_swc.rs b/tests/mdx_swc.rs new file mode 100644 index 0000000..c9a2a61 --- /dev/null +++ b/tests/mdx_swc.rs @@ -0,0 +1,47 @@ +extern crate micromark; +mod test_utils; +use micromark::{micromark_with_options, Constructs, Options}; +use pretty_assertions::assert_eq; +use test_utils::{parse_esm, parse_expression}; + +#[test] +fn mdx_swc() -> Result<(), String> { + let mdx = Options { + constructs: Constructs::mdx(), + mdx_esm_parse: Some(Box::new(parse_esm)), + mdx_expression_parse: Some(Box::new(parse_expression)), + ..Options::default() + }; + + assert_eq!( + micromark_with_options("{'}'}", &mdx)?, + "", + "should support JavaScript-aware flow expressions w/ `mdx_expression_parse`" + ); + + assert_eq!( + micromark_with_options("a {'}'} b", &mdx)?, + "<p>a b</p>", + "should support JavaScript-aware text expressions w/ `mdx_expression_parse`" + ); + + assert_eq!( + micromark_with_options("<a {...a/*}*/} />", &mdx)?, + "", + "should support JavaScript-aware attribute expressions w/ `mdx_expression_parse`" + ); + + assert_eq!( + micromark_with_options("<a b={'}'} />", &mdx)?, + "", + "should support JavaScript-aware attribute value expressions w/ `mdx_expression_parse`" + ); + + assert_eq!( + micromark_with_options("import a from 'b'\n\nexport {a}\n\n# c", &mdx)?, + "<h1>c</h1>", + "should support JavaScript-aware ESM w/ `mdx_esm_parse`" + ); + + Ok(()) +} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs new file mode 100644 index 0000000..10b9643 --- /dev/null +++ b/tests/test_utils/mod.rs @@ -0,0 +1,245 @@ +extern crate micromark; +extern crate swc_common; +extern crate swc_ecma_ast; +extern crate swc_ecma_parser; +use micromark::{MdxExpressionKind, MdxSignal}; +use swc_common::{source_map::Pos, BytePos, FileName, SourceFile, Spanned}; +use swc_ecma_ast::{EsVersion, Expr, Module}; +use swc_ecma_parser::{ + error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, +}; + +/// Parse ESM in MDX with SWC. +pub fn parse_esm(value: &str) -> MdxSignal { + let (file, syntax, version) = create_config(value.to_string()); + let mut errors = vec![]; + let result = parse_file_as_module(&file, syntax, version, None, &mut errors); + + match result { + Err(error) => swc_error_to_signal(&error, value.len(), 0, "esm"), + Ok(tree) => { + if errors.is_empty() { + check_esm_ast(tree) + } else { + if errors.len() > 1 { + println!("parse_esm: todo: multiple errors? {:?}", errors); + } + swc_error_to_signal(&errors[0], value.len(), 0, "esm") + } + } + } +} + +/// Parse expressions in MDX with SWC. +pub fn parse_expression(value: &str, kind: MdxExpressionKind) -> MdxSignal { + // Empty expressions are OK. + if matches!(kind, MdxExpressionKind::Expression) + && matches!(whitespace_and_comments(0, value), MdxSignal::Ok) + { + return MdxSignal::Ok; + } + + // For attribute expression, a spread is needed, for which we have to prefix + // and suffix the input. + // See `check_expression_ast` for how the AST is verified. + let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) { + ("({", "})") + } else { + ("", "") + }; + + let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); + let mut errors = vec![]; + let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); + + match result { + Err(error) => swc_error_to_signal(&error, value.len(), prefix.len(), "expression"), + Ok(tree) => { + if errors.is_empty() { + let place = fix_swc_position(tree.span().hi.to_usize(), prefix.len()); + let result = check_expression_ast(tree, kind); + if matches!(result, MdxSignal::Ok) { + whitespace_and_comments(place, value) + } else { + result + } + } else { + if errors.len() > 1 { + unreachable!("parse_expression: todo: multiple errors? {:?}", errors); + } + swc_error_to_signal(&errors[0], value.len(), prefix.len(), "expression") + } + } + } +} + +/// Check that the resulting AST of ESM is OK. +/// +/// This checks that only module declarations (import/exports) are used, not +/// statements. +fn check_esm_ast(tree: Module) -> MdxSignal { + let mut index = 0; + while index < tree.body.len() { + let node = &tree.body[index]; + + if !node.is_module_decl() { + let place = fix_swc_position(node.span().hi.to_usize(), 0); + return MdxSignal::Error( + "Unexpected statement in code: only import/exports are supported".to_string(), + place, + ); + } + + index += 1; + } + + MdxSignal::Ok +} + +/// Check that the resulting AST of an expressions is OK. +/// +/// This checks that attribute expressions are the expected spread. +fn check_expression_ast(tree: Box<Expr>, kind: MdxExpressionKind) -> MdxSignal { + if matches!(kind, MdxExpressionKind::AttributeExpression) + && tree + .unwrap_parens() + .as_object() + .and_then(|object| { + if object.props.len() == 1 { + object.props[0].as_spread() + } else { + None + } + }) + .is_none() + { + MdxSignal::Error( + "Expected a single spread value, such as `...x`".to_string(), + 0, + ) + } else { + MdxSignal::Ok + } +} + +/// Turn an SWC error into an `MdxSignal`. +/// +/// * If the error happens at `value_len`, yields `MdxSignal::Eof` +/// * Else, yields `MdxSignal::Error`. +fn swc_error_to_signal( + error: &SwcError, + value_len: usize, + prefix_len: usize, + name: &str, +) -> MdxSignal { + let message = error.kind().msg().to_string(); + let place = fix_swc_position(error.span().hi.to_usize(), prefix_len); + let message = format!("Could not parse {} with swc: {}", name, message); + + if place >= value_len { + MdxSignal::Eof(message) + } else { + MdxSignal::Error(message, place) + } +} + +/// Move past JavaScript whitespace (well, actually ASCII whitespace) and +/// comments. +/// +/// This is needed because for expressions, we use an API that parses up to +/// a valid expression, but there may be more expressions after it, which we +/// don’t alow. +fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal { + let bytes = value.as_bytes(); + let len = bytes.len(); + let mut in_multiline = false; + let mut in_line = false; + + while index < len { + // In a multiline comment: `/* a */`. + if in_multiline { + if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' { + index += 1; + in_multiline = false; + } + } + // In a line comment: `// a`. + else if in_line { + if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' { + index += 1; + in_line = false; + } else if bytes[index] == b'\r' || bytes[index] == b'\n' { + in_line = false; + } + } + // Not in a comment, opening a multiline comment: `/* a */`. + else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' { + index += 1; + in_multiline = true; + } + // Not in a comment, opening a line comment: `// a`. + else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' { + index += 1; + in_line = true; + } + // Outside comment, whitespace. + else if bytes[index].is_ascii_whitespace() { + // Fine! + } + // Outside comment, not whitespace. + else { + return MdxSignal::Error( + "Could not parse expression with swc: Unexpected content after expression" + .to_string(), + index, + ); + } + + index += 1; + } + + if in_multiline { + MdxSignal::Error( + "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".to_string(), + index, + ) + } else if in_line { + // EOF instead of EOL is specifically not allowed, because that would + // mean the closing brace is on the commented-out line + MdxSignal::Error( + "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".to_string(), + index, + ) + } else { + MdxSignal::Ok + } +} + +/// Create configuration for SWC, shared between ESM and expressions. +/// +/// This enables modern JavaScript (ES2022) + JSX. +fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { + ( + // File. + SourceFile::new( + FileName::Anon, + false, + FileName::Anon, + source, + BytePos::from_usize(1), + ), + // Syntax. + Syntax::Es(EsConfig { + jsx: true, + ..EsConfig::default() + }), + // Version. + EsVersion::Es2022, + ) +} + +/// Turn an SWC byte position from a resulting AST to an offset in the original +/// input string. +fn fix_swc_position(index: usize, prefix_len: usize) -> usize { + index - 1 - prefix_len +} |