diff options
Diffstat (limited to '')
-rw-r--r-- | tests/mdx_expression_flow.rs | 10 | ||||
-rw-r--r-- | tests/mdx_expression_text.rs | 4 | ||||
-rw-r--r-- | tests/mdx_jsx_text.rs | 2 | ||||
-rw-r--r-- | tests/test_utils/hast.rs | 508 | ||||
-rw-r--r-- | tests/test_utils/hast_util_to_swc.rs | 781 | ||||
-rw-r--r-- | tests/test_utils/mdast_util_to_hast.rs | 1275 | ||||
-rw-r--r-- | tests/test_utils/mdx.rs | 221 | ||||
-rw-r--r-- | tests/test_utils/mdx_plugin_recma_document.rs | 662 | ||||
-rw-r--r-- | tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs | 1165 | ||||
-rw-r--r-- | tests/test_utils/mod.rs | 6 | ||||
-rw-r--r-- | tests/test_utils/swc.rs | 383 | ||||
-rw-r--r-- | tests/test_utils/swc_utils.rs | 226 | ||||
-rw-r--r-- | tests/xxx_hast_util_to_swc.rs | 717 | ||||
-rw-r--r-- | tests/xxx_mdast_util_to_hast.rs | 1566 | ||||
-rw-r--r-- | tests/xxx_mdx.rs | 150 | ||||
-rw-r--r-- | tests/xxx_mdx_plugin_recma_document.rs | 207 | ||||
-rw-r--r-- | tests/xxx_mdx_plugin_recma_jsx_rewrite.rs | 375 |
17 files changed, 158 insertions, 8100 deletions
diff --git a/tests/mdx_expression_flow.rs b/tests/mdx_expression_flow.rs index b5e0c4f..e549080 100644 --- a/tests/mdx_expression_flow.rs +++ b/tests/mdx_expression_flow.rs @@ -176,7 +176,7 @@ fn mdx_expression_flow_gnostic() -> Result<(), String> { assert_eq!( to_html_with_options("{b { c }", &swc).err().unwrap(), - "1:4: Could not parse expression with swc: Unexpected content after expression", + "1:9: Could not parse expression with swc: Unexpected content after expression", "should crash if no closing brace is found (2)" ); @@ -239,7 +239,7 @@ fn mdx_expression_spread() -> Result<(), String> { assert_eq!( to_html_with_options("<a {b} />", &swc).err().unwrap(), - "1:5: Expected a single spread value, such as `...x`", + "1:5: Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)", "should crash if not a spread" ); @@ -251,13 +251,13 @@ fn mdx_expression_spread() -> Result<(), String> { assert_eq!( to_html_with_options("<a {...b,c} d>", &swc).err().unwrap(), - "1:5: Expected a single spread value, such as `...x`", + "1:5: Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)", "should crash if a spread and other things" ); assert_eq!( to_html_with_options("<a {} />", &swc).err().unwrap(), - "1:5: Expected a single spread value, such as `...x`", + "1:9: Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)", "should crash on an empty spread" ); @@ -269,7 +269,7 @@ fn mdx_expression_spread() -> Result<(), String> { assert_eq!( to_html_with_options("<a {/* b */} />", &swc).err().unwrap(), - "1:5: Expected a single spread value, such as `...x`", + "1:5: Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)", "should crash on a comment spread" ); diff --git a/tests/mdx_expression_text.rs b/tests/mdx_expression_text.rs index 0fccb0f..0d25105 100644 --- a/tests/mdx_expression_text.rs +++ b/tests/mdx_expression_text.rs @@ -138,7 +138,7 @@ fn mdx_expression_text_gnostic_core() -> Result<(), String> { to_html_with_options("> a {\n> b\n> c} d", &swc) .err() .unwrap(), - "3:3: Could not parse expression with swc: Unexpected content after expression", + "3:7: Could not parse expression with swc: Unexpected content after expression", "should crash on incorrect expressions in containers (2)" ); @@ -263,7 +263,7 @@ fn mdx_expression_text_gnostic() -> Result<(), String> { assert_eq!( to_html_with_options("a {b { c } d", &swc).err().unwrap(), - "1:6: Could not parse expression with swc: Unexpected content after expression", + "1:13: Could not parse expression with swc: Unexpected content after expression", "should crash if no closing brace is found (2)" ); diff --git a/tests/mdx_jsx_text.rs b/tests/mdx_jsx_text.rs index 0520ad2..e403e6d 100644 --- a/tests/mdx_jsx_text.rs +++ b/tests/mdx_jsx_text.rs @@ -608,7 +608,7 @@ fn mdx_jsx_text_gnostic() -> Result<(), String> { to_html_with_options("a <b{c=d}={}/> f", &swc) .err() .unwrap(), - "1:6: Expected a single spread value, such as `...x`", + "1:6: Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)", "should crash on invalid JS in an attribute expression (2)" ); diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs deleted file mode 100644 index 857df4d..0000000 --- a/tests/test_utils/hast.rs +++ /dev/null @@ -1,508 +0,0 @@ -//! HTML syntax tree: [hast][]. -//! -//! [hast]: https://github.com/syntax-tree/hast - -#![allow(dead_code)] -// ^-- To do: externalize. - -extern crate alloc; -use alloc::{ - fmt, - string::{String, ToString}, - vec::Vec, -}; -pub use markdown::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute, Stop}; -use markdown::unist::Position; - -/// Nodes. -#[derive(Clone, PartialEq, Eq)] -pub enum Node { - /// Root. - Root(Root), - /// Element. - Element(Element), - /// Document type. - Doctype(Doctype), - /// Comment. - Comment(Comment), - /// Text. - Text(Text), - - // MDX being passed through. - /// MDX: JSX element. - MdxJsxElement(MdxJsxElement), - /// MDX.js ESM. - MdxjsEsm(MdxjsEsm), - // MDX: expression. - MdxExpression(MdxExpression), -} - -impl fmt::Debug for Node { - // Debug the wrapped struct. - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Node::Root(x) => write!(f, "{:?}", x), - Node::Element(x) => write!(f, "{:?}", x), - Node::Doctype(x) => write!(f, "{:?}", x), - Node::Comment(x) => write!(f, "{:?}", x), - Node::Text(x) => write!(f, "{:?}", x), - Node::MdxJsxElement(x) => write!(f, "{:?}", x), - Node::MdxExpression(x) => write!(f, "{:?}", x), - Node::MdxjsEsm(x) => write!(f, "{:?}", x), - } - } -} - -fn children_to_string(children: &[Node]) -> String { - children.iter().map(ToString::to_string).collect() -} - -impl ToString for Node { - fn to_string(&self) -> String { - match self { - // Parents. - Node::Root(x) => children_to_string(&x.children), - Node::Element(x) => children_to_string(&x.children), - Node::MdxJsxElement(x) => children_to_string(&x.children), - // Literals. - Node::Comment(x) => x.value.clone(), - Node::Text(x) => x.value.clone(), - Node::MdxExpression(x) => x.value.clone(), - Node::MdxjsEsm(x) => x.value.clone(), - // Voids. - Node::Doctype(_) => "".into(), - } - } -} - -impl Node { - #[must_use] - pub fn children(&self) -> Option<&Vec<Node>> { - match self { - // Parent. - Node::Root(x) => Some(&x.children), - Node::Element(x) => Some(&x.children), - Node::MdxJsxElement(x) => Some(&x.children), - // Non-parent. - _ => None, - } - } - - pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> { - match self { - // Parent. - Node::Root(x) => Some(&mut x.children), - Node::Element(x) => Some(&mut x.children), - Node::MdxJsxElement(x) => Some(&mut x.children), - // Non-parent. - _ => None, - } - } - - pub fn position(&self) -> Option<&Position> { - match self { - Node::Root(x) => x.position.as_ref(), - Node::Element(x) => x.position.as_ref(), - Node::Doctype(x) => x.position.as_ref(), - Node::Comment(x) => x.position.as_ref(), - Node::Text(x) => x.position.as_ref(), - Node::MdxJsxElement(x) => x.position.as_ref(), - Node::MdxExpression(x) => x.position.as_ref(), - Node::MdxjsEsm(x) => x.position.as_ref(), - } - } - - pub fn position_mut(&mut self) -> Option<&mut Position> { - match self { - Node::Root(x) => x.position.as_mut(), - Node::Element(x) => x.position.as_mut(), - Node::Doctype(x) => x.position.as_mut(), - Node::Comment(x) => x.position.as_mut(), - Node::Text(x) => x.position.as_mut(), - Node::MdxJsxElement(x) => x.position.as_mut(), - Node::MdxExpression(x) => x.position.as_mut(), - Node::MdxjsEsm(x) => x.position.as_mut(), - } - } - - pub fn position_set(&mut self, position: Option<Position>) { - match self { - Node::Root(x) => x.position = position, - Node::Element(x) => x.position = position, - Node::Doctype(x) => x.position = position, - Node::Comment(x) => x.position = position, - Node::Text(x) => x.position = position, - Node::MdxJsxElement(x) => x.position = position, - Node::MdxExpression(x) => x.position = position, - Node::MdxjsEsm(x) => x.position = position, - } - } -} - -/// Document. -/// -/// ```html -/// > | a -/// ^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Root { - // Parent. - /// Content model. - pub children: Vec<Node>, - /// Positional info. - pub position: Option<Position>, -} - -/// Document type. -/// -/// ```html -/// > | <!doctype html> -/// ^^^^^^^^^^^^^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Element { - pub tag_name: String, - pub properties: Vec<(String, PropertyValue)>, - // Parent. - pub children: Vec<Node>, - /// Positional info. - pub position: Option<Position>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum PropertyValue { - Boolean(bool), - String(String), - CommaSeparated(Vec<String>), - SpaceSeparated(Vec<String>), -} - -/// Document type. -/// -/// ```html -/// > | <!doctype html> -/// ^^^^^^^^^^^^^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Doctype { - // Void. - /// Positional info. - pub position: Option<Position>, -} - -/// Comment. -/// -/// ```html -/// > | <!-- a --> -/// ^^^^^^^^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Comment { - // Text. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option<Position>, -} - -/// Text. -/// -/// ```html -/// > | a -/// ^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Text { - // Text. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option<Position>, -} - -/// MDX: JSX element. -/// -/// ```markdown -/// > | <a /> -/// ^^^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MdxJsxElement { - // JSX element. - /// Name. - /// - /// Fragments have no name. - pub name: Option<String>, - /// Attributes. - pub attributes: Vec<AttributeContent>, - // Parent. - /// Content model. - pub children: Vec<Node>, - /// Positional info. - pub position: Option<Position>, -} - -/// MDX: expression. -/// -/// ```markdown -/// > | {a} -/// ^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MdxExpression { - // Literal. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option<Position>, - - // Custom data on where each slice of `value` came from. - pub stops: Vec<Stop>, -} - -/// MDX: ESM. -/// -/// ```markdown -/// > | import a from 'b' -/// ^^^^^^^^^^^^^^^^^ -/// ``` -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct MdxjsEsm { - // Literal. - /// Content model. - pub value: String, - /// Positional info. - pub position: Option<Position>, - - // Custom data on where each slice of `value` came from. - pub stops: Vec<Stop>, -} - -#[cfg(test)] -mod tests { - use super::*; - use alloc::{format, string::ToString, vec}; - use markdown::unist::Position; - - // Literals. - - #[test] - fn text() { - let mut node = Node::Text(Text { - value: "a".into(), - position: None, - }); - - assert_eq!( - format!("{:?}", node), - "Text { value: \"a\", position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "a", "should support `ToString`"); - assert_eq!(node.children_mut(), None, "should support `children_mut`"); - assert_eq!(node.children(), None, "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "Text { value: \"a\", position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } - - #[test] - fn comment() { - let mut node = Node::Comment(Comment { - value: "a".into(), - position: None, - }); - - assert_eq!( - format!("{:?}", node), - "Comment { value: \"a\", position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "a", "should support `ToString`"); - assert_eq!(node.children_mut(), None, "should support `children_mut`"); - assert_eq!(node.children(), None, "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "Comment { value: \"a\", position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } - - #[test] - fn mdx_expression() { - let mut node = Node::MdxExpression(MdxExpression { - value: "a".into(), - stops: vec![], - position: None, - }); - - assert_eq!( - format!("{:?}", node), - "MdxExpression { value: \"a\", position: None, stops: [] }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "a", "should support `ToString`"); - assert_eq!(node.children_mut(), None, "should support `children_mut`"); - assert_eq!(node.children(), None, "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "MdxExpression { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }", - "should support `position_set`" - ); - } - - #[test] - fn mdxjs_esm() { - let mut node = Node::MdxjsEsm(MdxjsEsm { - value: "a".into(), - stops: vec![], - position: None, - }); - - assert_eq!( - format!("{:?}", node), - "MdxjsEsm { value: \"a\", position: None, stops: [] }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "a", "should support `ToString`"); - assert_eq!(node.children_mut(), None, "should support `children_mut`"); - assert_eq!(node.children(), None, "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "MdxjsEsm { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }", - "should support `position_set`" - ); - } - - // Voids. - - #[test] - fn doctype() { - let mut node = Node::Doctype(Doctype { position: None }); - - assert_eq!( - format!("{:?}", node), - "Doctype { position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "", "should support `ToString`"); - assert_eq!(node.children_mut(), None, "should support `children_mut`"); - assert_eq!(node.children(), None, "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "Doctype { position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } - - // Parents. - - #[test] - fn root() { - let mut node = Node::Root(Root { - position: None, - children: vec![], - }); - - assert_eq!( - format!("{:?}", node), - "Root { children: [], position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "", "should support `ToString`"); - assert_eq!( - node.children_mut(), - Some(&mut vec![]), - "should support `children_mut`" - ); - assert_eq!(node.children(), Some(&vec![]), "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "Root { children: [], position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } - - #[test] - fn element() { - let mut node = Node::Element(Element { - tag_name: "a".into(), - properties: vec![], - position: None, - children: vec![], - }); - - assert_eq!( - format!("{:?}", node), - "Element { tag_name: \"a\", properties: [], children: [], position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "", "should support `ToString`"); - assert_eq!( - node.children_mut(), - Some(&mut vec![]), - "should support `children_mut`" - ); - assert_eq!(node.children(), Some(&vec![]), "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "Element { tag_name: \"a\", properties: [], children: [], position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } - - #[test] - fn mdx_jsx_element() { - let mut node = Node::MdxJsxElement(MdxJsxElement { - name: None, - attributes: vec![], - position: None, - children: vec![], - }); - - assert_eq!( - format!("{:?}", node), - "MdxJsxElement { name: None, attributes: [], children: [], position: None }", - "should support `Debug`" - ); - assert_eq!(node.to_string(), "", "should support `ToString`"); - assert_eq!( - node.children_mut(), - Some(&mut vec![]), - "should support `children_mut`" - ); - assert_eq!(node.children(), Some(&vec![]), "should support `children`"); - assert_eq!(node.position(), None, "should support `position`"); - assert_eq!(node.position_mut(), None, "should support `position`"); - node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); - assert_eq!( - format!("{:?}", node), - "MdxJsxElement { name: None, attributes: [], children: [], position: Some(1:1-1:2 (0-1)) }", - "should support `position_set`" - ); - } -} diff --git a/tests/test_utils/hast_util_to_swc.rs b/tests/test_utils/hast_util_to_swc.rs deleted file mode 100644 index ea7ffd1..0000000 --- a/tests/test_utils/hast_util_to_swc.rs +++ /dev/null @@ -1,781 +0,0 @@ -//! Turn an HTML AST into a JavaScript AST. -//! -//! Port of <https://github.com/syntax-tree/hast-util-to-estree>, by the same -//! author: -//! -//! (The MIT License) -//! -//! Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com> -//! -//! Permission is hereby granted, free of charge, to any person obtaining -//! a copy of this software and associated documentation files (the -//! 'Software'), to deal in the Software without restriction, including -//! without limitation the rights to use, copy, modify, merge, publish, -//! distribute, sublicense, and/or sell copies of the Software, and to -//! permit persons to whom the Software is furnished to do so, subject to -//! the following conditions: -//! -//! The above copyright notice and this permission notice shall be -//! included in all copies or substantial portions of the Software. -//! -//! THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -//! EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -//! MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -//! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -//! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -//! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -//! SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -use crate::test_utils::{ - hast, - swc::{parse_esm_to_tree, parse_expression_to_tree}, - swc_utils::{create_ident, position_to_span}, -}; -use core::str; -use markdown::{Location, MdxExpressionKind}; - -/// Result. -#[derive(Debug, PartialEq, Eq)] -pub struct Program { - pub path: Option<String>, - /// JS AST. - pub module: swc_ecma_ast::Module, - /// Comments relating to AST. - pub comments: Vec<swc_common::comments::Comment>, -} - -/// Whether we’re in HTML or SVG. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Space { - Html, - Svg, -} - -#[derive(Debug)] -struct Context<'a> { - /// Whether we’re in HTML or SVG. - /// - /// Not used yet, likely useful in the future. - space: Space, - /// Comments we gather. - comments: Vec<swc_common::comments::Comment>, - /// Declarations and stuff. - esm: Vec<swc_ecma_ast::ModuleItem>, - /// Optional way to turn relative positions into points. - location: Option<&'a Location>, -} - -#[allow(dead_code)] -pub fn hast_util_to_swc( - tree: &hast::Node, - path: Option<String>, - location: Option<&Location>, -) -> Result<Program, String> { - let mut context = Context { - space: Space::Html, - comments: vec![], - esm: vec![], - location, - }; - let expr = match one(&mut context, tree)? { - Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { - Some(swc_ecma_ast::Expr::JSXFragment(x)) - } - Some(swc_ecma_ast::JSXElementChild::JSXElement(x)) => { - Some(swc_ecma_ast::Expr::JSXElement(x)) - } - Some(child) => Some(swc_ecma_ast::Expr::JSXFragment(create_fragment( - vec![child], - tree, - ))), - None => None, - }; - - // Add the ESM. - let mut module = swc_ecma_ast::Module { - shebang: None, - body: context.esm, - span: position_to_span(tree.position()), - }; - - // We have some content, wrap it. - if let Some(expr) = expr { - module - .body - .push(swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(expr), - span: swc_common::DUMMY_SP, - }, - ))); - } - - Ok(Program { - path, - module, - comments: context.comments, - }) -} - -/// Transform one node. -fn one( - context: &mut Context, - node: &hast::Node, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let value = match node { - hast::Node::Comment(x) => Some(transform_comment(context, node, x)), - hast::Node::Element(x) => transform_element(context, node, x)?, - hast::Node::MdxJsxElement(x) => transform_mdx_jsx_element(context, node, x)?, - hast::Node::MdxExpression(x) => transform_mdx_expression(context, node, x)?, - hast::Node::MdxjsEsm(x) => transform_mdxjs_esm(context, node, x)?, - hast::Node::Root(x) => transform_root(context, node, x)?, - hast::Node::Text(x) => transform_text(context, node, x), - // Ignore: - hast::Node::Doctype(_) => None, - }; - Ok(value) -} - -/// Transform children of `parent`. -fn all( - context: &mut Context, - parent: &hast::Node, -) -> Result<Vec<swc_ecma_ast::JSXElementChild>, String> { - let mut result = vec![]; - if let Some(children) = parent.children() { - let mut index = 0; - while index < children.len() { - let child = &children[index]; - // To do: remove line endings between table elements? - // <https://github.com/syntax-tree/hast-util-to-estree/blob/6c45f166d106ea3a165c14ec50c35ed190055e65/lib/index.js> - if let Some(child) = one(context, child)? { - result.push(child); - } - index += 1; - } - } - - Ok(result) -} - -/// [`Comment`][hast::Comment]. -fn transform_comment( - context: &mut Context, - node: &hast::Node, - comment: &hast::Comment, -) -> swc_ecma_ast::JSXElementChild { - context.comments.push(swc_common::comments::Comment { - kind: swc_common::comments::CommentKind::Block, - text: comment.value.clone().into(), - span: position_to_span(node.position()), - }); - - // Might be useless. - // Might be useful when transforming to acorn/babel later. - // This is done in the JS version too: - // <https://github.com/syntax-tree/hast-util-to-estree/blob/6c45f166d106ea3a165c14ec50c35ed190055e65/lib/index.js#L168> - swc_ecma_ast::JSXElementChild::JSXExprContainer(swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::JSXEmptyExpr(swc_ecma_ast::JSXEmptyExpr { - span: position_to_span(node.position()), - }), - span: position_to_span(node.position()), - }) -} - -/// [`Element`][hast::Element]. -fn transform_element( - context: &mut Context, - node: &hast::Node, - element: &hast::Element, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let space = context.space; - - if space == Space::Html && element.tag_name == "svg" { - context.space = Space::Svg; - } - - let children = all(context, node)?; - - context.space = space; - - let mut attrs = vec![]; - - let mut index = 0; - while index < element.properties.len() { - let prop = &element.properties[index]; - - // To do: turn style props into objects. - let value = match &prop.1 { - hast::PropertyValue::Boolean(x) => { - // No value is same as `{true}` / Ignore `false`. - if *x { - None - } else { - index += 1; - continue; - } - } - hast::PropertyValue::String(x) => Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: x.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - hast::PropertyValue::CommaSeparated(x) => { - Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: x.join(", ").into(), - span: swc_common::DUMMY_SP, - raw: None, - })) - } - hast::PropertyValue::SpaceSeparated(x) => { - Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: x.join(" ").into(), - span: swc_common::DUMMY_SP, - raw: None, - })) - } - }; - - // Turn property case into either React-specific case, or HTML - // attribute case. - // To do: create a spread if this is an invalid attr name. - let attr_name = prop_to_attr_name(&prop.0); - - attrs.push(swc_ecma_ast::JSXAttrOrSpread::JSXAttr( - swc_ecma_ast::JSXAttr { - name: create_jsx_attr_name(&attr_name), - value: value.map(swc_ecma_ast::JSXAttrValue::Lit), - span: swc_common::DUMMY_SP, - }, - )); - - index += 1; - } - - Ok(Some(swc_ecma_ast::JSXElementChild::JSXElement( - create_element(&element.tag_name, attrs, children, node), - ))) -} - -/// [`MdxJsxElement`][hast::MdxJsxElement]. -fn transform_mdx_jsx_element( - context: &mut Context, - node: &hast::Node, - element: &hast::MdxJsxElement, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let space = context.space; - - if let Some(name) = &element.name { - if space == Space::Html && name == "svg" { - context.space = Space::Svg; - } - } - - let children = all(context, node)?; - - context.space = space; - - let mut attrs = vec![]; - let mut index = 0; - - while index < element.attributes.len() { - let attr = match &element.attributes[index] { - hast::AttributeContent::Property(prop) => { - let value = match prop.value.as_ref() { - Some(hast::AttributeValue::Literal(x)) => { - Some(swc_ecma_ast::JSXAttrValue::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: x.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))) - } - Some(hast::AttributeValue::Expression(value, stops)) => { - Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( - value, - &MdxExpressionKind::AttributeValueExpression, - stops, - context.location, - )?), - span: swc_common::DUMMY_SP, - }, - )) - } - None => None, - }; - - swc_ecma_ast::JSXAttrOrSpread::JSXAttr(swc_ecma_ast::JSXAttr { - span: swc_common::DUMMY_SP, - name: create_jsx_attr_name(&prop.name), - value, - }) - } - hast::AttributeContent::Expression(value, stops) => { - let expr = parse_expression_to_tree( - value, - &MdxExpressionKind::AttributeExpression, - stops, - context.location, - )?; - swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr, - }) - } - }; - - attrs.push(attr); - index += 1; - } - - Ok(Some(if let Some(name) = &element.name { - swc_ecma_ast::JSXElementChild::JSXElement(create_element(name, attrs, children, node)) - } else { - swc_ecma_ast::JSXElementChild::JSXFragment(create_fragment(children, node)) - })) -} - -/// [`MdxExpression`][hast::MdxExpression]. -fn transform_mdx_expression( - context: &mut Context, - node: &hast::Node, - expression: &hast::MdxExpression, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - Ok(Some(swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( - &expression.value, - &MdxExpressionKind::Expression, - &expression.stops, - context.location, - )?), - span: position_to_span(node.position()), - }, - ))) -} - -/// [`MdxjsEsm`][hast::MdxjsEsm]. -fn transform_mdxjs_esm( - context: &mut Context, - _node: &hast::Node, - esm: &hast::MdxjsEsm, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; - let mut index = 0; - - // To do: check that identifiers are not duplicated across esm blocks. - while index < module.body.len() { - if !matches!(module.body[index], swc_ecma_ast::ModuleItem::ModuleDecl(_)) { - return Err("Unexpected `statement` in code: only import/exports are supported".into()); - } - index += 1; - } - - context.esm.append(&mut module.body); - Ok(None) -} - -/// [`Root`][hast::Root]. -fn transform_root( - context: &mut Context, - node: &hast::Node, - _root: &hast::Root, -) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> { - let mut children = all(context, node)?; - let mut queue = vec![]; - let mut nodes = vec![]; - let mut seen = false; - - children.reverse(); - - // Remove initial/final whitespace. - while let Some(child) = children.pop() { - let mut stash = false; - - if let swc_ecma_ast::JSXElementChild::JSXExprContainer(container) = &child { - if let swc_ecma_ast::JSXExpr::Expr(expr) = &container.expr { - if let swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(str)) = (*expr).as_ref() { - if inter_element_whitespace(str.value.as_ref()) { - stash = true; - } - } - } - } - - if stash { - if seen { - queue.push(child); - } - } else { - if !queue.is_empty() { - nodes.append(&mut queue); - } - nodes.push(child); - seen = true; - } - } - - Ok(Some(swc_ecma_ast::JSXElementChild::JSXFragment( - create_fragment(nodes, node), - ))) -} - -/// [`Text`][hast::Text]. -fn transform_text( - _context: &mut Context, - node: &hast::Node, - text: &hast::Text, -) -> Option<swc_ecma_ast::JSXElementChild> { - if text.value.is_empty() { - None - } else { - Some(swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(Box::new(swc_ecma_ast::Expr::Lit( - swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: text.value.clone().into(), - span: position_to_span(node.position()), - raw: None, - }), - ))), - span: position_to_span(node.position()), - }, - )) - } -} - -/// Create an element. -/// -/// Creates a void one if there are no children. -fn create_element( - name: &str, - attrs: Vec<swc_ecma_ast::JSXAttrOrSpread>, - children: Vec<swc_ecma_ast::JSXElementChild>, - node: &hast::Node, -) -> Box<swc_ecma_ast::JSXElement> { - Box::new(swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: create_jsx_name(name), - attrs, - self_closing: children.is_empty(), - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: if children.is_empty() { - None - } else { - Some(swc_ecma_ast::JSXClosingElement { - name: create_jsx_name(name), - span: swc_common::DUMMY_SP, - }) - }, - children, - span: position_to_span(node.position()), - }) -} - -/// Create a fragment. -fn create_fragment( - children: Vec<swc_ecma_ast::JSXElementChild>, - node: &hast::Node, -) -> swc_ecma_ast::JSXFragment { - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children, - span: position_to_span(node.position()), - } -} - -/// Create a JSX element name. -fn create_jsx_name(name: &str) -> swc_ecma_ast::JSXElementName { - match parse_jsx_name(name) { - // `<a.b.c />` - // `<a.b />` - JsxName::Member(parts) => { - // Always two or more items. - let mut member = swc_ecma_ast::JSXMemberExpr { - obj: swc_ecma_ast::JSXObject::Ident(create_ident(parts[0])), - prop: create_ident(parts[1]), - }; - let mut index = 2; - while index < parts.len() { - member = swc_ecma_ast::JSXMemberExpr { - obj: swc_ecma_ast::JSXObject::JSXMemberExpr(Box::new(member)), - prop: create_ident(parts[index]), - }; - index += 1; - } - swc_ecma_ast::JSXElementName::JSXMemberExpr(member) - } - // `<a:b />` - JsxName::Namespace(ns, name) => { - swc_ecma_ast::JSXElementName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName { - ns: create_ident(ns), - name: create_ident(name), - }) - } - // `<a />` - JsxName::Normal(name) => swc_ecma_ast::JSXElementName::Ident(create_ident(name)), - } -} - -/// Create a JSX attribute name. -fn create_jsx_attr_name(name: &str) -> swc_ecma_ast::JSXAttrName { - match parse_jsx_name(name) { - JsxName::Member(_) => { - unreachable!("member expressions in attribute names are not supported") - } - // `<a b:c />` - JsxName::Namespace(ns, name) => { - swc_ecma_ast::JSXAttrName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName { - ns: create_ident(ns), - name: create_ident(name), - }) - } - // `<a b />` - JsxName::Normal(name) => swc_ecma_ast::JSXAttrName::Ident(create_ident(name)), - } -} - -fn inter_element_whitespace(value: &str) -> bool { - let bytes = value.as_bytes(); - let mut index = 0; - - while index < bytes.len() { - match bytes[index] { - b'\t' | 0x0C | b'\r' | b'\n' | b' ' => {} - _ => return false, - } - index += 1; - } - - true -} - -/// Different kinds of JSX names. -enum JsxName<'a> { - // `a.b.c` - Member(Vec<&'a str>), - // `a:b` - Namespace(&'a str, &'a str), - // `a` - Normal(&'a str), -} - -/// Parse a JSX name from a string. -fn parse_jsx_name(name: &str) -> JsxName { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - // `<a.b.c />` - if !parts.is_empty() { - parts.push(&name[start..]); - JsxName::Member(parts) - } - // `<a:b />` - else if let Some(colon) = bytes.iter().position(|d| matches!(d, b':')) { - JsxName::Namespace(&name[0..colon], &name[(colon + 1)..]) - } - // `<a />` - else { - JsxName::Normal(name) - } -} - -/// Turn a hast property into something that particularly React understands. -fn prop_to_attr_name(prop: &str) -> String { - // Arbitrary data props, kebab case them. - if prop.len() > 4 && prop.starts_with("data") { - // Assume like two dashes maybe? - let mut result = String::with_capacity(prop.len() + 2); - let bytes = prop.as_bytes(); - let mut index = 4; - let mut start = index; - let mut valid = true; - - result.push_str("data"); - - while index < bytes.len() { - let byte = bytes[index]; - let mut dash = index == 4; - - match byte { - b'A'..=b'Z' => dash = true, - b'-' | b'.' | b':' | b'0'..=b'9' | b'a'..=b'z' => {} - _ => { - valid = false; - break; - } - } - - if dash { - if start != index { - result.push_str(&prop[start..index]); - } - result.push('-'); - result.push(byte.to_ascii_lowercase().into()); - start = index + 1; - } - - index += 1; - } - - if valid { - result.push_str(&prop[start..]); - return result; - } - } - - // Look up if prop differs from attribute case. - // Unknown things are passed through. - PROP_TO_REACT_PROP - .iter() - .find(|d| d.0 == prop) - .or_else(|| PROP_TO_ATTR_EXCEPTIONS_SHARED.iter().find(|d| d.0 == prop)) - .map(|d| d.1.into()) - .unwrap_or_else(|| prop.into()) -} - -// Below data is generated with: -// -// Note: there are currently no HTML and SVG specific exceptions. -// If those would start appearing, the logic that uses these lists needs -// To support spaces. -// -// ```js -// import * as x from "property-information"; -// -// /** @type {Record<string, string>} */ -// let shared = {}; -// /** @type {Record<string, string>} */ -// let html = {}; -// /** @type {Record<string, string>} */ -// let svg = {}; -// -// Object.keys(x.html.property).forEach((prop) => { -// let attr = x.html.property[prop].attribute; -// if (!x.html.property[prop].space && prop !== attr) { -// html[prop] = attr; -// } -// }); -// -// Object.keys(x.svg.property).forEach((prop) => { -// let attr = x.svg.property[prop].attribute; -// if (!x.svg.property[prop].space && prop !== attr) { -// // Shared. -// if (prop in html && html[prop] === attr) { -// shared[prop] = attr; -// delete html[prop]; -// } else { -// svg[prop] = attr; -// } -// } -// }); -// -// /** @type {Array<[string, Array<[string, string]>]>} */ -// const all = [ -// ["PROP_TO_REACT_PROP", Object.entries(x.hastToReact)], -// ["PROP_TO_ATTR_EXCEPTIONS", Object.entries(shared)], -// ["PROP_TO_ATTR_EXCEPTIONS_HTML", Object.entries(html)], -// ["PROP_TO_ATTR_EXCEPTIONS_SVG", Object.entries(svg)], -// ]; -// -// console.log( -// all -// .map((d) => { -// return `const ${d[0]}: [(&str, &str); ${d[1].length}] = [ -// ${d[1].map((d) => ` ("${d[0]}", "${d[1]}")`).join(",\n")} -// ];`; -// }) -// .join("\n\n") -// ); -// ``` -const PROP_TO_REACT_PROP: [(&str, &str); 17] = [ - ("classId", "classID"), - ("dataType", "datatype"), - ("itemId", "itemID"), - ("strokeDashArray", "strokeDasharray"), - ("strokeDashOffset", "strokeDashoffset"), - ("strokeLineCap", "strokeLinecap"), - ("strokeLineJoin", "strokeLinejoin"), - ("strokeMiterLimit", "strokeMiterlimit"), - ("typeOf", "typeof"), - ("xLinkActuate", "xlinkActuate"), - ("xLinkArcRole", "xlinkArcrole"), - ("xLinkHref", "xlinkHref"), - ("xLinkRole", "xlinkRole"), - ("xLinkShow", "xlinkShow"), - ("xLinkTitle", "xlinkTitle"), - ("xLinkType", "xlinkType"), - ("xmlnsXLink", "xmlnsXlink"), -]; - -const PROP_TO_ATTR_EXCEPTIONS_SHARED: [(&str, &str); 48] = [ - ("ariaActiveDescendant", "aria-activedescendant"), - ("ariaAtomic", "aria-atomic"), - ("ariaAutoComplete", "aria-autocomplete"), - ("ariaBusy", "aria-busy"), - ("ariaChecked", "aria-checked"), - ("ariaColCount", "aria-colcount"), - ("ariaColIndex", "aria-colindex"), - ("ariaColSpan", "aria-colspan"), - ("ariaControls", "aria-controls"), - ("ariaCurrent", "aria-current"), - ("ariaDescribedBy", "aria-describedby"), - ("ariaDetails", "aria-details"), - ("ariaDisabled", "aria-disabled"), - ("ariaDropEffect", "aria-dropeffect"), - ("ariaErrorMessage", "aria-errormessage"), - ("ariaExpanded", "aria-expanded"), - ("ariaFlowTo", "aria-flowto"), - ("ariaGrabbed", "aria-grabbed"), - ("ariaHasPopup", "aria-haspopup"), - ("ariaHidden", "aria-hidden"), - ("ariaInvalid", "aria-invalid"), - ("ariaKeyShortcuts", "aria-keyshortcuts"), - ("ariaLabel", "aria-label"), - ("ariaLabelledBy", "aria-labelledby"), - ("ariaLevel", "aria-level"), - ("ariaLive", "aria-live"), - ("ariaModal", "aria-modal"), - ("ariaMultiLine", "aria-multiline"), - ("ariaMultiSelectable", "aria-multiselectable"), - ("ariaOrientation", "aria-orientation"), - ("ariaOwns", "aria-owns"), - ("ariaPlaceholder", "aria-placeholder"), - ("ariaPosInSet", "aria-posinset"), - ("ariaPressed", "aria-pressed"), - ("ariaReadOnly", "aria-readonly"), - ("ariaRelevant", "aria-relevant"), - ("ariaRequired", "aria-required"), - ("ariaRoleDescription", "aria-roledescription"), - ("ariaRowCount", "aria-rowcount"), - ("ariaRowIndex", "aria-rowindex"), - ("ariaRowSpan", "aria-rowspan"), - ("ariaSelected", "aria-selected"), - ("ariaSetSize", "aria-setsize"), - ("ariaSort", "aria-sort"), - ("ariaValueMax", "aria-valuemax"), - ("ariaValueMin", "aria-valuemin"), - ("ariaValueNow", "aria-valuenow"), - ("ariaValueText", "aria-valuetext"), -]; diff --git a/tests/test_utils/mdast_util_to_hast.rs b/tests/test_utils/mdast_util_to_hast.rs deleted file mode 100644 index 29d9489..0000000 --- a/tests/test_utils/mdast_util_to_hast.rs +++ /dev/null @@ -1,1275 +0,0 @@ -//! Turn a markdown AST into an HTML AST. -//! -//! Port of <https://github.com/syntax-tree/mdast-util-to-hast>, by the same -//! author: -//! -//! (The MIT License) -//! -//! Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com> -//! -//! Permission is hereby granted, free of charge, to any person obtaining -//! a copy of this software and associated documentation files (the -//! 'Software'), to deal in the Software without restriction, including -//! without limitation the rights to use, copy, modify, merge, publish, -//! distribute, sublicense, and/or sell copies of the Software, and to -//! permit persons to whom the Software is furnished to do so, subject to -//! the following conditions: -//! -//! The above copyright notice and this permission notice shall be -//! included in all copies or substantial portions of the Software. -//! -//! THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -//! EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -//! MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -//! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -//! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -//! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -//! SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -use crate::test_utils::hast; -use markdown::{mdast, sanitize, unist::Position}; - -// To do: support these compile options: -// ``` -// pub gfm_footnote_label: Option<String>, -// pub gfm_footnote_label_tag_name: Option<String>, -// pub gfm_footnote_label_attributes: Option<String>, -// pub gfm_footnote_back_label: Option<String>, -// pub gfm_footnote_clobber_prefix: Option<String>, -// ``` -// -// Maybe also: -// * option to persist `meta`? -// * option to generate a `style` attribute instead of `align`? -// * support `Raw` nodes for HTML? -// -// To do: -// * revert references when undefined? -// <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a/lib/revert.js> - -#[derive(Debug)] -struct State { - definitions: Vec<(String, String, Option<String>)>, - footnote_definitions: Vec<(String, Vec<hast::Node>)>, - footnote_calls: Vec<(String, usize)>, -} - -#[derive(Debug)] -enum Result { - Fragment(Vec<hast::Node>), - Node(hast::Node), - None, -} - -#[allow(dead_code)] -pub fn mdast_util_to_hast(mdast: &mdast::Node) -> hast::Node { - let mut definitions = vec![]; - - // Collect definitions. - // Calls take info from their definition. - // Calls can come come before definitions. - // Footnote calls can also come before footnote definitions, but those - // calls *do not* take info from their definitions, so we don’t care - // about footnotes here. - visit(mdast, |node| { - if let mdast::Node::Definition(definition) = node { - definitions.push(( - definition.identifier.clone(), - definition.url.clone(), - definition.title.clone(), - )); - } - }); - - let mut state = State { - definitions, - footnote_definitions: vec![], - footnote_calls: vec![], - }; - - let result = one(&mut state, mdast, None); - - if state.footnote_calls.is_empty() { - if let Result::Node(node) = result { - return node; - } - } - - // We either have to generate a footer, or we don’t have a single node. - // So we need a root. - let mut root = hast::Root { - children: vec![], - position: None, - }; - - match result { - Result::Fragment(children) => root.children = children, - Result::Node(node) => { - if let hast::Node::Root(existing) = node { - root = existing; - } else { - root.children.push(node); - } - } - Result::None => {} - } - - if !state.footnote_calls.is_empty() { - let mut items = vec![]; - - let mut index = 0; - while index < state.footnote_calls.len() { - let (id, count) = &state.footnote_calls[index]; - let safe_id = sanitize(&id.to_lowercase()); - - // Find definition: we’ll always find it. - let mut definition_index = 0; - while definition_index < state.footnote_definitions.len() { - if &state.footnote_definitions[definition_index].0 == id { - break; - } - definition_index += 1; - } - debug_assert_ne!( - definition_index, - state.footnote_definitions.len(), - "expected definition" - ); - - // We’ll find each used definition once, so we can split off to take the content. - let mut content = state.footnote_definitions[definition_index].1.split_off(0); - - let mut reference_index = 0; - let mut backreferences = vec![]; - while reference_index < *count { - let mut backref_children = vec![hast::Node::Text(hast::Text { - value: "↩".into(), - position: None, - })]; - - if reference_index != 0 { - backreferences.push(hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - })); - - backref_children.push(hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: (reference_index + 1).to_string(), - position: None, - })], - position: None, - })); - } - - backreferences.push(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String(format!( - "#fnref-{}{}", - safe_id, - if reference_index == 0 { - "".into() - } else { - format!("-{}", &(reference_index + 1).to_string()) - } - )), - ), - ( - "dataFootnoteBackref".into(), - hast::PropertyValue::Boolean(true), - ), - ( - "ariaLabel".into(), - hast::PropertyValue::String("Back to content".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "data-footnote-backref".into() - ]), - ), - ], - children: backref_children, - position: None, - })); - - reference_index += 1; - } - - let mut backreference_opt = Some(backreferences); - - if let Some(hast::Node::Element(tail_element)) = content.last_mut() { - if tail_element.tag_name == "p" { - if let Some(hast::Node::Text(text)) = tail_element.children.last_mut() { - text.value.push(' '); - } else { - tail_element.children.push(hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - })); - } - - tail_element - .children - .append(&mut backreference_opt.take().unwrap()); - } - } - - // No paragraph, just push them. - if let Some(mut backreference) = backreference_opt { - content.append(&mut backreference); - } - - items.push(hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "id".into(), - hast::PropertyValue::String(format!("#fn-{}", safe_id)), - )], - children: wrap(content, true), - position: None, - })); - index += 1; - } - - root.children.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - root.children.push(hast::Node::Element(hast::Element { - tag_name: "section".into(), - properties: vec![ - ("dataFootnotes".into(), hast::PropertyValue::Boolean(true)), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]), - ), - ], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "h2".into(), - properties: vec![ - ( - "id".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["sr-only".into()]), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: "Footnotes".into(), - position: None, - })], - position: None, - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![], - children: wrap(items, true), - position: None, - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - ], - position: None, - })); - root.children.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - hast::Node::Root(root) -} - -fn one(state: &mut State, node: &mdast::Node, parent: Option<&mdast::Node>) -> Result { - match node { - mdast::Node::BlockQuote(d) => transform_block_quote(state, node, d), - mdast::Node::Break(d) => transform_break(state, node, d), - mdast::Node::Code(d) => transform_code(state, node, d), - mdast::Node::Delete(d) => transform_delete(state, node, d), - mdast::Node::Emphasis(d) => transform_emphasis(state, node, d), - mdast::Node::FootnoteDefinition(d) => transform_footnote_definition(state, node, d), - mdast::Node::FootnoteReference(d) => transform_footnote_reference(state, node, d), - mdast::Node::Heading(d) => transform_heading(state, node, d), - mdast::Node::Image(d) => transform_image(state, node, d), - mdast::Node::ImageReference(d) => transform_image_reference(state, node, d), - mdast::Node::InlineCode(d) => transform_inline_code(state, node, d), - mdast::Node::InlineMath(d) => transform_inline_math(state, node, d), - mdast::Node::Link(d) => transform_link(state, node, d), - mdast::Node::LinkReference(d) => transform_link_reference(state, node, d), - mdast::Node::ListItem(d) => transform_list_item(state, node, parent, d), - mdast::Node::List(d) => transform_list(state, node, d), - mdast::Node::Math(d) => transform_math(state, node, d), - mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => { - transform_mdx_expression(state, node) - } - mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => { - transform_mdx_jsx_element(state, node) - } - mdast::Node::MdxjsEsm(d) => transform_mdxjs_esm(state, node, d), - mdast::Node::Paragraph(d) => transform_paragraph(state, node, d), - mdast::Node::Root(d) => transform_root(state, node, d), - mdast::Node::Strong(d) => transform_strong(state, node, d), - // Note: this is only called here if there is a single cell passed, not when one is found in a table. - mdast::Node::TableCell(d) => { - transform_table_cell(state, node, false, mdast::AlignKind::None, d) - } - // Note: this is only called here if there is a single row passed, not when one is found in a table. - mdast::Node::TableRow(d) => transform_table_row(state, node, false, None, d), - mdast::Node::Table(d) => transform_table(state, node, d), - mdast::Node::Text(d) => transform_text(state, node, d), - mdast::Node::ThematicBreak(d) => transform_thematic_break(state, node, d), - // Ignore. - mdast::Node::Definition(_) - | mdast::Node::Html(_) - | mdast::Node::Yaml(_) - | mdast::Node::Toml(_) => Result::None, - } -} - -/// [`BlockQuote`][mdast::BlockQuote]. -fn transform_block_quote( - state: &mut State, - node: &mdast::Node, - block_quote: &mdast::BlockQuote, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "blockquote".into(), - properties: vec![], - children: wrap(all(state, node), true), - position: block_quote.position.clone(), - })) -} - -/// [`Break`][mdast::Break]. -fn transform_break(_state: &mut State, _node: &mdast::Node, break_: &mdast::Break) -> Result { - Result::Fragment(vec![ - hast::Node::Element(hast::Element { - tag_name: "br".into(), - properties: vec![], - children: vec![], - position: break_.position.clone(), - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - ]) -} - -/// [`Code`][mdast::Code]. -fn transform_code(_state: &mut State, _node: &mdast::Node, code: &mdast::Code) -> Result { - let mut value = code.value.clone(); - value.push('\n'); - let mut properties = vec![]; - - if let Some(lang) = code.lang.as_ref() { - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![format!("language-{}", lang)]), - )); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties, - children: vec![hast::Node::Text(hast::Text { - value, - position: None, - })], - position: code.position.clone(), - })], - position: code.position.clone(), - })) -} - -/// [`Delete`][mdast::Delete]. -fn transform_delete(state: &mut State, node: &mdast::Node, delete: &mdast::Delete) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "del".into(), - properties: vec![], - children: all(state, node), - position: delete.position.clone(), - })) -} - -/// [`Emphasis`][mdast::Emphasis]. -fn transform_emphasis(state: &mut State, node: &mdast::Node, emphasis: &mdast::Emphasis) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "em".into(), - properties: vec![], - children: all(state, node), - position: emphasis.position.clone(), - })) -} - -/// [`FootnoteDefinition`][mdast::FootnoteDefinition]. -fn transform_footnote_definition( - state: &mut State, - node: &mdast::Node, - footnote_definition: &mdast::FootnoteDefinition, -) -> Result { - let children = all(state, node); - // Set aside. - state - .footnote_definitions - .push((footnote_definition.identifier.clone(), children)); - Result::None -} - -/// [`FootnoteReference`][mdast::FootnoteReference]. -fn transform_footnote_reference( - state: &mut State, - _node: &mdast::Node, - footnote_reference: &mdast::FootnoteReference, -) -> Result { - let safe_id = sanitize(&footnote_reference.identifier.to_lowercase()); - let mut call_index = 0; - - // See if this has been called before. - while call_index < state.footnote_calls.len() { - if state.footnote_calls[call_index].0 == footnote_reference.identifier { - break; - } - call_index += 1; - } - - // New. - if call_index == state.footnote_calls.len() { - state - .footnote_calls - .push((footnote_reference.identifier.clone(), 0)); - } - - // Increment. - state.footnote_calls[call_index].1 += 1; - - let reuse_counter = state.footnote_calls[call_index].1; - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String(format!("#fn-{}", safe_id)), - ), - ( - "id".into(), - hast::PropertyValue::String(format!( - "fnref-{}{}", - safe_id, - if reuse_counter > 1 { - format!("-{}", reuse_counter) - } else { - "".into() - } - )), - ), - ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true)), - ( - "ariaDescribedBy".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: (call_index + 1).to_string(), - position: None, - })], - position: None, - })], - position: footnote_reference.position.clone(), - })) -} - -/// [`Heading`][mdast::Heading]. -fn transform_heading(state: &mut State, node: &mdast::Node, heading: &mdast::Heading) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: format!("h{}", heading.depth), - properties: vec![], - children: all(state, node), - position: heading.position.clone(), - })) -} - -/// [`Image`][mdast::Image]. -fn transform_image(_state: &mut State, _node: &mdast::Node, image: &mdast::Image) -> Result { - let mut properties = vec![]; - - properties.push(( - "src".into(), - hast::PropertyValue::String(sanitize(&image.url)), - )); - - properties.push(("alt".into(), hast::PropertyValue::String(image.alt.clone()))); - - if let Some(value) = image.title.as_ref() { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties, - children: vec![], - position: image.position.clone(), - })) -} - -/// [`ImageReference`][mdast::ImageReference]. -fn transform_image_reference( - state: &mut State, - _node: &mdast::Node, - image_reference: &mdast::ImageReference, -) -> Result { - let mut properties = vec![]; - - let definition = state - .definitions - .iter() - .find(|d| d.0 == image_reference.identifier); - - let (_, url, title) = - definition.expect("expected reference to have a corresponding definition"); - - properties.push(("src".into(), hast::PropertyValue::String(sanitize(url)))); - - properties.push(( - "alt".into(), - hast::PropertyValue::String(image_reference.alt.clone()), - )); - - if let Some(value) = title { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties, - children: vec![], - position: image_reference.position.clone(), - })) -} - -/// [`InlineCode`][mdast::InlineCode]. -fn transform_inline_code( - _state: &mut State, - _node: &mdast::Node, - inline_code: &mdast::InlineCode, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: replace_eols_with_spaces(&inline_code.value), - position: None, - })], - position: inline_code.position.clone(), - })) -} - -/// [`InlineMath`][mdast::InlineMath]. -fn transform_inline_math( - _state: &mut State, - _node: &mdast::Node, - inline_math: &mdast::InlineMath, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["language-math".into(), "math-inline".into()]), - )], - children: vec![hast::Node::Text(hast::Text { - value: replace_eols_with_spaces(&inline_math.value), - position: None, - })], - position: inline_math.position.clone(), - })) -} - -/// [`Link`][mdast::Link]. -fn transform_link(state: &mut State, node: &mdast::Node, link: &mdast::Link) -> Result { - let mut properties = vec![]; - - properties.push(( - "href".into(), - hast::PropertyValue::String(sanitize(&link.url)), - )); - - if let Some(value) = link.title.as_ref() { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties, - children: all(state, node), - position: link.position.clone(), - })) -} - -/// [`LinkReference`][mdast::LinkReference]. -fn transform_link_reference( - state: &mut State, - node: &mdast::Node, - link_reference: &mdast::LinkReference, -) -> Result { - let mut properties = vec![]; - - let definition = state - .definitions - .iter() - .find(|d| d.0 == link_reference.identifier); - - let (_, url, title) = - definition.expect("expected reference to have a corresponding definition"); - - properties.push(("href".into(), hast::PropertyValue::String(sanitize(url)))); - - if let Some(value) = title { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties, - children: all(state, node), - position: link_reference.position.clone(), - })) -} - -/// [`ListItem`][mdast::ListItem]. -fn transform_list_item( - state: &mut State, - node: &mdast::Node, - parent: Option<&mdast::Node>, - list_item: &mdast::ListItem, -) -> Result { - let mut children = all(state, node); - let mut loose = list_item_loose(node); - - if let Some(parent) = parent { - if matches!(parent, mdast::Node::List(_)) { - loose = list_loose(parent); - } - }; - - let mut properties = vec![]; - - // Inject a checkbox. - if let Some(checked) = list_item.checked { - // According to github-markdown-css, this class hides bullet. - // See: <https://github.com/sindresorhus/github-markdown-css>. - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]), - )); - - let mut input = Some(hast::Node::Element(hast::Element { - tag_name: "input".into(), - properties: vec![ - ( - "type".into(), - hast::PropertyValue::String("checkbox".into()), - ), - ("checked".into(), hast::PropertyValue::Boolean(checked)), - ("disabled".into(), hast::PropertyValue::Boolean(true)), - ], - children: vec![], - position: None, - })); - - if let Some(hast::Node::Element(x)) = children.first_mut() { - if x.tag_name == "p" { - if !x.children.is_empty() { - x.children.insert( - 0, - hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - }), - ); - } - - x.children.insert(0, input.take().unwrap()); - } - } - - // If the input wasn‘t injected yet, inject a paragraph. - if let Some(input) = input { - children.insert( - 0, - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![input], - position: None, - }), - ); - } - } - - children.reverse(); - let mut result = vec![]; - let mut head = true; - let empty = children.is_empty(); - let mut tail_p = false; - - while let Some(child) = children.pop() { - let mut is_p = false; - if let hast::Node::Element(el) = &child { - if el.tag_name == "p" { - is_p = true; - } - } - - // Add eols before nodes, except if this is a tight, first paragraph. - if loose || !head || !is_p { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - if is_p && !loose { - // Unwrap the paragraph. - if let hast::Node::Element(mut el) = child { - result.append(&mut el.children); - } - } else { - result.push(child); - } - - head = false; - tail_p = is_p; - } - - // Add eol after last node, except if it is tight or a paragraph. - if !empty && (loose || !tail_p) { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties, - children: result, - position: list_item.position.clone(), - })) -} - -/// [`List`][mdast::List]. -fn transform_list(state: &mut State, node: &mdast::Node, list: &mdast::List) -> Result { - let mut contains_task_list = false; - let mut index = 0; - - while index < list.children.len() { - if let mdast::Node::ListItem(item) = &list.children[index] { - if item.checked.is_some() { - contains_task_list = true; - } - } - - index += 1; - } - - let mut properties = vec![]; - - // Add start. - if let Some(start) = list.start { - if list.ordered && start != 1 { - properties.push(( - "start".into(), - hast::PropertyValue::String(start.to_string()), - )); - } - } - - // Like GitHub, add a class for custom styling. - if contains_task_list { - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]), - )); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: if list.ordered { - "ol".into() - } else { - "ul".into() - }, - properties, - children: wrap(all(state, node), true), - position: list.position.clone(), - })) -} - -/// [`Math`][mdast::Math]. -fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) -> Result { - let mut value = math.value.clone(); - value.push('\n'); - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "language-math".into(), - "math-display".into(), - ]), - )], - children: vec![hast::Node::Text(hast::Text { - value, - position: None, - })], - position: math.position.clone(), - })], - position: math.position.clone(), - })) -} - -/// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. -fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { - match node { - mdast::Node::MdxFlowExpression(node) => { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.value.clone(), - position: node.position.clone(), - stops: node.stops.clone(), - })) - } - mdast::Node::MdxTextExpression(node) => { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.value.clone(), - position: node.position.clone(), - stops: node.stops.clone(), - })) - } - _ => unreachable!("expected expression"), - } -} - -/// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. -fn transform_mdx_jsx_element(state: &mut State, node: &mdast::Node) -> Result { - let (name, attributes) = match node { - mdast::Node::MdxJsxFlowElement(n) => (&n.name, &n.attributes), - mdast::Node::MdxJsxTextElement(n) => (&n.name, &n.attributes), - _ => unreachable!("expected mdx jsx element"), - }; - - Result::Node(hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: name.clone(), - attributes: attributes.clone(), - children: all(state, node), - position: node.position().cloned(), - })) -} - -/// [`MdxjsEsm`][mdast::MdxjsEsm]. -fn transform_mdxjs_esm( - _state: &mut State, - _node: &mdast::Node, - mdxjs_esm: &mdast::MdxjsEsm, -) -> Result { - Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: mdxjs_esm.value.clone(), - position: mdxjs_esm.position.clone(), - stops: mdxjs_esm.stops.clone(), - })) -} - -/// [`Paragraph`][mdast::Paragraph]. -fn transform_paragraph( - state: &mut State, - node: &mdast::Node, - paragraph: &mdast::Paragraph, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: all(state, node), - position: paragraph.position.clone(), - })) -} - -/// [`Root`][mdast::Root]. -fn transform_root(state: &mut State, node: &mdast::Node, root: &mdast::Root) -> Result { - Result::Node(hast::Node::Root(hast::Root { - children: wrap(all(state, node), false), - position: root.position.clone(), - })) -} - -/// [`Strong`][mdast::Strong]. -fn transform_strong(state: &mut State, node: &mdast::Node, strong: &mdast::Strong) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "strong".into(), - properties: vec![], - children: all(state, node), - position: strong.position.clone(), - })) -} - -/// [`TableCell`][mdast::TableCell]. -fn transform_table_cell( - state: &mut State, - node: &mdast::Node, - head: bool, - align: mdast::AlignKind, - table_cell: &mdast::TableCell, -) -> Result { - let align_value = match align { - mdast::AlignKind::None => None, - mdast::AlignKind::Left => Some("left"), - mdast::AlignKind::Right => Some("right"), - mdast::AlignKind::Center => Some("center"), - }; - - let mut properties = vec![]; - - if let Some(value) = align_value { - properties.push(("align".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: if head { "th".into() } else { "td".into() }, - properties, - children: all(state, node), - position: table_cell.position.clone(), - })) -} - -/// [`TableRow`][mdast::TableRow]. -fn transform_table_row( - state: &mut State, - _node: &mdast::Node, - head: bool, - align: Option<&[mdast::AlignKind]>, - table_row: &mdast::TableRow, -) -> Result { - let mut children = vec![]; - let mut index = 0; - #[allow(clippy::redundant_closure_for_method_calls)] - let len = align.map_or(table_row.children.len(), |d| d.len()); - let empty_cell = mdast::Node::TableCell(mdast::TableCell { - children: vec![], - position: None, - }); - - while index < len { - let align_value = align - .and_then(|d| d.get(index)) - .unwrap_or(&mdast::AlignKind::None); - - let child = table_row.children.get(index).unwrap_or(&empty_cell); - - let result = if let mdast::Node::TableCell(table_cell) = child { - transform_table_cell(state, child, head, *align_value, table_cell) - } else { - unreachable!("expected tale cell in table row") - }; - - append_result(&mut children, result); - index += 1; - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "tr".into(), - properties: vec![], - children: wrap(children, true), - position: table_row.position.clone(), - })) -} - -/// [`Table`][mdast::Table]. -fn transform_table(state: &mut State, _node: &mdast::Node, table: &mdast::Table) -> Result { - let mut rows = vec![]; - let mut index = 0; - - while index < table.children.len() { - let child = &table.children[index]; - let result = if let mdast::Node::TableRow(table_row) = child { - transform_table_row( - state, - &table.children[index], - index == 0, - Some(&table.align), - table_row, - ) - } else { - unreachable!("expected table row as child of table") - }; - - append_result(&mut rows, result); - index += 1; - } - - let body_rows = rows.split_off(1); - let head_row = rows.pop(); - let mut children = vec![]; - - if let Some(row) = head_row { - let position = row.position().cloned(); - children.push(hast::Node::Element(hast::Element { - tag_name: "thead".into(), - properties: vec![], - children: wrap(vec![row], true), - position, - })); - } - - if !body_rows.is_empty() { - let mut position = None; - - if let Some(position_start) = body_rows.first().and_then(hast::Node::position) { - if let Some(position_end) = body_rows.last().and_then(hast::Node::position) { - position = Some(Position { - start: position_start.start.clone(), - end: position_end.end.clone(), - }); - } - } - - children.push(hast::Node::Element(hast::Element { - tag_name: "tbody".into(), - properties: vec![], - children: wrap(body_rows, true), - position, - })); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "table".into(), - properties: vec![], - children: wrap(children, true), - position: table.position.clone(), - })) -} - -/// [`Text`][mdast::Text]. -fn transform_text(_state: &mut State, _node: &mdast::Node, text: &mdast::Text) -> Result { - Result::Node(hast::Node::Text(hast::Text { - value: text.value.clone(), - position: text.position.clone(), - })) -} - -/// [`ThematicBreak`][mdast::ThematicBreak]. -fn transform_thematic_break( - _state: &mut State, - _node: &mdast::Node, - thematic_break: &mdast::ThematicBreak, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "hr".into(), - properties: vec![], - children: vec![], - position: thematic_break.position.clone(), - })) -} - -// Transform children of `parent`. -fn all(state: &mut State, parent: &mdast::Node) -> Vec<hast::Node> { - let mut nodes = vec![]; - if let Some(children) = parent.children() { - let mut index = 0; - while index < children.len() { - let child = &children[index]; - let result = one(state, child, Some(parent)); - append_result(&mut nodes, result); - index += 1; - } - } - - nodes -} - -/// Wrap `nodes` with line feeds between each entry. -/// Optionally adds line feeds at the start and end. -fn wrap(mut nodes: Vec<hast::Node>, loose: bool) -> Vec<hast::Node> { - let mut result = vec![]; - let was_empty = nodes.is_empty(); - let mut head = true; - - nodes.reverse(); - - if loose { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - while let Some(item) = nodes.pop() { - // Inject when there’s more: - if !head { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - head = false; - result.push(item); - } - - if loose && !was_empty { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - result -} - -/// Visit. -fn visit<Visitor>(node: &mdast::Node, visitor: Visitor) -where - Visitor: FnMut(&mdast::Node), -{ - visit_impl(node, visitor); -} - -/// Visit, mutably. -// Probably useful later: -#[allow(dead_code)] -fn visit_mut<Visitor>(node: &mut mdast::Node, visitor: Visitor) -where - Visitor: FnMut(&mut mdast::Node), -{ - visit_mut_impl(node, visitor); -} - -/// Internal implementation to visit. -fn visit_impl<Visitor>(node: &mdast::Node, mut visitor: Visitor) -> Visitor -where - Visitor: FnMut(&mdast::Node), -{ - visitor(node); - - if let Some(children) = node.children() { - let mut index = 0; - while index < children.len() { - let child = &children[index]; - visitor = visit_impl(child, visitor); - index += 1; - } - } - - visitor -} - -/// Internal implementation to visit, mutably. -fn visit_mut_impl<Visitor>(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor -where - Visitor: FnMut(&mut mdast::Node), -{ - visitor(node); - - if let Some(children) = node.children_mut() { - let mut index = 0; - while let Some(child) = children.get_mut(index) { - visitor = visit_mut_impl(child, visitor); - index += 1; - } - } - - visitor -} - -// To do: trim arounds breaks: <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a/lib/traverse.js>. -/// Append an (optional, variadic) result. -fn append_result(list: &mut Vec<hast::Node>, result: Result) { - match result { - Result::Fragment(mut fragment) => list.append(&mut fragment), - Result::Node(node) => list.push(node), - Result::None => {} - }; -} - -/// Replace line endings (CR, LF, CRLF) with spaces. -/// -/// Used for inline code and inline math. -fn replace_eols_with_spaces(value: &str) -> String { - // It’ll grow a bit small for each CR+LF. - let mut result = String::with_capacity(value.len()); - let bytes = value.as_bytes(); - let mut index = 0; - let mut start = 0; - - while index < bytes.len() { - let byte = bytes[index]; - - if byte == b'\r' || byte == b'\n' { - result.push_str(&value[start..index]); - result.push(' '); - - if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' { - index += 1; - } - - start = index + 1; - } - - index += 1; - } - - result.push_str(&value[start..]); - - result -} - -/// Check if a list is loose. -fn list_loose(node: &mdast::Node) -> bool { - if let mdast::Node::List(list) = node { - if list.spread { - return true; - } - - if let Some(children) = node.children() { - let mut index = 0; - while index < children.len() { - if list_item_loose(&children[index]) { - return true; - } - index += 1; - } - } - } - - false -} - -/// Check if a list item is loose. -fn list_item_loose(node: &mdast::Node) -> bool { - if let mdast::Node::ListItem(item) = node { - item.spread - } else { - false - } -} diff --git a/tests/test_utils/mdx.rs b/tests/test_utils/mdx.rs deleted file mode 100644 index 4481b78..0000000 --- a/tests/test_utils/mdx.rs +++ /dev/null @@ -1,221 +0,0 @@ -use crate::test_utils::{ - hast_util_to_swc::hast_util_to_swc, - mdast_util_to_hast::mdast_util_to_hast, - mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions}, - mdx_plugin_recma_jsx_rewrite::{mdx_plugin_recma_jsx_rewrite, Options as RewriteOptions}, - swc::{parse_esm, parse_expression, serialize}, -}; -use markdown::{to_mdast, Constructs, Location, ParseOptions}; - -pub use super::mdx_plugin_recma_document::JsxRuntime; - -use swc_common::comments::{Comments, SingleThreadedComments}; -// use swc_ecma_transforms_react::{jsx, Options as JsxBuildOptions}; -// use swc_ecma_visit::VisitMutWith; - -// To do: use `Format`. -/// Format the file is in (default: `Format::Detect`). -#[allow(dead_code)] -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub enum Format { - /// Use `Format::Markdown` for files with an extension in `md_extensions` - /// and `Format::Mdx` otherwise. - #[default] - Detect, - /// Treat file as MDX. - Mdx, - /// Treat file as plain vanilla markdown. - Markdown, -} - -// To do: use `OutputFormat`. -/// Output format to generate (default: `OutputFormat::Program`). -#[allow(dead_code)] -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub enum OutputFormat { - /// The `Program` format will use import statements to import the JSX - /// runtime (and optionally provider) and use an export statement to yield - /// the `MDXContent` component. - #[default] - Program, - /// The `FunctionBody` format will get the JSX runtime (and optionally - /// provider) from `arguments[0]`, rewrite export statements, and use a - /// return statement to yield what was exported. - /// Normally, this output format will throw on `import` (and - /// `export … from`) statements, but you can support them by setting - /// `options.useDynamicImport`. - FunctionBody, -} - -/// Configuration (optional). -#[derive(Clone, Debug, Default)] -pub struct Options { - // /// List of markdown extensions, with dot. - // /// - // /// Default: `vec![".md".into(), ".markdown".into(), ".mdown".into(), ".mkdn".into(), ".mkd".into(), ".mdwn".into(), ".mkdown".into(), ".ron".into()]`. - // pub md_extensions: Option<Vec<String>>, - // /// List of MDX extensions, with dot. - // /// - // /// Default: `vec![".mdx".into()]`. - // pub mdx_extensions: Option<Vec<String>>, - // /// Format the file is in (default: `Format::Detect`). - // pub format: Format, - // /// To do: support `output_format: FunctionBody - // /// Output format to generate (default: `OutputFormat::Program`). - // /// - // /// In most cases `OutputFormat::Program` should be used, as it results in a - // /// whole program. - // /// `OutputFormat::FunctionBody` can be used to compile to code that can be - // /// `eval`ed. - // /// In some cases, you might want to do that, such as when compiling on the - // /// server and running on the client. - // pub output_format: OutputFormat, - // /// Whether to compile to dynamic import expressions (default: - // /// `false`). - // /// - // /// This option applies when `options.output_format` is - // /// `OutputFormat::FunctionBody`. - // /// - // /// This project can turn import statements (`import x from 'y'`) into - // /// dynamic imports (`const {x} = await import('y')`). - // /// This is useful because import statements only work at the top level of - // /// JavaScript modules, whereas `import()` is available inside function - // /// bodies. - // /// - // /// When you turn `use_dynamic_import` on, you should probably set - // /// `options.base_url` too. - // pub use_dynamic_import: bool, - // /// Resolve `import`s (and `export … from`, `import.meta.url`) from this - // /// URL (default: `None`, example: `Some("https://example.com/".into())`). - // /// - // /// Relative specifiers are non-absolute URLs that start with `/`, `./`, or - // /// `../`. - // /// For example: `/index.js`, `./folder/file.js`, or `../main.js`. - // /// - // /// This option is useful when code will run in a different place. - // /// One example is when `.mdx` files are in path *a* but compiled to path - // /// *b* and imports should run relative the path *b*. - // /// Another example is when evaluating code, whether in Node or a browser. - // pub base_url: Option<String>, - /// Whether to add extra information to error messages in generated code - /// (default: `false`). - pub development: bool, - - // To do: some alternative to generate source maps. - // SourceMapGenerator - /// Place to import a provider from (default: `None`, example: - /// `Some("@mdx-js/react").into()`). - /// - /// Useful for runtimes that support context (React, Preact). - /// The provider must export a `useMDXComponents`, which is called to - /// access an object of components. - pub provider_import_source: Option<String>, - - /// Whether to keep JSX (default: `false`). - /// - /// The default is to compile JSX away so that the resulting file is - /// immediately runnable. - pub jsx: bool, - - /// JSX runtime to use (default: `Some(JsxRuntime::Automatic)`). - /// - /// The classic runtime compiles to calls such as `h('p')`, the automatic - /// runtime compiles to - /// `import _jsx from '$importSource/jsx-runtime'\n_jsx('p')`. - pub jsx_runtime: Option<JsxRuntime>, - - /// Place to import automatic JSX runtimes from (`Option<String>`, default: - /// `Some("react".into())`). - /// - /// When in the automatic runtime, this is used to define an import for - /// `_Fragment`, `_jsx`, and `_jsxs`. - pub jsx_import_source: Option<String>, - - /// Pragma for JSX (default: `Some("React.createElement".into())`). - /// - /// When in the classic runtime, this is used as an identifier for function - /// calls: `<x />` to `React.createElement('x')`. - /// - /// You should most probably define `pragma_frag` and `pragma_import_source` - /// too when changing this. - pub pragma: Option<String>, - - /// Pragma for JSX fragments (default: `Some("React.Fragment".into())`). - /// - /// When in the classic runtime, this is used as an identifier for - /// fragments: `<>` to `React.createElement(React.Fragment)`. - /// - /// You should most probably define `pragma` and `pragma_import_source` - /// too when changing this. - pub pragma_frag: Option<String>, - - /// Where to import the identifier of `pragma` from (default: - /// `Some("react".into())`). - /// - /// When in the classic runtime, this is used to import the `pragma` - /// function. - /// To illustrate with an example: when `pragma` is `"a.b"` and - /// `pragma_import_source` is `"c"`, the following will be generated: - /// `import a from 'c'`. - pub pragma_import_source: Option<String>, -} - -#[allow(dead_code)] -pub fn mdx(value: &str, filepath: Option<String>, options: &Options) -> Result<String, String> { - let parse_options = ParseOptions { - constructs: Constructs::mdx(), - mdx_esm_parse: Some(Box::new(parse_esm)), - mdx_expression_parse: Some(Box::new(parse_expression)), - ..ParseOptions::default() - }; - let document_options = DocumentOptions { - pragma: options.pragma.clone(), - pragma_frag: options.pragma_frag.clone(), - pragma_import_source: options.pragma_import_source.clone(), - jsx_import_source: options.jsx_import_source.clone(), - jsx_runtime: options.jsx_runtime, - }; - let rewrite_options = RewriteOptions { - development: options.development, - provider_import_source: options.provider_import_source.clone(), - }; - - let location = Location::new(value.as_bytes()); - let mdast = to_mdast(value, &parse_options)?; - let hast = mdast_util_to_hast(&mdast); - let mut program = hast_util_to_swc(&hast, filepath, Some(&location))?; - mdx_plugin_recma_document(&mut program, &document_options, Some(&location))?; - mdx_plugin_recma_jsx_rewrite(&mut program, &rewrite_options, Some(&location)); - - // Add our comments. - let comments = SingleThreadedComments::default(); - - for c in program.comments { - comments.add_leading(c.span.lo, c); - } - - println!("comments for swc: {:?}", comments); - - if !options.jsx { - // let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); - // let build_options = JsxBuildOptions { - // next: Some(true), - // throw_if_namespace: Some(false), - // development: Some(options.development), - // use_spread: Some(true), - // refresh: None, - // runtime: None, - // import_source: None, - // pragma: None, - // pragma_frag: None, - // use_builtins: None, - // }; - // let mark = Mark::fresh(Mark::default()); - // program - // .module - // .visit_mut_with(&mut jsx(cm, Some(comments), build_options, mark)); - } - - // To do: include comments. - Ok(serialize(&program.module)) -} diff --git a/tests/test_utils/mdx_plugin_recma_document.rs b/tests/test_utils/mdx_plugin_recma_document.rs deleted file mode 100644 index 48648b9..0000000 --- a/tests/test_utils/mdx_plugin_recma_document.rs +++ /dev/null @@ -1,662 +0,0 @@ -//! Turn a JavaScript AST, coming from MD(X), into a component. -//! -//! Port of <https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/recma-document.js>, -//! by the same author. - -use crate::test_utils::{ - hast_util_to_swc::Program, - swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, -}; -use markdown::{ - unist::{Point, Position}, - Location, -}; - -/// JSX runtimes (default: `JsxRuntime: Automatic`). -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum JsxRuntime { - /// Automatic runtime. - /// - /// With the automatic runtime, some module is expected to exist somewhere. - /// That modules is expected to expose a certain API. - /// The compiler adds an import of that module and compiles JSX away to - /// function calls that use that API. - #[default] - Automatic, - /// Classic runtime. - /// - /// With the classic runtime, you define two values yourself in each file, - /// which are expected to work a certain way. - /// The compiler compiles JSX away to function calls using those two values. - Classic, -} - -/// Configuration. -#[derive(Debug, PartialEq, Eq)] -pub struct Options { - /// Pragma for JSX (used in classic runtime). - /// - /// Default: `React.createElement`. - pub pragma: Option<String>, - /// Pragma for JSX fragments (used in classic runtime). - /// - /// Default: `React.Fragment`. - pub pragma_frag: Option<String>, - /// Where to import the identifier of `pragma` from (used in classic runtime). - /// - /// Default: `react`. - pub pragma_import_source: Option<String>, - /// Place to import automatic JSX runtimes from (used in automatic runtime). - /// - /// Default: `react`. - pub jsx_import_source: Option<String>, - /// JSX runtime to use. - /// - /// Default: `automatic`. - pub jsx_runtime: Option<JsxRuntime>, -} - -impl Default for Options { - /// Use the automatic JSX runtime with React. - fn default() -> Self { - Self { - pragma: None, - pragma_frag: None, - pragma_import_source: None, - jsx_import_source: None, - jsx_runtime: Some(JsxRuntime::default()), - } - } -} - -#[allow(dead_code)] -pub fn mdx_plugin_recma_document( - program: &mut Program, - options: &Options, - location: Option<&Location>, -) -> Result<(), String> { - // New body children. - let mut replacements = vec![]; - - // Inject JSX configuration comment. - if let Some(runtime) = &options.jsx_runtime { - let mut pragmas = vec![]; - let react = &"react".into(); - let create_element = &"React.createElement".into(); - let fragment = &"React.Fragment".into(); - - if *runtime == JsxRuntime::Automatic { - pragmas.push("@jsxRuntime automatic".into()); - pragmas.push(format!( - "@jsxImportSource {}", - if let Some(jsx_import_source) = &options.jsx_import_source { - jsx_import_source - } else { - react - } - )); - } else { - pragmas.push("@jsxRuntime classic".into()); - pragmas.push(format!( - "@jsx {}", - if let Some(pragma) = &options.pragma { - pragma - } else { - create_element - } - )); - pragmas.push(format!( - "@jsxFrag {}", - if let Some(pragma_frag) = &options.pragma_frag { - pragma_frag - } else { - fragment - } - )); - } - - if !pragmas.is_empty() { - program.comments.insert( - 0, - swc_common::comments::Comment { - kind: swc_common::comments::CommentKind::Block, - text: pragmas.join(" ").into(), - span: swc_common::DUMMY_SP, - }, - ); - } - } - - // Inject an import in the classic runtime for the pragma (and presumably, - // fragment). - if options.jsx_runtime == Some(JsxRuntime::Classic) { - let pragma = if let Some(pragma) = &options.pragma { - pragma - } else { - "React" - }; - let sym = pragma.split('.').next().expect("first item always exists"); - - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - imported: None, - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: Box::new(swc_ecma_ast::Str { - value: (if let Some(source) = &options.pragma_import_source { - source.clone() - } else { - "react".into() - }) - .into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }), - )); - } - - // Find the `export default`, the JSX expression, and leave the rest as it - // is. - let mut input = program.module.body.split_off(0); - input.reverse(); - let mut layout = false; - let mut layout_position = None; - let content = true; - - while let Some(module_item) = input.pop() { - match module_item { - // ```js - // export default props => <>{props.children}</> - // ``` - // - // Treat it as an inline layout declaration. - // - // In estree, the below two are the same node (`ExportDefault`). - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( - decl, - )) => { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&decl.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - - layout = true; - layout_position = span_to_position(&decl.span, location); - match decl.decl { - swc_ecma_ast::DefaultDecl::Class(cls) => { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) - } - swc_ecma_ast::DefaultDecl::Fn(func) => { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Fn(func))) - } - swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { - return Err( - prefix_error_with_point( - "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), - bytepos_to_point(&decl.span.lo, location).as_ref() - ) - ); - } - } - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( - expr, - )) => { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&expr.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - - layout = true; - layout_position = span_to_position(&expr.span, location); - replacements.push(create_layout_decl(*expr.expr)); - } - // ```js - // export {a, b as c} from 'd' - // export {a, b as c} - // ``` - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed( - mut named_export, - )) => { - // SWC is currently crashing when generating code, w/o source - // map, if an actual location is set on this node. - named_export.span = swc_common::DUMMY_SP; - - let mut index = 0; - let mut id = None; - - while index < named_export.specifiers.len() { - let mut take = false; - // Note: the `swc_ecma_ast::ExportSpecifier::Default` - // branch of this looks interesting, but as far as I - // understand it *is not* valid ES. - // `export a from 'b'` is a syntax error, even in SWC. - if let swc_ecma_ast::ExportSpecifier::Named(named) = - &named_export.specifiers[index] - { - if let Some(swc_ecma_ast::ModuleExportName::Ident(ident)) = &named.exported - { - if ident.sym.as_ref() == "default" { - // For some reason the AST supports strings - // instead of identifiers. - // Looks like some TC39 proposal. Ignore for now - // and only do things if this is an ID. - if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&ident.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - layout = true; - layout_position = span_to_position(&ident.span, location); - take = true; - id = Some(ident.clone()); - } - } - } - } - - if take { - named_export.specifiers.remove(index); - } else { - index += 1; - } - } - - if let Some(id) = id { - let source = named_export.src.clone(); - - // If there was just a default export, we can drop the original node. - if !named_export.specifiers.is_empty() { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportNamed(named_export), - )); - } - - // It’s an `export {x} from 'y'`, so generate an import. - if let Some(source) = source { - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - imported: Some(swc_ecma_ast::ModuleExportName::Ident(id)), - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: source, - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }), - )) - } - // It’s an `export {x}`, so generate a variable declaration. - else { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Ident(id))); - } - } else { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportNamed(named_export), - )); - } - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(mut x)) => { - // SWC is currently crashing when generating code, w/o source - // map, if an actual location is set on this node. - x.span = swc_common::DUMMY_SP; - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(x), - )); - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDecl(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportAll(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsImportEquals(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsExportAssignment( - _, - )) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsNamespaceExport( - _, - )) => { - // Pass through. - replacements.push(module_item); - } - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr(expr_stmt)) => { - match *expr_stmt.expr { - swc_ecma_ast::Expr::JSXElement(elem) => { - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXElement(elem)), - layout, - )); - } - swc_ecma_ast::Expr::JSXFragment(mut frag) => { - // Unwrap if possible. - if frag.children.len() == 1 { - let item = frag.children.pop().unwrap(); - - if let swc_ecma_ast::JSXElementChild::JSXElement(elem) = item { - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXElement(elem)), - layout, - )); - continue; - } - - frag.children.push(item) - } - - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXFragment(frag)), - layout, - )); - } - _ => { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::Stmt( - swc_ecma_ast::Stmt::Expr(expr_stmt), - )); - } - } - } - swc_ecma_ast::ModuleItem::Stmt(stmt) => { - replacements.push(swc_ecma_ast::ModuleItem::Stmt(stmt)); - } - } - } - - // Generate an empty component. - if !content { - replacements.append(&mut create_mdx_content(None, layout)); - } - - // ```jsx - // export default MDXContent - // ``` - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportDefaultExpr(swc_ecma_ast::ExportDefaultExpr { - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "MDXContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }), - )); - - program.module.body = replacements; - - Ok(()) -} - -/// Create a content component. -fn create_mdx_content( - expr: Option<swc_ecma_ast::Expr>, - has_internal_layout: bool, -) -> Vec<swc_ecma_ast::ModuleItem> { - // ```jsx - // <MDXLayout {...props}>xxx</MDXLayout> - // ``` - let mut result = swc_ecma_ast::Expr::JSXElement(Box::new(swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( - swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }, - )], - self_closing: false, - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: Some(swc_ecma_ast::JSXClosingElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - span: swc_common::DUMMY_SP, - }), - // ```jsx - // <_createMdxContent {...props} /> - // ``` - children: vec![swc_ecma_ast::JSXElementChild::JSXElement(Box::new( - swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( - swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }, - )], - self_closing: true, - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: None, - children: vec![], - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - })); - - if !has_internal_layout { - // ```jsx - // MDXLayout ? <MDXLayout>xxx</MDXLayout> : _createMdxContent(props) - // ``` - result = swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - cons: Box::new(result), - alt: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Ident( - swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - ))), - args: vec![swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }], - type_args: None, - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }); - } - - // ```jsx - // function _createMdxContent(props) { - // return xxx - // } - // ``` - let create_mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( - swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { - ident: swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: vec![swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }], - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - arg: Some(Box::new(expr.unwrap_or({ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Null(swc_ecma_ast::Null { - span: swc_common::DUMMY_SP, - })) - }))), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }), - )); - - // ```jsx - // function MDXContent(props = {}) { - // return <MDXLayout>xxx</MDXLayout> - // } - // ``` - let mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( - swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { - ident: swc_ecma_ast::Ident { - sym: "MDXContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: vec![swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Assign(swc_ecma_ast::AssignPat { - left: Box::new(swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - })), - right: Box::new(swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: vec![], - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }], - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - arg: Some(Box::new(result)), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }), - )); - - vec![create_mdx_content, mdx_content] -} - -/// Create a layout, inside the document. -fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { - // ```jsx - // const MDXLayout = xxx - // ``` - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( - swc_ecma_ast::VarDecl { - kind: swc_ecma_ast::VarDeclKind::Const, - declare: false, - decls: vec![swc_ecma_ast::VarDeclarator { - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - }), - init: Some(Box::new(expr)), - span: swc_common::DUMMY_SP, - definite: false, - }], - span: swc_common::DUMMY_SP, - }, - )))) -} - -/// Create an error message about multiple layouts. -fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { - prefix_error_with_point( - format!( - "Cannot specify multiple layouts{}", - if let Some(previous) = previous { - format!(" (previous: {:?})", previous) - } else { - "".into() - } - ), - at, - ) -} diff --git a/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs b/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs deleted file mode 100644 index 6b058fd..0000000 --- a/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs +++ /dev/null @@ -1,1165 +0,0 @@ -//! Rewrite JSX tags to accept them from props and an optional provider. -//! -//! Port of <https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/recma-jsx-rewrite.js>, -//! by the same author. - -use crate::test_utils::{ - hast_util_to_swc::Program, - swc_utils::{ - create_binary_expression, create_ident, create_ident_expression, create_member_expression, - position_to_string, span_to_position, - }, -}; -use markdown::{id_cont, id_start, unist::Position, Location}; -use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; - -/// Configuration. -#[derive(Debug, Default, Clone)] -pub struct Options { - /// Place to import a provider from. - /// - /// See [MDX provider](https://mdxjs.com/docs/using-mdx/#mdx-provider) - /// on the MDX website for more info. - pub provider_import_source: Option<String>, - /// Whether to add extra information to error messages in generated code. - /// This is not yet supported. - pub development: bool, -} - -/// Rewrite JSX in an MDX file so that components can be passed in and provided. -#[allow(dead_code)] -pub fn mdx_plugin_recma_jsx_rewrite( - program: &mut Program, - options: &Options, - location: Option<&Location>, -) { - let mut state = State { - scopes: vec![], - location, - provider: options.provider_import_source.is_some(), - path: program.path.clone(), - development: options.development, - create_provider_import: false, - create_error_helper: false, - }; - state.enter(Some(Info::default())); - program.module.visit_mut_with(&mut state); - - // If a provider is used (and can be used), import it. - if let Some(source) = &options.provider_import_source { - if state.create_provider_import { - program - .module - .body - .insert(0, create_import_provider(source)) - } - } - - // If potentially missing components are used, add the helper used for - // errors. - if state.create_error_helper { - program - .module - .body - .push(create_error_helper(state.development, state.path)); - } -} - -/// Collection of different SWC functions. -#[derive(Debug)] -enum Func<'a> { - /// Function declaration. - Decl(&'a mut swc_ecma_ast::FnDecl), - /// Function expression. - Expr(&'a mut swc_ecma_ast::FnExpr), - /// Arrow function. - Arrow(&'a mut swc_ecma_ast::ArrowExpr), -} - -/// Info for a function scope. -#[derive(Debug, Default, Clone)] -struct Info { - /// Function name. - name: Option<String>, - /// Used objects (`a` in `<a.b />`). - objects: Vec<String>, - /// Used components (`<A />`). - components: Vec<String>, - /// Used literals (`<a />`). - tags: Vec<String>, - /// List of JSX identifiers of literal tags that are not valid JS - /// identifiers in the shape of `Vec<(invalid, valid)>`. - /// - /// Example: - /// - /// ``` - /// vec![("a-b".into(), "_component0".into())] - /// ``` - aliases: Vec<(String, String)>, - /// Non-literal references in the shape of `Vec<(name, is_component)>`. - /// - /// Example: - /// - /// ``` - /// vec![("a".into(), false), ("a.b".into(), true)] - /// ``` - references: Vec<(String, bool, Option<Position>)>, -} - -/// Scope (block or function/global). -#[derive(Debug, Clone)] -struct Scope { - /// If this is a function (or global) scope, we track info. - info: Option<Info>, - /// Things that are defined in this scope. - defined: Vec<String>, -} - -/// Context. -#[derive(Debug, Default, Clone)] -struct State<'a> { - location: Option<&'a Location>, - /// Path to file. - path: Option<String>, - /// List of current scopes. - scopes: Vec<Scope>, - /// Whether the user is in development mode. - development: bool, - /// Whether the user uses a provider. - provider: bool, - /// Whether a provider is referenced. - create_provider_import: bool, - /// Whether a missing component helper is referenced. - /// - /// When things are referenced that might not be defined, we reference a - /// helper function to throw when they are missing. - create_error_helper: bool, -} - -impl<'a> State<'a> { - /// Open a new scope. - fn enter(&mut self, info: Option<Info>) { - self.scopes.push(Scope { - info, - defined: vec![], - }); - } - - /// Close the current scope. - fn exit(&mut self) -> Scope { - self.scopes.pop().expect("expected scope") - } - - /// Close a function. - fn exit_func(&mut self, func: Func) { - let mut scope = self.exit(); - let mut defaults = vec![]; - let mut info = scope.info.take().unwrap(); - let mut index = 0; - - // Create defaults for tags. - // - // ```jsx - // {h1: 'h1'} - // ``` - while index < info.tags.len() { - let name = &info.tags[index]; - - defaults.push(swc_ecma_ast::PropOrSpread::Prop(Box::new( - swc_ecma_ast::Prop::KeyValue(swc_ecma_ast::KeyValueProp { - key: if is_identifier_name(name) { - swc_ecma_ast::PropName::Ident(create_ident(name)) - } else { - swc_ecma_ast::PropName::Str(swc_ecma_ast::Str { - value: name.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }) - }, - value: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: name.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }), - ))); - - index += 1; - } - - let mut actual = info.components.split_off(0); - let mut index = 0; - - // In some cases, a component is used directly (`<X>`) but it’s also - // used as an object (`<X.Y>`). - while index < info.objects.len() { - if !actual.contains(&info.objects[index]) { - actual.push(info.objects[index].clone()); - } - index += 1; - } - - let mut statements = vec![]; - - if !defaults.is_empty() || !actual.is_empty() || !info.aliases.is_empty() { - let mut parameters = vec![]; - - // Use a provider, if configured. - // - // ```jsx - // _provideComponents() - // ``` - if self.provider { - self.create_provider_import = true; - parameters.push(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( - "_provideComponents", - ))), - args: vec![], - type_args: None, - span: swc_common::DUMMY_SP, - })); - } - - // Accept `components` as a prop if this is the `MDXContent` or - // `_createMdxContent` function. - // - // ```jsx - // props.components - // ``` - if is_props_receiving_fn(&info.name) { - parameters.push(swc_ecma_ast::Expr::Member(swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("props")), - prop: swc_ecma_ast::MemberProp::Ident(create_ident("components")), - span: swc_common::DUMMY_SP, - })); - } - - // Inject an object at the start, when: - // - there are defaults, - // - there are two sources - // - // ```jsx - // (_provideComponents(), props.components) - // () - // ``` - // - // To: - // - // ```jsx - // ({}, _provideComponents(), props.components) - // ({h1: 'h1'}) - // ``` - if !defaults.is_empty() || parameters.len() > 1 { - parameters.insert( - 0, - swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: defaults, - span: swc_common::DUMMY_SP, - }), - ); - } - - // Merge things and prevent errors. - // - // ```jsx - // {}, _provideComponents(), props.components - // props.components - // _provideComponents() - // ``` - // - // To: - // - // ```jsx - // Object.assign({}, _provideComponents(), props.components) - // props.components || {} - // _provideComponents() - // ``` - let mut components_init = if parameters.len() > 1 { - let mut args = vec![]; - parameters.reverse(); - while let Some(param) = parameters.pop() { - args.push(swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(param), - }); - } - swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Member( - swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("Object")), - prop: swc_ecma_ast::MemberProp::Ident(create_ident("assign")), - span: swc_common::DUMMY_SP, - }, - ))), - args, - type_args: None, - span: swc_common::DUMMY_SP, - }) - } else { - // Always one. - let param = parameters.pop().unwrap(); - - if let swc_ecma_ast::Expr::Member(_) = param { - create_binary_expression( - vec![ - param, - swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: vec![], - span: swc_common::DUMMY_SP, - }), - ], - swc_ecma_ast::BinaryOp::LogicalOr, - ) - } else { - param - } - }; - - // Add components to scope. - // - // For `['MyComponent', 'MDXLayout']` this generates: - // - // ```js - // const {MyComponent, wrapper: MDXLayout} = _components - // ``` - // - // Note that MDXLayout is special as it’s taken from - // `_components.wrapper`. - let components_pattern = if actual.is_empty() { - None - } else { - let mut props = vec![]; - actual.reverse(); - while let Some(key) = actual.pop() { - // `wrapper: MDXLayout` - if key == "MDXLayout" { - props.push(swc_ecma_ast::ObjectPatProp::KeyValue( - swc_ecma_ast::KeyValuePatProp { - key: swc_ecma_ast::PropName::Ident(create_ident("wrapper")), - value: Box::new(swc_ecma_ast::Pat::Ident( - swc_ecma_ast::BindingIdent { - id: create_ident(&key), - type_ann: None, - }, - )), - }, - )) - } - // `MyComponent` - else { - props.push(swc_ecma_ast::ObjectPatProp::Assign( - swc_ecma_ast::AssignPatProp { - key: create_ident(&key), - value: None, - span: swc_common::DUMMY_SP, - }, - )) - } - } - - Some(swc_ecma_ast::ObjectPat { - props, - optional: false, - span: swc_common::DUMMY_SP, - type_ann: None, - }) - }; - - let mut declarators = vec![]; - - // If there are tags, they take them from `_components`, so we need - // to make it defined. - if !info.tags.is_empty() { - declarators.push(swc_ecma_ast::VarDeclarator { - span: swc_common::DUMMY_SP, - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("_components"), - type_ann: None, - }), - init: Some(Box::new(components_init)), - definite: false, - }); - components_init = create_ident_expression("_components"); - } - - // For JSX IDs that can’t be represented as JavaScript IDs (as in, - // those with dashes, such as `custom-element`), we generated a - // separate variable that is a valid JS ID (such as `_component0`), - // and here we take it from components: - // ```js - // const _component0 = _components['custom-element'] - // ``` - if !info.aliases.is_empty() { - info.aliases.reverse(); - - while let Some((id, name)) = info.aliases.pop() { - declarators.push(swc_ecma_ast::VarDeclarator { - span: swc_common::DUMMY_SP, - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident(&name), - type_ann: None, - }), - init: Some(Box::new(swc_ecma_ast::Expr::Member( - swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("_components")), - prop: swc_ecma_ast::MemberProp::Computed( - swc_ecma_ast::ComputedPropName { - expr: Box::new(swc_ecma_ast::Expr::Lit( - swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: id.into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - )), - span: swc_common::DUMMY_SP, - }, - ), - span: swc_common::DUMMY_SP, - }, - ))), - definite: false, - }); - } - } - - if let Some(pat) = components_pattern { - declarators.push(swc_ecma_ast::VarDeclarator { - name: swc_ecma_ast::Pat::Object(pat), - init: Some(Box::new(components_init)), - span: swc_common::DUMMY_SP, - definite: false, - }); - } - - // Add the variable declaration. - statements.push(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( - swc_ecma_ast::VarDecl { - kind: swc_ecma_ast::VarDeclKind::Const, - decls: declarators, - span: swc_common::DUMMY_SP, - declare: false, - }, - )))); - } - - // Add checks at runtime to verify that object/components are passed. - // - // ```js - // if (!a) _missingMdxReference("a", false); - // if (!a.b) _missingMdxReference("a.b", true); - // ``` - for (id, component, position) in info.references { - self.create_error_helper = true; - - let mut args = vec![ - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: id.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }, - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( - swc_ecma_ast::Bool { - value: component, - span: swc_common::DUMMY_SP, - }, - ))), - }, - ]; - - // Add the source location if it exists and if `development` is on. - if let Some(position) = position.as_ref() { - if self.development { - args.push(swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: position_to_string(position).into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }) - } - } - - statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { - test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { - op: swc_ecma_ast::UnaryOp::Bang, - arg: Box::new(create_member_expression(&id)), - span: swc_common::DUMMY_SP, - })), - cons: Box::new(swc_ecma_ast::Stmt::Expr(swc_ecma_ast::ExprStmt { - span: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( - "_missingMdxReference", - ))), - args, - type_args: None, - span: swc_common::DUMMY_SP, - })), - })), - alt: None, - span: swc_common::DUMMY_SP, - })); - } - - // Add statements to functions. - if !statements.is_empty() { - let mut body: &mut swc_ecma_ast::BlockStmt = match func { - Func::Expr(expr) => { - if expr.function.body.is_none() { - expr.function.body = Some(swc_ecma_ast::BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }); - } - expr.function.body.as_mut().unwrap() - } - Func::Decl(decl) => { - if decl.function.body.is_none() { - decl.function.body = Some(swc_ecma_ast::BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }); - } - decl.function.body.as_mut().unwrap() - } - Func::Arrow(arr) => { - if let swc_ecma_ast::BlockStmtOrExpr::Expr(expr) = &mut arr.body { - arr.body = - swc_ecma_ast::BlockStmtOrExpr::BlockStmt(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - // To do: figure out non-clone. - arg: Some(expr.clone()), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }); - } - arr.body.as_mut_block_stmt().unwrap() - } - }; - - statements.append(&mut body.stmts.split_off(0)); - body.stmts = statements; - } - } - - /// Get the current function scope. - fn current_fn_scope_mut(&mut self) -> &mut Scope { - let mut index = self.scopes.len(); - - while index > 0 { - index -= 1; - if self.scopes[index].info.is_some() { - return &mut self.scopes[index]; - } - } - - unreachable!("expected scope") - } - - /// Get the current scope. - fn current_scope_mut(&mut self) -> &mut Scope { - self.scopes.last_mut().expect("expected scope") - } - - /// Get the top-level scope’s info. - fn current_top_level_info(&self) -> Option<&Info> { - if let Some(scope) = self.scopes.get(1) { - scope.info.as_ref() - } else { - None - } - } - - /// Get the top-level scope’s info, mutably. - fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { - if let Some(scope) = self.scopes.get_mut(1) { - scope.info.as_mut() - } else { - None - } - } - - /// Check if `id` is in scope. - fn in_scope(&self, id: &String) -> bool { - let mut index = self.scopes.len(); - - while index > 0 { - index -= 1; - if self.scopes[index].defined.contains(id) { - return true; - } - } - - false - } - - /// Add an identifier to a scope. - fn add_id(&mut self, id: String, block: bool) { - let scope = if block { - self.current_scope_mut() - } else { - self.current_fn_scope_mut() - }; - scope.defined.push(id); - } - - // Add a pattern to a scope. - fn add_pat(&mut self, pat: &swc_ecma_ast::Pat, block: bool) { - match pat { - // `x` - swc_ecma_ast::Pat::Ident(d) => self.add_id(d.id.sym.to_string(), block), - // `...x` - swc_ecma_ast::Pat::Array(d) => { - let mut index = 0; - while index < d.elems.len() { - if let Some(d) = &d.elems[index] { - self.add_pat(d, block); - } - index += 1; - } - } - // `...x` - swc_ecma_ast::Pat::Rest(d) => self.add_pat(&d.arg, block), - // `{x=y}` - swc_ecma_ast::Pat::Assign(d) => self.add_pat(&d.left, block), - swc_ecma_ast::Pat::Object(d) => { - let mut index = 0; - while index < d.props.len() { - match &d.props[index] { - // `{...x}` - swc_ecma_ast::ObjectPatProp::Rest(d) => { - self.add_pat(&d.arg, block); - } - // `{key: value}` - swc_ecma_ast::ObjectPatProp::KeyValue(d) => { - self.add_pat(&d.value, block); - } - // `{key}` or `{key = value}` - swc_ecma_ast::ObjectPatProp::Assign(d) => { - self.add_id(d.key.to_string(), block); - } - } - index += 1; - } - } - // Ignore `Invalid` / `Expr`. - _ => {} - } - } -} - -impl<'a> VisitMut for State<'a> { - noop_visit_mut_type!(); - - /// Rewrite JSX identifiers. - fn visit_mut_jsx_element(&mut self, node: &mut swc_ecma_ast::JSXElement) { - // If there is a top-level, non-global, scope which is a function. - if let Some(info) = self.current_top_level_info() { - // Rewrite only if we can rewrite. - if is_props_receiving_fn(&info.name) || self.provider { - let position = span_to_position(&node.span, self.location); - match &node.opening.name { - // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`. - swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { - let mut ids = vec![]; - let mut mem = d; - loop { - ids.push(mem.prop.sym.to_string()); - match &mem.obj { - swc_ecma_ast::JSXObject::Ident(d) => { - ids.push(d.sym.to_string()); - break; - } - swc_ecma_ast::JSXObject::JSXMemberExpr(d) => { - mem = d; - } - } - } - ids.reverse(); - let primary_id = ids.first().unwrap().clone(); - let in_scope = self.in_scope(&primary_id); - - if !in_scope { - let info_mut = self.current_top_level_info_mut().unwrap(); - - let mut index = 1; - while index <= ids.len() { - let full_id = ids[0..index].join("."); - let component = index == ids.len(); - if let Some(reference) = - info_mut.references.iter_mut().find(|d| d.0 == full_id) - { - if component { - reference.1 = true; - } - } else { - info_mut - .references - .push((full_id, component, position.clone())) - } - index += 1; - } - - if !info_mut.objects.contains(&primary_id) { - info_mut.objects.push(primary_id); - } - } - } - // `<foo>`, `<Foo>`, `<$>`, `<_bar>`, `<a_b>`. - swc_ecma_ast::JSXElementName::Ident(d) => { - // If the name is a valid ES identifier, and it doesn’t - // start with a lowercase letter, it’s a component. - // For example, `$foo`, `_bar`, `Baz` are all component - // names. - // But `foo` and `b-ar` are tag names. - let id = d.sym.to_string(); - - if is_literal_name(&id) { - // To do: ignore explicit JSX? - - let mut invalid = None; - - let name = if is_identifier_name(&id) { - swc_ecma_ast::JSXElementName::JSXMemberExpr( - swc_ecma_ast::JSXMemberExpr { - obj: swc_ecma_ast::JSXObject::Ident(create_ident( - "_components", - )), - prop: create_ident(&id), - }, - ) - } else { - let name = if let Some(invalid_ref) = - info.aliases.iter().find(|d| d.0 == id) - { - invalid_ref.1.clone() - } else { - let name = format!("_component{}", info.aliases.len()); - invalid = Some((id.clone(), name.clone())); - name - }; - - swc_ecma_ast::JSXElementName::Ident(create_ident(&name)) - }; - - let info_mut = self.current_top_level_info_mut().unwrap(); - - if !info_mut.tags.contains(&id) { - info_mut.tags.push(id); - } - - if let Some(invalid) = invalid { - info_mut.aliases.push(invalid) - } - - if let Some(closing) = node.closing.as_mut() { - closing.name = name.clone(); - } - - node.opening.name = name; - } else { - let mut is_layout = false; - - // The MDXLayout is wrapped in a - if let Some(name) = &info.name { - if name == "MDXContent" && id == "MDXLayout" { - is_layout = true; - } - } - - if !self.in_scope(&id) { - let info_mut = self.current_top_level_info_mut().unwrap(); - - if !is_layout { - if let Some(reference) = - info_mut.references.iter_mut().find(|d| d.0 == id) - { - reference.1 = true; - } else { - info_mut.references.push((id.clone(), true, position)) - } - } - - if !info_mut.components.contains(&id) { - info_mut.components.push(id); - } - } - } - } - // `<xml:thing>`. - swc_ecma_ast::JSXElementName::JSXNamespacedName(_) => { - // Ignore. - } - } - } - } - - node.visit_mut_children_with(self); - } - - /// Add specifiers of import declarations. - fn visit_mut_import_decl(&mut self, node: &mut swc_ecma_ast::ImportDecl) { - let mut index = 0; - while index < node.specifiers.len() { - let ident = match &node.specifiers[index] { - swc_ecma_ast::ImportSpecifier::Default(x) => &x.local.sym, - swc_ecma_ast::ImportSpecifier::Namespace(x) => &x.local.sym, - swc_ecma_ast::ImportSpecifier::Named(x) => &x.local.sym, - }; - self.add_id(ident.to_string(), false); - index += 1; - } - - node.visit_mut_children_with(self); - } - - /// Add patterns of variable declarations. - fn visit_mut_var_decl(&mut self, node: &mut swc_ecma_ast::VarDecl) { - let block = node.kind != swc_ecma_ast::VarDeclKind::Var; - let mut index = 0; - while index < node.decls.len() { - self.add_pat(&node.decls[index].name, block); - index += 1; - } - node.visit_mut_children_with(self); - } - - /// Add identifier of class declaration. - fn visit_mut_class_decl(&mut self, node: &mut swc_ecma_ast::ClassDecl) { - self.add_id(node.ident.sym.to_string(), false); - node.visit_mut_children_with(self); - } - - /// On function declarations, add name, create scope, add parameters. - fn visit_mut_fn_decl(&mut self, node: &mut swc_ecma_ast::FnDecl) { - let id = node.ident.sym.to_string(); - self.add_id(id.clone(), false); - self.enter(Some(Info { - name: Some(id), - ..Default::default() - })); - let mut index = 0; - while index < node.function.params.len() { - self.add_pat(&node.function.params[index].pat, false); - index += 1; - } - node.visit_mut_children_with(self); - // Rewrite. - self.exit_func(Func::Decl(node)); - } - - /// On function expressions, add name, create scope, add parameters. - fn visit_mut_fn_expr(&mut self, node: &mut swc_ecma_ast::FnExpr) { - // Note: `periscopic` adds the ID to the newly generated scope, for - // fn expressions. - // That seems wrong? - let name = if let Some(ident) = &node.ident { - let id = ident.sym.to_string(); - self.add_id(id.clone(), false); - Some(id) - } else { - None - }; - - self.enter(Some(Info { - name, - ..Default::default() - })); - let mut index = 0; - while index < node.function.params.len() { - self.add_pat(&node.function.params[index].pat, false); - index += 1; - } - node.visit_mut_children_with(self); - self.exit_func(Func::Expr(node)); - } - - /// On arrow functions, create scope, add parameters. - fn visit_mut_arrow_expr(&mut self, node: &mut swc_ecma_ast::ArrowExpr) { - self.enter(Some(Info::default())); - let mut index = 0; - while index < node.params.len() { - self.add_pat(&node.params[index], false); - index += 1; - } - node.visit_mut_children_with(self); - self.exit_func(Func::Arrow(node)); - } - - // Blocks. - // Not sure why `periscopic` only does `For`/`ForIn`/`ForOf`/`Block`. - // I added `While`/`DoWhile` here just to be sure. - // But there are more. - /// On for statements, create scope. - fn visit_mut_for_stmt(&mut self, node: &mut swc_ecma_ast::ForStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On for/in statements, create scope. - fn visit_mut_for_in_stmt(&mut self, node: &mut swc_ecma_ast::ForInStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On for/of statements, create scope. - fn visit_mut_for_of_stmt(&mut self, node: &mut swc_ecma_ast::ForOfStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On while statements, create scope. - fn visit_mut_while_stmt(&mut self, node: &mut swc_ecma_ast::WhileStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On do/while statements, create scope. - fn visit_mut_do_while_stmt(&mut self, node: &mut swc_ecma_ast::DoWhileStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On block statements, create scope. - fn visit_mut_block_stmt(&mut self, node: &mut swc_ecma_ast::BlockStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - - /// On catch clauses, create scope, add param. - fn visit_mut_catch_clause(&mut self, node: &mut swc_ecma_ast::CatchClause) { - self.enter(None); - if let Some(pat) = &node.param { - self.add_pat(pat, true); - } - node.visit_mut_children_with(self); - self.exit(); - } -} - -/// Generate an import provider. -/// -/// ```js -/// import { useMDXComponents as _provideComponents } from "x" -/// ``` -fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import( - swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: create_ident("_provideComponents"), - imported: Some(swc_ecma_ast::ModuleExportName::Ident(create_ident( - "useMDXComponents", - ))), - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: Box::new(swc_ecma_ast::Str { - value: source.into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }, - )) -} - -/// Generate an error helper. -/// -/// ```js -/// function _missingMdxReference(id, component) { -/// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); -/// } -/// ``` -fn create_error_helper(development: bool, path: Option<String>) -> swc_ecma_ast::ModuleItem { - let mut parameters = vec![ - swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("id"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }, - swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("component"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }, - ]; - - // Accept a source location (which might be undefiend). - if development { - parameters.push(swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("place"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }) - } - - let mut message = vec![ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "Expected ".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - // `component ? "component" : "object"` - swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { - expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(create_ident_expression("component")), - cons: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "component".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "object".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: " `".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - create_ident_expression("id"), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "` to be defined: you likely forgot to import, pass, or provide it.".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - ]; - - // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` - if development { - message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { - expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(create_ident_expression("place")), - cons: Box::new(create_binary_expression( - vec![ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "\nIt’s referenced in your code at `".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - create_ident_expression("place"), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: if let Some(path) = path { - format!("` in `{}`", path).into() - } else { - "`".into() - }, - span: swc_common::DUMMY_SP, - raw: None, - })), - ], - swc_ecma_ast::BinaryOp::Add, - )), - alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - })) - } - - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( - swc_ecma_ast::FnDecl { - ident: create_ident("_missingMdxReference"), - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: parameters, - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Throw(swc_ecma_ast::ThrowStmt { - arg: Box::new(swc_ecma_ast::Expr::New(swc_ecma_ast::NewExpr { - callee: Box::new(create_ident_expression("Error")), - args: Some(vec![swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(create_binary_expression( - message, - swc_ecma_ast::BinaryOp::Add, - )), - }]), - span: swc_common::DUMMY_SP, - type_args: None, - })), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }, - ))) -} - -/// Check if this function is a props receiving component: it’s one of ours. -fn is_props_receiving_fn(name: &Option<String>) -> bool { - if let Some(name) = name { - name == "_createMdxContent" || name == "MDXContent" - } else { - false - } -} - -/// Check if a name is a literal tag name or an identifier to a component. -fn is_literal_name(name: &str) -> bool { - matches!(name.as_bytes().first(), Some(b'a'..=b'z')) || !is_identifier_name(name) -} - -// Check if a name is a valid identifier name. -fn is_identifier_name(name: &str) -> bool { - for (index, char) in name.chars().enumerate() { - if if index == 0 { - !id_start(char) - } else { - !id_cont(char, false) - } { - return false; - } - } - - true -} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index de203c1..00b4e4e 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,8 +1,2 @@ -pub mod hast; -pub mod hast_util_to_swc; -pub mod mdast_util_to_hast; -pub mod mdx; -pub mod mdx_plugin_recma_document; -pub mod mdx_plugin_recma_jsx_rewrite; pub mod swc; pub mod swc_utils; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index f30561d..e85a239 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -1,116 +1,85 @@ //! Bridge between `markdown-rs` and SWC. -use crate::test_utils::swc_utils::{bytepos_to_point, prefix_error_with_point, RewriteContext}; -use markdown::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal}; -use swc_common::{ - source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned, +extern crate markdown; + +use crate::test_utils::swc_utils::{create_span, RewritePrefixContext}; +use markdown::{MdxExpressionKind, MdxSignal}; +use std::rc::Rc; +use swc_core::common::{ + comments::{Comment, SingleThreadedComments, SingleThreadedCommentsMap}, + source_map::Pos, + BytePos, FileName, SourceFile, Span, Spanned, }; -use swc_ecma_ast::{EsVersion, Expr, Module}; -use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; -use swc_ecma_parser::{ +use swc_core::ecma::ast::{EsVersion, Expr, Module, PropOrSpread}; +use swc_core::ecma::parser::{ error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax, }; -use swc_ecma_visit::VisitMutWith; +use swc_core::ecma::visit::VisitMutWith; /// Lex ESM in MDX with SWC. -#[allow(dead_code)] pub fn parse_esm(value: &str) -> MdxSignal { - let (file, syntax, version) = create_config(value.into()); - let mut errors = vec![]; - let result = parse_file_as_module(&file, syntax, version, None, &mut errors); + let result = parse_esm_core(value); match result { - Err(error) => swc_error_to_signal(&error, "esm", value.len(), 0), - Ok(tree) => { - if errors.is_empty() { - check_esm_ast(&tree) - } else { - swc_error_to_signal(&errors[0], "esm", value.len(), 0) - } - } + Err((span, message)) => swc_error_to_signal(span, &message, value.len()), + Ok(_) => MdxSignal::Ok, } } -/// Parse ESM in MDX with SWC. -/// See `drop_span` in `swc_ecma_utils` for inspiration? -#[allow(dead_code)] -pub fn parse_esm_to_tree( - value: &str, - stops: &[Stop], - location: Option<&Location>, -) -> Result<swc_ecma_ast::Module, String> { +/// Core to parse ESM. +fn parse_esm_core(value: &str) -> Result<Module, (Span, String)> { let (file, syntax, version) = create_config(value.into()); let mut errors = vec![]; let result = parse_file_as_module(&file, syntax, version, None, &mut errors); - let mut rewrite_context = RewriteContext { - stops, - location, - prefix_len: 0, - }; match result { - Err(error) => Err(swc_error_to_error(&error, "esm", &rewrite_context)), - Ok(mut module) => { + Err(error) => Err(( + fix_span(error.span(), 1), + format!( + "Could not parse esm with swc: {}", + swc_error_to_string(&error) + ), + )), + Ok(module) => { if errors.is_empty() { - module.visit_mut_with(&mut rewrite_context); - Ok(module) - } else { - Err(swc_error_to_error(&errors[0], "esm", &rewrite_context)) - } - } - } -} - -/// Lex expressions in MDX with SWC. -#[allow(dead_code)] -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); + let mut index = 0; + while index < module.body.len() { + let node = &module.body[index]; + + if !node.is_module_decl() { + return Err(( + fix_span(node.span(), 1), + "Unexpected statement in code: only import/exports are supported" + .into(), + )); + } - match result { - Err(error) => swc_error_to_signal(&error, "expression", value.len(), prefix.len()), - Ok(tree) => { - if errors.is_empty() { - let expression_end = 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(expression_end, value) - } else { - result + index += 1; } + + Ok(module) } else { - swc_error_to_signal(&errors[0], "expression", value.len(), prefix.len()) + Err(( + fix_span(errors[0].span(), 1), + format!( + "Could not parse esm with swc: {}", + swc_error_to_string(&errors[0]) + ), + )) } } } } -/// Parse ESM in MDX with SWC. -/// See `drop_span` in `swc_ecma_utils` for inspiration? -#[allow(dead_code)] -pub fn parse_expression_to_tree( +fn parse_expression_core( value: &str, kind: &MdxExpressionKind, - stops: &[Stop], - location: Option<&Location>, -) -> Result<Box<swc_ecma_ast::Expr>, String> { +) -> Result<Option<Box<Expr>>, (Span, String)> { + // Empty expressions are OK. + if matches!(kind, MdxExpressionKind::Expression) && whitespace_and_comments(0, value).is_ok() { + return Ok(None); + } + // 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. @@ -123,185 +92,103 @@ pub fn parse_expression_to_tree( 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); - let mut rewrite_context = RewriteContext { - stops, - location, - prefix_len: prefix.len(), - }; match result { - Err(error) => Err(swc_error_to_error(&error, "expression", &rewrite_context)), + Err(error) => Err(( + fix_span(error.span(), prefix.len() + 1), + format!( + "Could not parse expression with swc: {}", + swc_error_to_string(&error) + ), + )), Ok(mut expr) => { if errors.is_empty() { - // Fix positions. - expr.visit_mut_with(&mut rewrite_context); + let expression_end = expr.span().hi.to_usize() - 1; + if let Err((span, reason)) = whitespace_and_comments(expression_end, value) { + return Err((span, reason)); + } - let expr_bytepos = expr.span().lo; + expr.visit_mut_with(&mut RewritePrefixContext { + prefix_len: prefix.len() as u32, + }); if matches!(kind, MdxExpressionKind::AttributeExpression) { - let mut obj = None; + let expr_span = expr.span(); - if let swc_ecma_ast::Expr::Paren(d) = *expr { - if let swc_ecma_ast::Expr::Object(d) = *d.expr { - obj = Some(d) + if let Expr::Paren(d) = *expr { + if let Expr::Object(mut obj) = *d.expr { + if obj.props.len() > 1 { + return Err((obj.span, "Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)".into())); + } + + if let Some(PropOrSpread::Spread(d)) = obj.props.pop() { + return Ok(Some(d.expr)); + } } }; - if let Some(mut obj) = obj { - if obj.props.len() > 1 { - Err(create_error_message( - "Unexpected extra content in spread: only a single spread is supported", - "expression", - bytepos_to_point(&obj.span.lo, location).as_ref() - )) - } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop() - { - Ok(d.expr) - } else { - Err(create_error_message( - "Unexpected prop in spread: only a spread is supported", - "expression", - bytepos_to_point(&obj.span.lo, location).as_ref(), - )) - } - } else { - Err(create_error_message( - "Expected an object spread (`{...spread}`)", - "expression", - bytepos_to_point(&expr_bytepos, location).as_ref(), - )) - } - } else { - Ok(expr) + return Err(( + expr_span, + "Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)".into(), + )); } + + Ok(Some(expr)) } else { - Err(swc_error_to_error( - &errors[0], - "expression", - &rewrite_context, + Err(( + fix_span(errors[0].span(), prefix.len() + 1), + format!( + "Could not parse expression with swc: {}", + swc_error_to_string(&errors[0]) + ), )) } } } } -/// Serialize an SWC module. -/// To do: support comments. -#[allow(dead_code)] -pub fn serialize(module: &Module) -> String { - let mut buf = vec![]; - let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); - // let comm = &program.comments as &dyn swc_common::comments::Comments; - { - let mut emitter = Emitter { - cfg: swc_ecma_codegen::Config { - ..Default::default() - }, - cm: cm.clone(), - // To do: figure out how to pass them. - comments: None, - wr: JsWriter::new(cm, "\n", &mut buf, None), - }; - - emitter.emit_module(module).unwrap(); - } - - String::from_utf8_lossy(&buf).into() -} - -/// 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 relative = fix_swc_position(node.span().lo.to_usize(), 0); - return MdxSignal::Error( - "Unexpected statement in code: only import/exports are supported".into(), - relative, - ); - } +/// Lex expressions in MDX with SWC. +pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { + let result = parse_expression_core(value, kind); - index += 1; + match result { + Err((span, message)) => swc_error_to_signal(span, &message, value.len()), + Ok(_) => MdxSignal::Ok, } - - 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: &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`".into(), 0) - } else { - MdxSignal::Ok - } +// To do: remove this attribute, use it somewhere. +#[allow(dead_code)] +/// Turn SWC comments into a flat vec. +pub fn flat_comments(single_threaded_comments: SingleThreadedComments) -> Vec<Comment> { + let raw_comments = single_threaded_comments.take_all(); + let take = |list: SingleThreadedCommentsMap| { + Rc::try_unwrap(list) + .unwrap() + .into_inner() + .into_values() + .flatten() + .collect::<Vec<_>>() + }; + let mut list = take(raw_comments.0); + list.append(&mut take(raw_comments.1)); + list } /// 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, - name: &str, - value_len: usize, - prefix_len: usize, -) -> MdxSignal { - let reason = create_error_reason(&swc_error_to_string(error), name); - let error_end = fix_swc_position(error.span().hi.to_usize(), prefix_len); +fn swc_error_to_signal(span: Span, reason: &str, value_len: usize) -> MdxSignal { + let error_end = span.hi.to_usize(); if error_end >= value_len { - MdxSignal::Eof(reason) + MdxSignal::Eof(reason.into()) } else { - MdxSignal::Error( - reason, - fix_swc_position(error.span().lo.to_usize(), prefix_len), - ) + MdxSignal::Error(reason.into(), span.lo.to_usize()) } } -fn swc_error_to_error(error: &SwcError, name: &str, context: &RewriteContext) -> String { - create_error_message( - &swc_error_to_string(error), - name, - context - .location - .and_then(|location| { - location.relative_to_point( - context.stops, - fix_swc_position(error.span().lo.to_usize(), context.prefix_len), - ) - }) - .as_ref(), - ) -} - -fn create_error_message(reason: &str, name: &str, point: Option<&Point>) -> String { - prefix_error_with_point(create_error_reason(name, reason), point) -} - -fn create_error_reason(reason: &str, name: &str) -> String { - format!("Could not parse {} with swc: {}", name, reason) -} - /// Turn an SWC error into a string. fn swc_error_to_string(error: &SwcError) -> String { error.kind().msg().into() @@ -313,7 +200,7 @@ fn swc_error_to_string(error: &SwcError) -> String { /// 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 { +fn whitespace_and_comments(mut index: usize, value: &str) -> Result<(), (Span, String)> { let bytes = value.as_bytes(); let len = bytes.len(); let mut in_multiline = false; @@ -329,10 +216,7 @@ fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal { } // 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' { + if bytes[index] == b'\r' || bytes[index] == b'\n' { in_line = false; } } @@ -352,30 +236,27 @@ fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal { } // Outside comment, not whitespace. else { - return MdxSignal::Error( + return Err(( + create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected content after expression".into(), - index, - ); + )); } index += 1; } if in_multiline { - MdxSignal::Error( - "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into(), - index, - ) - } else if in_line { + return Err(( + create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into())); + } + + 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`".into(), - index, - ) - } else { - MdxSignal::Ok + return Err((create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".into())); } + + Ok(()) } /// Create configuration for SWC, shared between ESM and expressions. @@ -401,8 +282,8 @@ fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { ) } -/// 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 +fn fix_span(mut span: Span, offset: usize) -> Span { + span.lo = BytePos::from_usize(span.lo.to_usize() - offset); + span.hi = BytePos::from_usize(span.hi.to_usize() - offset); + span } diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs index ac4c4db..326550b 100644 --- a/tests/test_utils/swc_utils.rs +++ b/tests/test_utils/swc_utils.rs @@ -1,228 +1,38 @@ -//! Lots of helpers for dealing with SWC, particularly from unist. +//! Lots of helpers for dealing with SWC, particularly from unist, and for +//! building its ES AST. -use markdown::{ - mdast::Stop, - unist::{Point, Position}, - Location, -}; +use swc_core::common::{BytePos, Span, SyntaxContext, DUMMY_SP}; +use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut}; -use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; -use swc_ecma_ast::{BinExpr, BinaryOp, Expr, Ident, MemberExpr, MemberProp}; -use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; - -/// Turn a unist position, into an SWC span, of two byte positions. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn position_to_span(position: Option<&Position>) -> Span { - position.map_or(DUMMY_SP, |d| Span { - lo: point_to_bytepos(&d.start), - hi: point_to_bytepos(&d.end), - ctxt: SyntaxContext::empty(), - }) -} - -/// Turn an SWC span, of two byte positions, into a unist position. -/// -/// This assumes the span comes from a fixed tree, or is a dummy. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option<Position> { - let lo = span.lo.0 as usize; - let hi = span.hi.0 as usize; - - if lo > 0 && hi > 0 { - if let Some(location) = location { - if let Some(start) = location.to_point(lo - 1) { - if let Some(end) = location.to_point(hi - 1) { - return Some(Position { start, end }); - } - } - } - } - - None -} - -/// Turn a unist point into an SWC byte position. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn point_to_bytepos(point: &Point) -> BytePos { - BytePos(point.offset as u32 + 1) -} - -/// Turn an SWC byte position into a unist point. -/// -/// This assumes the byte position comes from a fixed tree, or is a dummy. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option<Point> { - let pos = bytepos.0 as usize; - - if pos > 0 { - if let Some(location) = location { - return location.to_point(pos - 1); - } - } - - None -} - -/// Prefix an error message with an optional point. -pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { - if let Some(point) = point { - format!("{}: {}", point_to_string(point), reason) - } else { - reason - } -} - -/// Serialize a unist position for humans. -pub fn position_to_string(position: &Position) -> String { - format!( - "{}-{}", - point_to_string(&position.start), - point_to_string(&position.end) - ) -} - -/// Serialize a unist point for humans. -pub fn point_to_string(point: &Point) -> String { - format!("{}:{}", point.line, point.column) -} - -/// Visitor to fix SWC byte positions. -/// -/// This assumes the byte position comes from an **unfixed** tree. +/// Visitor to fix SWC byte positions by removing a prefix. /// /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they /// > are missing or incremented by `1` when valid. #[derive(Debug, Default, Clone)] -pub struct RewriteContext<'a> { - pub prefix_len: usize, - pub stops: &'a [Stop], - pub location: Option<&'a Location>, +pub struct RewritePrefixContext { + /// Size of prefix considered outside this tree. + pub prefix_len: u32, } -impl<'a> VisitMut for RewriteContext<'a> { +impl VisitMut for RewritePrefixContext { noop_visit_mut_type!(); - // Rewrite spans. + /// Rewrite spans. fn visit_mut_span(&mut self, span: &mut Span) { let mut result = DUMMY_SP; - let lo_rel = span.lo.0 as usize; - let hi_rel = span.hi.0 as usize; - - if lo_rel > self.prefix_len && hi_rel > self.prefix_len { - if let Some(lo_abs) = - Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) - { - if let Some(hi_abs) = - Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) - { - result = Span { - lo: BytePos(lo_abs as u32 + 1), - hi: BytePos(hi_abs as u32 + 1), - ctxt: SyntaxContext::empty(), - }; - } - } + if span.lo.0 > self.prefix_len && span.hi.0 > self.prefix_len { + result = create_span(span.lo.0 - self.prefix_len, span.hi.0 - self.prefix_len); } *span = result; } } -/// Generate an ident. -/// -/// ```js -/// a -/// ``` -pub fn create_ident(sym: &str) -> Ident { - Ident { - sym: sym.into(), - optional: false, - span: DUMMY_SP, - } -} - -/// Generate an ident expression. -/// -/// ```js -/// a -/// ``` -pub fn create_ident_expression(sym: &str) -> Expr { - Expr::Ident(create_ident(sym)) -} - -/// Generate a binary expression. -/// -/// ```js -/// a + b + c -/// a || b -/// ``` -pub fn create_binary_expression(mut exprs: Vec<Expr>, op: BinaryOp) -> Expr { - exprs.reverse(); - - let mut left = None; - - while let Some(right_expr) = exprs.pop() { - left = Some(if let Some(left_expr) = left { - Expr::Bin(BinExpr { - left: Box::new(left_expr), - right: Box::new(right_expr), - op, - span: swc_common::DUMMY_SP, - }) - } else { - right_expr - }); - } - - left.expect("expected one or more expressions") -} - -/// Generate a member expression. -/// -/// ```js -/// a.b -/// a -/// ``` -pub fn create_member_expression(name: &str) -> Expr { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - if parts.len() > 1 { - let mut member = MemberExpr { - obj: Box::new(create_ident_expression(parts[0])), - prop: MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - let mut index = 2; - while index < parts.len() { - member = MemberExpr { - obj: Box::new(Expr::Member(member)), - prop: MemberProp::Ident(create_ident(parts[1])), - span: swc_common::DUMMY_SP, - }; - index += 1; - } - Expr::Member(member) - } else { - create_ident_expression(name) +/// Generate a span. +pub fn create_span(lo: u32, hi: u32) -> Span { + Span { + lo: BytePos(lo), + hi: BytePos(hi), + ctxt: SyntaxContext::default(), } } diff --git a/tests/xxx_hast_util_to_swc.rs b/tests/xxx_hast_util_to_swc.rs deleted file mode 100644 index f58eb65..0000000 --- a/tests/xxx_hast_util_to_swc.rs +++ /dev/null @@ -1,717 +0,0 @@ -mod test_utils; -use pretty_assertions::assert_eq; -use test_utils::{ - hast, - hast_util_to_swc::{hast_util_to_swc, Program}, - swc::serialize, -}; - -#[test] -fn hast_util_to_swc_test() -> Result<(), String> { - let comment_ast = hast_util_to_swc( - &hast::Node::Comment(hast::Comment { - value: "a".into(), - position: None, - }), - None, - None, - )?; - - assert_eq!( - comment_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXFragment( - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children: vec![swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::JSXEmptyExpr( - swc_ecma_ast::JSXEmptyExpr { - span: swc_common::DUMMY_SP, - } - ), - span: swc_common::DUMMY_SP, - }, - )], - span: swc_common::DUMMY_SP, - } - )), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![swc_common::comments::Comment { - kind: swc_common::comments::CommentKind::Block, - text: "a".into(), - span: swc_common::DUMMY_SP, - }], - }, - "should support a `Comment`", - ); - - assert_eq!( - serialize(&comment_ast.module), - // To do: comment should be in this. - "<>{}</>;\n", - "should support a `Comment` (serialize)", - ); - - let element_ast = hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["b".into()]), - )], - children: vec![], - position: None, - }), - None, - None, - )?; - - assert_eq!( - element_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXElement(Box::new( - swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: swc_ecma_ast::JSXElementName::Ident( - swc_ecma_ast::Ident { - span: swc_common::DUMMY_SP, - sym: "a".into(), - optional: false, - } - ), - attrs: vec![swc_ecma_ast::JSXAttrOrSpread::JSXAttr( - swc_ecma_ast::JSXAttr { - name: swc_ecma_ast::JSXAttrName::Ident( - swc_ecma_ast::Ident { - sym: "className".into(), - span: swc_common::DUMMY_SP, - optional: false, - } - ), - value: Some(swc_ecma_ast::JSXAttrValue::Lit( - swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "b".into(), - span: swc_common::DUMMY_SP, - raw: None, - }) - )), - span: swc_common::DUMMY_SP, - }, - )], - self_closing: true, - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: None, - children: vec![], - span: swc_common::DUMMY_SP, - } - ))), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support an `Element`", - ); - - assert_eq!( - serialize(&element_ast.module), - "<a className=\"b\"/>;\n", - "should support an `Element` (serialize)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None, - })], - position: None, - }), - None, - None - )? - .module - ), - "<a >{\"a\"}</a>;\n", - "should support an `Element` w/ children", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::String("c".into()),)], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b=\"c\"/>;\n", - "should support an `Element` w/ a string attribute", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(true),)], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b/>;\n", - "should support an `Element` w/ a boolean (true) attribute", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("b".into(), hast::PropertyValue::Boolean(false),)], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a />;\n", - "should support an `Element` w/ a boolean (false) attribute", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![( - "b".into(), - hast::PropertyValue::CommaSeparated(vec!["c".into(), "d".into()]), - )], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b=\"c, d\"/>;\n", - "should support an `Element` w/ a comma-separated attribute", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("data123".into(), hast::PropertyValue::Boolean(true),), - ("dataFoo".into(), hast::PropertyValue::Boolean(true),), - ("dataBAR".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a data-123 data-foo data-b-a-r/>;\n", - "should support an `Element` w/ data attributes", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("role".into(), hast::PropertyValue::Boolean(true),), - ("ariaValueNow".into(), hast::PropertyValue::Boolean(true),), - ("ariaDescribedBy".into(), hast::PropertyValue::Boolean(true),) - ], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a role aria-valuenow aria-describedby/>;\n", - "should support an `Element` w/ aria attributes", - ); - - let mdx_element_ast = hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - }), - None, - None, - )?; - - assert_eq!( - mdx_element_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXFragment( - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children: vec![], - span: swc_common::DUMMY_SP, - } - )), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support an `MdxElement` (fragment)", - ); - - assert_eq!( - serialize(&mdx_element_ast.module), - "<></>;\n", - "should support an `MdxElement` (fragment, serialize)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a />;\n", - "should support an `MdxElement` (element, no children)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a:b".into()), - attributes: vec![], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a:b />;\n", - "should support an `MdxElement` (element, namespace id)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a.b.c".into()), - attributes: vec![], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a.b.c />;\n", - "should support an `MdxElement` (element, member expression)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), - position: None, - })], - position: None, - }), - None, - None - )? - .module - ), - "<a >{\"b\"}</a>;\n", - "should support an `MdxElement` (element, children)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: None - })], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b/>;\n", - "should support an `MdxElement` (element, boolean attribute)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Literal("c".into())) - })], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b=\"c\"/>;\n", - "should support an `MdxElement` (element, attribute w/ literal value)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute { - name: "b".into(), - value: Some(hast::AttributeValue::Expression("c".into(), vec![])) - })], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a b={c}/>;\n", - "should support an `MdxElement` (element, attribute w/ expression value)", - ); - - assert_eq!( - serialize( - &hast_util_to_swc( - &hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: Some("a".into()), - attributes: vec![hast::AttributeContent::Expression("...c".into(), vec![])], - children: vec![], - position: None, - }), - None, - None - )? - .module - ), - "<a {...c}/>;\n", - "should support an `MdxElement` (element, expression attribute)", - ); - - let mdx_expression_ast = hast_util_to_swc( - &hast::Node::MdxExpression(hast::MdxExpression { - value: "a".into(), - position: None, - stops: vec![], - }), - None, - None, - )?; - - assert_eq!( - mdx_expression_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXFragment( - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children: vec![swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(Box::new( - swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "a".into(), - span: swc_common::DUMMY_SP, - optional: false, - }) - )), - span: swc_common::DUMMY_SP, - }, - )], - span: swc_common::DUMMY_SP, - } - )), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support an `MdxExpression`", - ); - - assert_eq!( - serialize(&mdx_expression_ast.module), - "<>{a}</>;\n", - "should support an `MdxExpression` (serialize)", - ); - - let mdxjs_esm_ast = hast_util_to_swc( - &hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: "import a from 'b'".into(), - position: None, - stops: vec![], - }), - None, - None, - )?; - - assert_eq!( - mdxjs_esm_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Default( - swc_ecma_ast::ImportDefaultSpecifier { - local: swc_ecma_ast::Ident { - sym: "a".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - span: swc_common::DUMMY_SP, - } - )], - src: Box::new(swc_ecma_ast::Str { - value: "b".into(), - span: swc_common::DUMMY_SP, - raw: Some("\'b\'".into()), - }), - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }) - )], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support an `MdxjsEsm`", - ); - - assert_eq!( - serialize(&mdxjs_esm_ast.module), - "import a from 'b';\n", - "should support an `MdxjsEsm` (serialize)", - ); - - let root_ast = hast_util_to_swc( - &hast::Node::Root(hast::Root { - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None, - })], - position: None, - }), - None, - None, - )?; - - assert_eq!( - root_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXFragment( - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children: vec![swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(Box::new( - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "a".into(), - span: swc_common::DUMMY_SP, - raw: None, - } - ),) - )), - span: swc_common::DUMMY_SP, - }, - )], - span: swc_common::DUMMY_SP, - } - )), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support a `Root`", - ); - - assert_eq!( - serialize(&root_ast.module), - "<>{\"a\"}</>;\n", - "should support a `Root` (serialize)", - ); - - let text_ast = hast_util_to_swc( - &hast::Node::Text(hast::Text { - value: "a".into(), - position: None, - }), - None, - None, - )?; - - assert_eq!( - text_ast, - Program { - path: None, - module: swc_ecma_ast::Module { - shebang: None, - body: vec![swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(swc_ecma_ast::Expr::JSXFragment( - swc_ecma_ast::JSXFragment { - opening: swc_ecma_ast::JSXOpeningFragment { - span: swc_common::DUMMY_SP, - }, - closing: swc_ecma_ast::JSXClosingFragment { - span: swc_common::DUMMY_SP, - }, - children: vec![swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(Box::new( - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "a".into(), - span: swc_common::DUMMY_SP, - raw: None, - } - ),) - )), - span: swc_common::DUMMY_SP, - }, - )], - span: swc_common::DUMMY_SP, - } - )), - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - }, - comments: vec![], - }, - "should support a `Text`", - ); - - assert_eq!( - serialize(&text_ast.module), - "<>{\"a\"}</>;\n", - "should support a `Text` (serialize)", - ); - - Ok(()) -} diff --git a/tests/xxx_mdast_util_to_hast.rs b/tests/xxx_mdast_util_to_hast.rs deleted file mode 100644 index ffee44a..0000000 --- a/tests/xxx_mdast_util_to_hast.rs +++ /dev/null @@ -1,1566 +0,0 @@ -mod test_utils; -use markdown::mdast; -use pretty_assertions::assert_eq; -use test_utils::{hast, mdast_util_to_hast::mdast_util_to_hast}; - -#[test] -fn mdast_util_to_hast_test() { - assert_eq!( - mdast_util_to_hast(&mdast::Node::BlockQuote(mdast::BlockQuote { - children: vec![], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "blockquote".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - })], - position: None - }), - "should support a `BlockQuote`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Break(mdast::Break { position: None })), - hast::Node::Root(hast::Root { - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "br".into(), - properties: vec![], - children: vec![], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }) - ], - position: None - }), - "should support a `Break`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Code(mdast::Code { - lang: Some("b".into()), - meta: None, - value: "a".into(), - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["language-b".into()]), - ),], - children: vec![hast::Node::Text(hast::Text { - value: "a\n".into(), - position: None - })], - position: None - })], - position: None - }), - "should support a `Code`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Definition(mdast::Definition { - url: "b".into(), - title: None, - identifier: "a".into(), - label: None, - position: None - })), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support a `Definition`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Delete(mdast::Delete { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "del".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support a `Delete`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Emphasis(mdast::Emphasis { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "em".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support an `Emphasis`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::FootnoteDefinition( - mdast::FootnoteDefinition { - identifier: "a".into(), - label: None, - children: vec![], - position: None - } - )), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support a `FootnoteDefinition`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![ - mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition { - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "b".into(), - position: None - })], - position: None - }),], - identifier: "a".into(), - label: None, - position: None - }), - mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::FootnoteReference(mdast::FootnoteReference { - identifier: "a".into(), - label: None, - position: None, - })], - position: None - }), - ], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![ - // Main. - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("href".into(), hast::PropertyValue::String("#fn-a".into()),), - ("id".into(), hast::PropertyValue::String("fnref-a".into()),), - ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), - ( - "ariaDescribedBy".into(), - hast::PropertyValue::String("footnote-label".into()), - ) - ], - children: vec![hast::Node::Text(hast::Text { - value: "1".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - // Footer. - hast::Node::Element(hast::Element { - tag_name: "section".into(), - properties: vec![ - ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]), - ), - ], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "h2".into(), - properties: vec![ - ( - "id".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["sr-only".into(),]), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: "Footnotes".into(), - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "id".into(), - hast::PropertyValue::String("#fn-a".into()), - )], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "b ".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String( - "#fnref-a".into() - ), - ), - ( - "dataFootnoteBackref".into(), - hast::PropertyValue::Boolean(true), - ), - ( - "ariaLabel".into(), - hast::PropertyValue::String( - "Back to content".into() - ), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated( - vec!["data-footnote-backref".into()] - ), - ) - ], - children: vec![hast::Node::Text(hast::Text { - value: "↩".into(), - position: None - }),], - position: None - }) - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support an `FootnoteReference`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![ - mdast::Node::FootnoteDefinition(mdast::FootnoteDefinition { - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "b".into(), - position: None - })], - position: None - }),], - identifier: "a".into(), - label: None, - position: None - }), - mdast::Node::Paragraph(mdast::Paragraph { - children: vec![ - mdast::Node::FootnoteReference(mdast::FootnoteReference { - identifier: "a".into(), - label: None, - position: None, - }), - mdast::Node::FootnoteReference(mdast::FootnoteReference { - identifier: "a".into(), - label: None, - position: None, - }) - ], - position: None - }), - ], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![ - // Main. - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("href".into(), hast::PropertyValue::String("#fn-a".into()),), - ("id".into(), hast::PropertyValue::String("fnref-a".into()),), - ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), - ( - "ariaDescribedBy".into(), - hast::PropertyValue::String("footnote-label".into()), - ) - ], - children: vec![hast::Node::Text(hast::Text { - value: "1".into(), - position: None - })], - position: None - }),], - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ("href".into(), hast::PropertyValue::String("#fn-a".into()),), - ("id".into(), hast::PropertyValue::String("fnref-a-2".into()),), - ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true),), - ( - "ariaDescribedBy".into(), - hast::PropertyValue::String("footnote-label".into()), - ) - ], - children: vec![hast::Node::Text(hast::Text { - value: "1".into(), - position: None - })], - position: None - }),], - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - // Footer. - hast::Node::Element(hast::Element { - tag_name: "section".into(), - properties: vec![ - ("dataFootnotes".into(), hast::PropertyValue::Boolean(true),), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]), - ), - ], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "h2".into(), - properties: vec![ - ( - "id".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["sr-only".into(),]), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: "Footnotes".into(), - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "id".into(), - hast::PropertyValue::String("#fn-a".into()), - )], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "b ".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String( - "#fnref-a".into() - ), - ), - ( - "dataFootnoteBackref".into(), - hast::PropertyValue::Boolean(true), - ), - ( - "ariaLabel".into(), - hast::PropertyValue::String( - "Back to content".into() - ), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated( - vec!["data-footnote-backref".into()] - ), - ) - ], - children: vec![hast::Node::Text(hast::Text { - value: "↩".into(), - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: " ".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String( - "#fnref-a-2".into() - ), - ), - ( - "dataFootnoteBackref".into(), - hast::PropertyValue::Boolean(true), - ), - ( - "ariaLabel".into(), - hast::PropertyValue::String( - "Back to content".into() - ), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated( - vec!["data-footnote-backref".into()] - ), - ) - ], - children: vec![ - hast::Node::Text(hast::Text { - value: "↩".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Text( - hast::Text { - value: "2".into(), - position: None - } - ),], - position: None - }) - ], - position: None - }) - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support an `FootnoteReference` (multiple calls to same definition)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Heading(mdast::Heading { - depth: 1, - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "h1".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support a `Heading`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Html(mdast::Html { - value: "<div>".into(), - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support an `Html`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Image(mdast::Image { - url: "a".into(), - alt: "b".into(), - title: None, - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties: vec![ - ("src".into(), hast::PropertyValue::String("a".into()),), - ("alt".into(), hast::PropertyValue::String("b".into()),) - ], - children: vec![], - position: None - }), - "should support an `Image`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![ - mdast::Node::Definition(mdast::Definition { - url: "b".into(), - title: None, - identifier: "a".into(), - label: None, - position: None - }), - mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::ImageReference(mdast::ImageReference { - reference_kind: mdast::ReferenceKind::Full, - identifier: "a".into(), - alt: "c".into(), - label: None, - position: None, - })], - position: None - }), - ], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties: vec![ - ("src".into(), hast::PropertyValue::String("b".into()),), - ("alt".into(), hast::PropertyValue::String("c".into()),) - ], - children: vec![], - position: None - }),], - position: None - }),], - position: None - }), - "should support an `ImageReference`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::InlineCode(mdast::InlineCode { - value: "a\nb".into(), - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a b".into(), - position: None - })], - position: None - }), - "should support an `InlineCode`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::InlineMath(mdast::InlineMath { - value: "a\nb".into(), - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "language-math".into(), - "math-inline".into() - ]), - ),], - children: vec![hast::Node::Text(hast::Text { - value: "a b".into(), - position: None - })], - position: None - }), - "should support an `InlineMath`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Link(mdast::Link { - url: "a".into(), - title: None, - children: vec![mdast::Node::Text(mdast::Text { - value: "b".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("href".into(), hast::PropertyValue::String("a".into()),),], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), - position: None - })], - position: None - }), - "should support a `Link`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![ - mdast::Node::Definition(mdast::Definition { - url: "b".into(), - title: None, - identifier: "a".into(), - label: None, - position: None - }), - mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::LinkReference(mdast::LinkReference { - reference_kind: mdast::ReferenceKind::Full, - identifier: "a".into(), - label: None, - children: vec![mdast::Node::Text(mdast::Text { - value: "c".into(), - position: None - })], - position: None, - })], - position: None - }), - ], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![("href".into(), hast::PropertyValue::String("b".into()),),], - children: vec![hast::Node::Text(hast::Text { - value: "c".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None - }), - "should support a `LinkReference`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: false, - checked: None, - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - }),], - position: None - }),], - position: None - }), - "should support a `ListItem`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: true, - checked: None, - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }),], - position: None - }), - "should support a `ListItem` (spread: true)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: false, - checked: Some(true), - children: vec![], - position: None - }),], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]) - )], - children: vec![hast::Node::Element(hast::Element { - tag_name: "input".into(), - properties: vec![ - ( - "type".into(), - hast::PropertyValue::String("checkbox".into()), - ), - ("checked".into(), hast::PropertyValue::Boolean(true)), - ("disabled".into(), hast::PropertyValue::Boolean(true)), - ], - children: vec![], - position: None - }),], - position: None - }),], - position: None - }), - "should support a `ListItem` (checked, w/o paragraph)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: false, - checked: Some(false), - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]) - )], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "input".into(), - properties: vec![ - ( - "type".into(), - hast::PropertyValue::String("checkbox".into()), - ), - ("checked".into(), hast::PropertyValue::Boolean(false)), - ("disabled".into(), hast::PropertyValue::Boolean(true)), - ], - children: vec![], - position: None - }), - hast::Node::Text(hast::Text { - value: " ".into(), - position: None - }), - hast::Node::Text(hast::Text { - value: "a".into(), - position: None - }), - ], - position: None - }),], - position: None - }), - "should support a `ListItem` (unchecked, w/ paragraph)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::List(mdast::List { - ordered: true, - start: Some(1), - spread: false, - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: false, - checked: None, - children: vec![mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None - }),], - position: None - }),], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support a `List` (ordered, start: 1)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::List(mdast::List { - ordered: true, - start: Some(123), - spread: false, - children: vec![], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![("start".into(), hast::PropertyValue::String("123".into()),),], - children: vec![hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - })], - position: None - }), - "should support a `List` (ordered, start: 123)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::List(mdast::List { - ordered: false, - start: None, - spread: false, - children: vec![], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "ul".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - })], - position: None - }), - "should support a `List` (unordered)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::List(mdast::List { - ordered: false, - start: None, - spread: false, - children: vec![mdast::Node::ListItem(mdast::ListItem { - spread: false, - checked: Some(true), - children: vec![], - position: None - }),], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "ul".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]) - )], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]) - )], - children: vec![hast::Node::Element(hast::Element { - tag_name: "input".into(), - properties: vec![ - ( - "type".into(), - hast::PropertyValue::String("checkbox".into()), - ), - ("checked".into(), hast::PropertyValue::Boolean(true)), - ("disabled".into(), hast::PropertyValue::Boolean(true)), - ], - children: vec![], - position: None - }),], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support a `List` (w/ checked item)", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Math(mdast::Math { - meta: None, - value: "a".into(), - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "language-math".into(), - "math-display".into() - ]), - ),], - children: vec![hast::Node::Text(hast::Text { - value: "a\n".into(), - position: None - })], - position: None - })], - position: None - }), - "should support a `Math`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::MdxFlowExpression(mdast::MdxFlowExpression { - value: "a".into(), - position: None, - stops: vec![] - })), - hast::Node::MdxExpression(hast::MdxExpression { - value: "a".into(), - position: None, - stops: vec![] - }), - "should support an `MdxFlowExpression`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::MdxTextExpression(mdast::MdxTextExpression { - value: "a".into(), - position: None, - stops: vec![] - })), - hast::Node::MdxExpression(hast::MdxExpression { - value: "a".into(), - position: None, - stops: vec![] - }), - "should support an `MdxTextExpression`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::MdxJsxFlowElement(mdast::MdxJsxFlowElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - })), - hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - }), - "should support an `MdxJsxFlowElement`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::MdxJsxTextElement(mdast::MdxJsxTextElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - })), - hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: None, - attributes: vec![], - children: vec![], - position: None, - }), - "should support an `MdxJsxTextElement`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::MdxjsEsm(mdast::MdxjsEsm { - value: "a".into(), - position: None, - stops: vec![] - })), - hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: "a".into(), - position: None, - stops: vec![] - }), - "should support an `MdxjsEsm`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Paragraph(mdast::Paragraph { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support a `Paragraph`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Root(mdast::Root { - children: vec![], - position: None, - })), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support a `Root`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Strong(mdast::Strong { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "strong".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support a `Strong`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "td".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - "should support a `TableCell`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::TableRow(mdast::TableRow { - children: vec![ - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - }), - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "b".into(), - position: None - })], - position: None, - }) - ], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "tr".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "td".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "td".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support a `TableRow`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Table(mdast::Table { - align: vec![mdast::AlignKind::Left, mdast::AlignKind::None], - children: vec![ - mdast::Node::TableRow(mdast::TableRow { - children: vec![ - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None - })], - position: None, - }), - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "b".into(), - position: None - })], - position: None, - }) - ], - position: None, - }), - mdast::Node::TableRow(mdast::TableRow { - children: vec![ - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "c".into(), - position: None - })], - position: None, - }), - mdast::Node::TableCell(mdast::TableCell { - children: vec![mdast::Node::Text(mdast::Text { - value: "d".into(), - position: None - })], - position: None, - }) - ], - position: None, - }) - ], - position: None, - })), - hast::Node::Element(hast::Element { - tag_name: "table".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "thead".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "tr".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "th".into(), - properties: vec![( - "align".into(), - hast::PropertyValue::String("left".into()), - ),], - children: vec![hast::Node::Text(hast::Text { - value: "a".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "th".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "b".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "tbody".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "tr".into(), - properties: vec![], - children: vec![ - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "td".into(), - properties: vec![( - "align".into(), - hast::PropertyValue::String("left".into()), - ),], - children: vec![hast::Node::Text(hast::Text { - value: "c".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - hast::Node::Element(hast::Element { - tag_name: "td".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: "d".into(), - position: None - })], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None - }), - ], - position: None - }), - "should support a `Table`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Text(mdast::Text { - value: "a".into(), - position: None, - })), - hast::Node::Text(hast::Text { - value: "a".into(), - position: None - }), - "should support a `Text`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::ThematicBreak(mdast::ThematicBreak { - position: None - })), - hast::Node::Element(hast::Element { - tag_name: "hr".into(), - properties: vec![], - children: vec![], - position: None - }), - "should support a `Thematicbreak`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Yaml(mdast::Yaml { - value: "a".into(), - position: None - })), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support a `Yaml`", - ); - - assert_eq!( - mdast_util_to_hast(&mdast::Node::Toml(mdast::Toml { - value: "a".into(), - position: None - })), - hast::Node::Root(hast::Root { - children: vec![], - position: None - }), - "should support a `Toml`", - ); -} diff --git a/tests/xxx_mdx.rs b/tests/xxx_mdx.rs deleted file mode 100644 index eb8a211..0000000 --- a/tests/xxx_mdx.rs +++ /dev/null @@ -1,150 +0,0 @@ -mod test_utils; -use pretty_assertions::assert_eq; -use test_utils::mdx::{mdx, JsxRuntime, Options}; - -#[test] -fn mdx_test() -> Result<(), String> { - // To do: JSX should be compiled away. - assert_eq!( - mdx("", Some("example.mdx".into()), &Options::default())?, - "function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should work", - ); - - // To do: JSX should be compiled away. - assert_eq!( - mdx("<A />", Some("example.mdx".into()), &Options { - development: true, - ..Default::default() - })?, - "function _createMdxContent(props) { - const { A } = props.components || {}; - if (!A) _missingMdxReference(\"A\", true, \"1:1-1:6\"); - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component, place) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\")); -} -", - "should support `options.development: true`", - ); - - // To do: JSX should be compiled away. - assert_eq!( - mdx("<A />", Some("example.mdx".into()), &Options { - provider_import_source: Some("@mdx-js/react".into()), - ..Default::default() - })?, - "import { useMDXComponents as _provideComponents } from \"@mdx-js/react\"; -function _createMdxContent(props) { - const { A } = Object.assign({}, _provideComponents(), props.components); - if (!A) _missingMdxReference(\"A\", true); - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support `options.provider_import_source`", - ); - - assert_eq!( - mdx("", Some("example.mdx".into()), &Options { - jsx: true, - ..Default::default() - })?, - "function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support `options.jsx: true`", - ); - - // To do: JSX should be compiled away. - // To do: should use calls of `React.createElement` / `React.Fragment`. - assert_eq!( - mdx("", Some("example.mdx".into()), &Options { - jsx_runtime: Some(JsxRuntime::Classic), - ..Default::default() - })?, - "import { React } from \"react\"; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support `options.jsx_runtime: JsxRuntime::Classic`", - ); - - // To do: JSX should be compiled away. - // To do: should import `_jsx` and such. - // To do: should use calls of `_jsx`, etc. - assert_eq!( - mdx("", Some("example.mdx".into()), &Options { - jsx_import_source: Some("preact".into()), - ..Default::default() - })?, - "function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support `options.jsx_import_source: Some(\"preact\".into())`", - ); - - // To do: JSX should be compiled away. - // To do: should use calls of `a.b`, symbol of `a.c`. - assert_eq!( - mdx("", Some("example.mdx".into()), &Options { - jsx_runtime: Some(JsxRuntime::Classic), - pragma: Some("a.b".into()), - pragma_frag: Some("a.c".into()), - pragma_import_source: Some("d".into()), - ..Default::default() - })?, - "import { a } from \"d\"; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support `options.pragma`, `options.pragma_frag`, `options.pragma_import_source`", - ); - - Ok(()) -} diff --git a/tests/xxx_mdx_plugin_recma_document.rs b/tests/xxx_mdx_plugin_recma_document.rs deleted file mode 100644 index c534d58..0000000 --- a/tests/xxx_mdx_plugin_recma_document.rs +++ /dev/null @@ -1,207 +0,0 @@ -mod test_utils; -use markdown::{to_mdast, Location, ParseOptions}; -use pretty_assertions::assert_eq; -use test_utils::{ - hast_util_to_swc::hast_util_to_swc, - mdast_util_to_hast::mdast_util_to_hast, - mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions}, - swc::{parse_esm, parse_expression, serialize}, -}; - -fn from_markdown(value: &str) -> Result<String, String> { - let location = Location::new(value.as_bytes()); - let mdast = to_mdast( - value, - &ParseOptions { - mdx_esm_parse: Some(Box::new(parse_esm)), - mdx_expression_parse: Some(Box::new(parse_expression)), - ..ParseOptions::mdx() - }, - )?; - let hast = mdast_util_to_hast(&mdast); - let mut program = hast_util_to_swc(&hast, None, Some(&location))?; - mdx_plugin_recma_document(&mut program, &DocumentOptions::default(), Some(&location))?; - Ok(serialize(&program.module)) -} - -#[test] -fn mdx_plugin_recma_document_test() -> Result<(), String> { - assert_eq!( - from_markdown("# hi\n\nAlpha *bravo* **charlie**.")?, - "function _createMdxContent(props) { - return <><h1 >{\"hi\"}</h1>{\"\\n\"}<p >{\"Alpha \"}<em >{\"bravo\"}</em>{\" \"}<strong >{\"charlie\"}</strong>{\".\"}</p></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a small program", - ); - - assert_eq!( - from_markdown("import a from 'b'\n\n# {a}")?, - "import a from 'b'; -function _createMdxContent(props) { - return <h1 >{a}</h1>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support an import", - ); - - assert_eq!( - from_markdown("export * from 'a'\n\n# b")?, - "export * from 'a'; -function _createMdxContent(props) { - return <h1 >{\"b\"}</h1>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support an export all", - ); - - assert_eq!( - from_markdown("export function a() {}")?, - "export function a() {} -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support an export declaration", - ); - - assert_eq!( - from_markdown("export default a")?, - "const MDXLayout = a; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout>; -} -export default MDXContent; -", - "should support an export default expression", - ); - - assert_eq!( - from_markdown("export default function () {}")?, - "const MDXLayout = function() {}; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout>; -} -export default MDXContent; -", - "should support an export default declaration", - ); - - assert_eq!( - from_markdown("export {a, b as default}")?, - "export { a }; -const MDXLayout = b; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout>; -} -export default MDXContent; -", - "should support a named export w/o source, w/ a default specifier", - ); - - assert_eq!( - from_markdown("export {a}")?, - "export { a }; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a named export w/o source, w/o a default specifier", - ); - - assert_eq!( - from_markdown("export {}")?, - "export { }; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a named export w/o source, w/o a specifiers", - ); - - assert_eq!( - from_markdown("export {a, b as default} from 'c'")?, - "export { a } from 'c'; -import { b as MDXLayout } from 'c'; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout>; -} -export default MDXContent; -", - "should support a named export w/ source, w/ a default specifier", - ); - - assert_eq!( - from_markdown("export {a} from 'b'")?, - "export { a } from 'b'; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a named export w/ source, w/o a default specifier", - ); - - assert_eq!( - from_markdown("export {} from 'a'")?, - "export { } from 'a'; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a named export w/ source, w/o a specifiers", - ); - - assert_eq!( - from_markdown("export default a = 1\n\nexport default b = 2") - .err() - .unwrap(), - "3:1: Cannot specify multiple layouts (previous: 1:1-1:21 (0-20))", - "should crash on a comment spread" - ); - - Ok(()) -} diff --git a/tests/xxx_mdx_plugin_recma_jsx_rewrite.rs b/tests/xxx_mdx_plugin_recma_jsx_rewrite.rs deleted file mode 100644 index 77c794a..0000000 --- a/tests/xxx_mdx_plugin_recma_jsx_rewrite.rs +++ /dev/null @@ -1,375 +0,0 @@ -mod test_utils; -use markdown::{to_mdast, Location, ParseOptions}; -use pretty_assertions::assert_eq; -use test_utils::{ - hast_util_to_swc::hast_util_to_swc, - mdast_util_to_hast::mdast_util_to_hast, - mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions}, - mdx_plugin_recma_jsx_rewrite::{mdx_plugin_recma_jsx_rewrite, Options as RewriteOptions}, - swc::{parse_esm, parse_expression, serialize}, -}; - -fn from_markdown(value: &str, options: &RewriteOptions) -> Result<String, String> { - let location = Location::new(value.as_bytes()); - let mdast = to_mdast( - value, - &ParseOptions { - mdx_esm_parse: Some(Box::new(parse_esm)), - mdx_expression_parse: Some(Box::new(parse_expression)), - ..ParseOptions::mdx() - }, - )?; - let hast = mdast_util_to_hast(&mdast); - let mut program = hast_util_to_swc(&hast, Some("example.mdx".into()), Some(&location))?; - mdx_plugin_recma_document(&mut program, &DocumentOptions::default(), Some(&location))?; - mdx_plugin_recma_jsx_rewrite(&mut program, options, Some(&location)); - Ok(serialize(&program.module)) -} - -#[test] -fn mdx_plugin_recma_jsx_rewrite_test() -> Result<(), String> { - assert_eq!( - from_markdown("", &Default::default())?, - "function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should work on an empty file", - ); - - assert_eq!( - from_markdown("# hi", &Default::default())?, - "function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, props.components); - return <_components.h1 >{\"hi\"}</_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support passing in a layout (as `wrapper`) and components for literal tags", - ); - - assert_eq!( - from_markdown( - "export {MyLayout as default} from './a.js'\n\n# hi", - &Default::default() - )?, - "import { MyLayout as MDXLayout } from './a.js'; -function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, props.components); - return <_components.h1 >{\"hi\"}</_components.h1>; -} -function MDXContent(props = {}) { - return <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout>; -} -export default MDXContent; -", - "should not support passing in a layout if one is defined locally", - ); - - assert_eq!( - from_markdown("# <Hi />", &Default::default())?, - "function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, props.components), { Hi } = _components; - if (!Hi) _missingMdxReference(\"Hi\", true); - return <_components.h1 ><Hi /></_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support passing in a component", - ); - - assert_eq!( - from_markdown("<X />, <X.y />, <Y.Z />", &Default::default())?, - "function _createMdxContent(props) { - const _components = Object.assign({ - p: \"p\" - }, props.components), { X , Y } = _components; - if (!X) _missingMdxReference(\"X\", true); - if (!X.y) _missingMdxReference(\"X.y\", true); - if (!Y) _missingMdxReference(\"Y\", false); - if (!Y.Z) _missingMdxReference(\"Y.Z\", true); - return <_components.p ><X />{\", \"}<X.y />{\", \"}<Y.Z /></_components.p>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support passing in component objects", - ); - - assert_eq!( - from_markdown("import {Hi} from './a.js'\n\n# <Hi />", &Default::default())?, - "import { Hi } from './a.js'; -function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, props.components); - return <_components.h1 ><Hi /></_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should not support passing in a component if one is defined locally", - ); - - assert_eq!( - from_markdown("# <a-b />", &Default::default())?, - "function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\", - \"a-b\": \"a-b\" - }, props.components), _component0 = _components[\"a-b\"]; - return <_components.h1 ><_component0 /></_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support passing in a component for a JSX identifier that is not a valid JS identifier", - ); - - assert_eq!( - from_markdown("# <Hi />", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() - })?, - "import { useMDXComponents as _provideComponents } from \"x\"; -function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, _provideComponents(), props.components), { Hi } = _components; - if (!Hi) _missingMdxReference(\"Hi\", true); - return <_components.h1 ><Hi /></_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support providing a layout, literal tags, and components", - ); - - assert_eq!( - from_markdown("", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() - })?, - "import { useMDXComponents as _provideComponents } from \"x\"; -function _createMdxContent(props) { - return <></>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support a provider on an empty file", - ); - - assert_eq!( - from_markdown("<X />, <X.y />, <Y.Z />", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() - })?, - "import { useMDXComponents as _provideComponents } from \"x\"; -function _createMdxContent(props) { - const _components = Object.assign({ - p: \"p\" - }, _provideComponents(), props.components), { X , Y } = _components; - if (!X) _missingMdxReference(\"X\", true); - if (!X.y) _missingMdxReference(\"X.y\", true); - if (!Y) _missingMdxReference(\"Y\", false); - if (!Y.Z) _missingMdxReference(\"Y.Z\", true); - return <_components.p ><X />{\", \"}<X.y />{\", \"}<Y.Z /></_components.p>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support providing component objects", - ); - - assert_eq!( - from_markdown("export function A() { - return <B /> -} - -<A /> -", &Default::default())?, - "export function A() { - return <B />; -} -function _createMdxContent(props) { - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should not support passing components in locally defined components", - ); - - assert_eq!( - from_markdown("export function A() { - return <B /> -} - -<A /> -", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() -})?, - "import { useMDXComponents as _provideComponents } from \"x\"; -export function A() { - const { B } = _provideComponents(); - if (!B) _missingMdxReference(\"B\", true); - return <B />; -} -function _createMdxContent(props) { - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support providing components in locally defined components", - ); - - assert_eq!( - from_markdown("export function A() { - return <b-c /> -} - -<A /> -", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() -})?, - "import { useMDXComponents as _provideComponents } from \"x\"; -export function A() { - const _components = Object.assign({ - \"b-c\": \"b-c\" - }, _provideComponents()), _component0 = _components[\"b-c\"]; - return <_component0 />; -} -function _createMdxContent(props) { - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -", - "should support providing components with JSX identifiers that are not JS identifiers in locally defined components", - ); - - assert_eq!( - from_markdown("export function A() { - return <X />, <X.y />, <Y.Z /> -} - -<A /> -", &RewriteOptions { - provider_import_source: Some("x".into()), - ..Default::default() -})?, - "import { useMDXComponents as _provideComponents } from \"x\"; -export function A() { - const { X , Y } = _provideComponents(); - if (!X) _missingMdxReference(\"X\", true); - if (!X.y) _missingMdxReference(\"X.y\", true); - if (!Y) _missingMdxReference(\"Y\", false); - if (!Y.Z) _missingMdxReference(\"Y.Z\", true); - return <X />, <X.y />, <Y.Z />; -} -function _createMdxContent(props) { - return <A />; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); -} -", - "should support providing components with JSX identifiers that are not JS identifiers in locally defined components", - ); - - assert_eq!( - from_markdown("# <Hi />", &RewriteOptions { - development: true, - ..Default::default() - })?, - "function _createMdxContent(props) { - const _components = Object.assign({ - h1: \"h1\" - }, props.components), { Hi } = _components; - if (!Hi) _missingMdxReference(\"Hi\", true, \"1:3-1:9\"); - return <_components.h1 ><Hi /></_components.h1>; -} -function MDXContent(props = {}) { - const { wrapper: MDXLayout } = props.components || {}; - return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props); -} -export default MDXContent; -function _missingMdxReference(id, component, place) { - throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\")); -} -", - "should create missing reference helpers w/o positional info in `development` mode", - ); - - Ok(()) -} |