use crate::hast;
use crate::swc::{parse_esm_to_tree, parse_expression_to_tree};
use crate::swc_utils::{
create_jsx_attr_name_from_str, create_jsx_name_from_str, inter_element_whitespace,
position_to_span,
};
use core::str;
use markdown::{Location, MdxExpressionKind};
use swc_core::ecma::ast::{
Expr, ExprStmt, JSXAttr, JSXAttrOrSpread, JSXAttrValue, JSXClosingElement, JSXClosingFragment,
JSXElement, JSXElementChild, JSXEmptyExpr, JSXExpr, JSXExprContainer, JSXFragment,
JSXOpeningElement, JSXOpeningFragment, Lit, Module, ModuleItem, SpreadElement, Stmt, Str,
};
pub const MAGIC_EXPLICIT_MARKER: u32 = 1337;
#[derive(Debug, PartialEq, Eq)]
pub struct Program {
pub path: Option<String>,
pub module: Module,
pub comments: Vec<swc_core::common::comments::Comment>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Space {
Html,
Svg,
}
#[derive(Debug)]
struct Context<'a> {
space: Space,
comments: Vec<swc_core::common::comments::Comment>,
esm: Vec<ModuleItem>,
location: Option<&'a Location>,
}
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(JSXElementChild::JSXFragment(x)) => Some(Expr::JSXFragment(x)),
Some(JSXElementChild::JSXElement(x)) => Some(Expr::JSXElement(x)),
Some(child) => Some(Expr::JSXFragment(create_fragment(vec![child], tree))),
None => None,
};
let mut module = Module {
shebang: None,
body: context.esm,
span: position_to_span(tree.position()),
};
if let Some(expr) = expr {
module.body.push(ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(expr),
span: swc_core::common::DUMMY_SP,
})));
}
Ok(Program {
path,
module,
comments: context.comments,
})
}
fn one(context: &mut Context, node: &hast::Node) -> Result<Option<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),
hast::Node::Doctype(_) => None,
};
Ok(value)
}
fn all(context: &mut Context, parent: &hast::Node) -> Result<Vec<JSXElementChild>, String> {
let mut result = vec![];
if let Some(children) = parent.children() {
let mut index = 0;
while index < children.len() {
let child = &children[index];
if let Some(child) = one(context, child)? {
result.push(child);
}
index += 1;
}
}
Ok(result)
}
fn transform_comment(
context: &mut Context,
node: &hast::Node,
comment: &hast::Comment,
) -> JSXElementChild {
context.comments.push(swc_core::common::comments::Comment {
kind: swc_core::common::comments::CommentKind::Block,
text: comment.value.clone().into(),
span: position_to_span(node.position()),
});
JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::JSXEmptyExpr(JSXEmptyExpr {
span: position_to_span(node.position()),
}),
span: position_to_span(node.position()),
})
}
fn transform_element(
context: &mut Context,
node: &hast::Node,
element: &hast::Element,
) -> Result<Option<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];
let value = match &prop.1 {
hast::PropertyValue::Boolean(x) => {
if *x {
None
} else {
index += 1;
continue;
}
}
hast::PropertyValue::String(x) => Some(Lit::Str(Str {
value: x.clone().into(),
span: swc_core::common::DUMMY_SP,
raw: None,
})),
hast::PropertyValue::CommaSeparated(x) => Some(Lit::Str(Str {
value: x.join(", ").into(),
span: swc_core::common::DUMMY_SP,
raw: None,
})),
hast::PropertyValue::SpaceSeparated(x) => Some(Lit::Str(Str {
value: x.join(" ").into(),
span: swc_core::common::DUMMY_SP,
raw: None,
})),
};
let attr_name = prop_to_attr_name(&prop.0);
attrs.push(JSXAttrOrSpread::JSXAttr(JSXAttr {
name: create_jsx_attr_name_from_str(&attr_name),
value: value.map(JSXAttrValue::Lit),
span: swc_core::common::DUMMY_SP,
}));
index += 1;
}
Ok(Some(JSXElementChild::JSXElement(create_element(
&element.tag_name,
attrs,
children,
node,
false,
))))
}
fn transform_mdx_jsx_element(
context: &mut Context,
node: &hast::Node,
element: &hast::MdxJsxElement,
) -> Result<Option<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(JSXAttrValue::Lit(Lit::Str(Str {
value: x.clone().into(),
span: swc_core::common::DUMMY_SP,
raw: None,
})))
}
Some(hast::AttributeValue::Expression(expression)) => {
Some(JSXAttrValue::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(
parse_expression_to_tree(
&expression.value,
&MdxExpressionKind::AttributeValueExpression,
&expression.stops,
context.location,
)?
.unwrap(),
),
span: swc_core::common::DUMMY_SP,
}))
}
None => None,
};
JSXAttrOrSpread::JSXAttr(JSXAttr {
span: swc_core::common::DUMMY_SP,
name: create_jsx_attr_name_from_str(&prop.name),
value,
})
}
hast::AttributeContent::Expression { value, stops } => {
let expr = parse_expression_to_tree(
value,
&MdxExpressionKind::AttributeExpression,
stops,
context.location,
)?;
JSXAttrOrSpread::SpreadElement(SpreadElement {
dot3_token: swc_core::common::DUMMY_SP,
expr: expr.unwrap(),
})
}
};
attrs.push(attr);
index += 1;
}
Ok(Some(if let Some(name) = &element.name {
JSXElementChild::JSXElement(create_element(name, attrs, children, node, true))
} else {
JSXElementChild::JSXFragment(create_fragment(children, node))
}))
}
fn transform_mdx_expression(
context: &mut Context,
node: &hast::Node,
expression: &hast::MdxExpression,
) -> Result<Option<JSXElementChild>, String> {
let expr = parse_expression_to_tree(
&expression.value,
&MdxExpressionKind::Expression,
&expression.stops,
context.location,
)?;
let span = position_to_span(node.position());
let child = if let Some(expr) = expr {
JSXExpr::Expr(expr)
} else {
JSXExpr::JSXEmptyExpr(JSXEmptyExpr { span })
};
Ok(Some(JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: child,
span,
})))
}
fn transform_mdxjs_esm(
context: &mut Context,
_node: &hast::Node,
esm: &hast::MdxjsEsm,
) -> Result<Option<JSXElementChild>, String> {
let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?;
context.esm.append(&mut module.body);
Ok(None)
}
fn transform_root(
context: &mut Context,
node: &hast::Node,
_root: &hast::Root,
) -> Result<Option<JSXElementChild>, String> {
let mut children = all(context, node)?;
let mut queue = vec![];
let mut nodes = vec![];
let mut seen = false;
children.reverse();
while let Some(child) = children.pop() {
let mut stash = false;
if let JSXElementChild::JSXExprContainer(container) = &child {
if let JSXExpr::Expr(expr) = &container.expr {
if let Expr::Lit(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(JSXElementChild::JSXFragment(create_fragment(
nodes, node,
))))
}
fn transform_text(
_context: &mut Context,
node: &hast::Node,
text: &hast::Text,
) -> Option<JSXElementChild> {
if text.value.is_empty() {
None
} else {
Some(JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Str(Str {
value: text.value.clone().into(),
span: position_to_span(node.position()),
raw: None,
})))),
span: position_to_span(node.position()),
}))
}
}
fn create_element(
name: &str,
attrs: Vec<JSXAttrOrSpread>,
children: Vec<JSXElementChild>,
node: &hast::Node,
explicit: bool,
) -> Box<JSXElement> {
let mut span = position_to_span(node.position());
span.ctxt = if explicit {
swc_core::common::SyntaxContext::from_u32(MAGIC_EXPLICIT_MARKER)
} else {
swc_core::common::SyntaxContext::empty()
};
Box::new(JSXElement {
opening: JSXOpeningElement {
name: create_jsx_name_from_str(name),
attrs,
self_closing: children.is_empty(),
type_args: None,
span: swc_core::common::DUMMY_SP,
},
closing: if children.is_empty() {
None
} else {
Some(JSXClosingElement {
name: create_jsx_name_from_str(name),
span: swc_core::common::DUMMY_SP,
})
},
children,
span,
})
}
fn create_fragment(children: Vec<JSXElementChild>, node: &hast::Node) -> JSXFragment {
JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children,
span: position_to_span(node.position()),
}
}
fn prop_to_attr_name(prop: &str) -> String {
if prop.len() > 4 && prop.starts_with("data") {
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 {
result.push_str(&prop[start..index]);
if byte != b'-' {
result.push('-');
}
result.push(byte.to_ascii_lowercase().into());
start = index + 1;
}
index += 1;
}
if valid {
result.push_str(&prop[start..]);
return result;
}
}
PROP_TO_REACT_PROP
.iter()
.find(|d| d.0 == prop)
.or_else(|| PROP_TO_ATTR_EXCEPTIONS_SHARED.iter().find(|d| d.0 == prop))
.map_or_else(|| prop.into(), |d| d.1.into())
}
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"),
];
#[cfg(test)]
mod tests {
use super::*;
use crate::hast;
use crate::hast_util_to_swc::{hast_util_to_swc, Program};
use crate::markdown::mdast;
use crate::swc::serialize;
use pretty_assertions::assert_eq;
use swc_core::ecma::ast::{
Ident, ImportDecl, ImportDefaultSpecifier, ImportSpecifier, JSXAttrName, JSXElementName,
ModuleDecl,
};
#[test]
fn comments() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXFragment(JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children: vec![JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::JSXEmptyExpr(JSXEmptyExpr {
span: swc_core::common::DUMMY_SP,
}),
span: swc_core::common::DUMMY_SP,
},)],
span: swc_core::common::DUMMY_SP,
})),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![swc_core::common::comments::Comment {
kind: swc_core::common::comments::CommentKind::Block,
text: "a".into(),
span: swc_core::common::DUMMY_SP,
}],
},
"should support a `Comment`",
);
assert_eq!(
serialize(&mut comment_ast.module, Some(&comment_ast.comments)),
"<>{}</>;\n",
"should support a `Comment` (serialize)",
);
Ok(())
}
#[test]
fn elements() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXElement(Box::new(JSXElement {
opening: JSXOpeningElement {
name: JSXElementName::Ident(Ident {
span: swc_core::common::DUMMY_SP,
sym: "a".into(),
optional: false,
}),
attrs: vec![JSXAttrOrSpread::JSXAttr(JSXAttr {
name: JSXAttrName::Ident(Ident {
sym: "className".into(),
span: swc_core::common::DUMMY_SP,
optional: false,
}),
value: Some(JSXAttrValue::Lit(Lit::Str(Str {
value: "b".into(),
span: swc_core::common::DUMMY_SP,
raw: None,
}))),
span: swc_core::common::DUMMY_SP,
},)],
self_closing: true,
type_args: None,
span: swc_core::common::DUMMY_SP,
},
closing: None,
children: vec![],
span: swc_core::common::DUMMY_SP,
}))),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support an `Element`",
);
assert_eq!(
serialize(&mut element_ast.module, Some(&element_ast.comments)),
"<a className=\"b\"/>;\n",
"should support an `Element` (serialize)",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a>{\"a\"}</a>;\n",
"should support an `Element` w/ children",
);
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::Element(hast::Element {
tag_name: "svg".into(),
properties: vec![],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<svg/>;\n",
"should support an `Element` in the SVG space",
);
Ok(())
}
#[test]
fn element_attributes() -> Result<(), String> {
assert_eq!(
serialize(
&mut 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,
None
),
"<a b=\"c\"/>;\n",
"should support an `Element` w/ a string attribute",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a b/>;\n",
"should support an `Element` w/ a boolean (true) attribute",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a/>;\n",
"should support an `Element` w/ a boolean (false) attribute",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a b=\"c, d\"/>;\n",
"should support an `Element` w/ a comma-separated attribute",
);
assert_eq!(
serialize(
&mut 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)),
("data+invalid".into(), hast::PropertyValue::Boolean(true)),
("data--x".into(), hast::PropertyValue::Boolean(true))
],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a data-123 data-foo data-b-a-r data+invalid data--x/>;\n",
"should support an `Element` w/ data attributes",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a role aria-valuenow aria-describedby/>;\n",
"should support an `Element` w/ aria attributes",
);
Ok(())
}
#[test]
fn mdx_element() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXFragment(JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children: vec![],
span: swc_core::common::DUMMY_SP,
})),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support an `MdxElement` (fragment)",
);
assert_eq!(
serialize(&mut mdx_element_ast.module, Some(&mdx_element_ast.comments)),
"<></>;\n",
"should support an `MdxElement` (fragment, serialize)",
);
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a".into()),
attributes: vec![],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a/>;\n",
"should support an `MdxElement` (element, no children)",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a>{\"b\"}</a>;\n",
"should support an `MdxElement` (element, children)",
);
Ok(())
}
#[test]
fn mdx_element_name() -> Result<(), String> {
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a:b".into()),
attributes: vec![],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a:b/>;\n",
"should support an `MdxElement` (element, namespace id)",
);
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a.b.c".into()),
attributes: vec![],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a.b.c/>;\n",
"should support an `MdxElement` (element, member expression)",
);
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("svg".into()),
attributes: vec![],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<svg/>;\n",
"should support an `MdxElement` (element, `<svg>`)",
);
Ok(())
}
#[test]
fn mdx_element_attributes() -> Result<(), String> {
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a".into()),
attributes: vec![hast::AttributeContent::Property(hast::MdxJsxAttribute {
name: "b:c".into(),
value: None
})],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a b:c/>;\n",
"should support an `MdxElement` (element, namespace attribute name)",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a b/>;\n",
"should support an `MdxElement` (element, boolean attribute)",
);
assert_eq!(
serialize(
&mut 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,
None
),
"<a b=\"c\"/>;\n",
"should support an `MdxElement` (element, attribute w/ literal value)",
);
assert_eq!(
serialize(
&mut 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(
mdast::AttributeValueExpression {
value: "c".into(),
stops: vec![]
}
))
})],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a b={c}/>;\n",
"should support an `MdxElement` (element, attribute w/ expression value)",
);
assert_eq!(
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(
mdast::AttributeValueExpression {
value: "!".into(),
stops: vec![]
}
))
})],
children: vec![],
position: None,
}),
None,
None
),
Err("0:0: Could not parse expression with swc: Unexpected eof".into()),
"should support an `MdxElement` (element, attribute w/ broken expression value)",
);
assert_eq!(
serialize(
&mut hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a".into()),
attributes: vec![hast::AttributeContent::Expression {
value: "...b".into(),
stops: vec![]
}],
children: vec![],
position: None,
}),
None,
None
)?
.module,
None
),
"<a {...b}/>;\n",
"should support an `MdxElement` (element, expression attribute)",
);
assert_eq!(
hast_util_to_swc(
&hast::Node::MdxJsxElement(hast::MdxJsxElement {
name: Some("a".into()),
attributes: vec![hast::AttributeContent::Expression { value: "...b,c".into(), stops: vec![] } ],
children: vec![],
position: None,
}),
None,
None
),
Err("0:0: Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)".into()),
"should support an `MdxElement` (element, broken expression attribute)",
);
Ok(())
}
#[test]
fn mdx_expression() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXFragment(JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children: vec![JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(Box::new(Expr::Ident(Ident {
sym: "a".into(),
span: swc_core::common::DUMMY_SP,
optional: false,
}))),
span: swc_core::common::DUMMY_SP,
},)],
span: swc_core::common::DUMMY_SP,
})),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support an `MdxExpression`",
);
assert_eq!(
serialize(
&mut mdx_expression_ast.module,
Some(&mdx_expression_ast.comments)
),
"<>{a}</>;\n",
"should support an `MdxExpression` (serialize)",
);
Ok(())
}
#[test]
fn mdx_esm() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier {
local: Ident {
sym: "a".into(),
optional: false,
span: swc_core::common::DUMMY_SP,
},
span: swc_core::common::DUMMY_SP,
})],
src: Box::new(Str {
value: "b".into(),
span: swc_core::common::DUMMY_SP,
raw: Some("\'b\'".into()),
}),
type_only: false,
asserts: None,
span: swc_core::common::DUMMY_SP,
}))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support an `MdxjsEsm`",
);
assert_eq!(
serialize(&mut mdxjs_esm_ast.module, Some(&mdxjs_esm_ast.comments)),
"import a from 'b';\n",
"should support an `MdxjsEsm` (serialize)",
);
assert_eq!(
hast_util_to_swc(
&hast::Node::MdxjsEsm(hast::MdxjsEsm {
value: "import 1/1".into(),
position: None,
stops: vec![],
}),
None,
None
),
Err(
"0:0: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)'"
.into()
),
"should support an `MdxjsEsm` (w/ broken content)",
);
Ok(())
}
#[test]
fn root() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXFragment(JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children: vec![JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Str(Str {
value: "a".into(),
span: swc_core::common::DUMMY_SP,
raw: None,
}),))),
span: swc_core::common::DUMMY_SP,
},)],
span: swc_core::common::DUMMY_SP,
})),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support a `Root`",
);
assert_eq!(
serialize(&mut root_ast.module, Some(&root_ast.comments)),
"<>{\"a\"}</>;\n",
"should support a `Root` (serialize)",
);
Ok(())
}
#[test]
fn text() -> Result<(), String> {
let mut 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: Module {
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
expr: Box::new(Expr::JSXFragment(JSXFragment {
opening: JSXOpeningFragment {
span: swc_core::common::DUMMY_SP,
},
closing: JSXClosingFragment {
span: swc_core::common::DUMMY_SP,
},
children: vec![JSXElementChild::JSXExprContainer(JSXExprContainer {
expr: JSXExpr::Expr(Box::new(Expr::Lit(Lit::Str(Str {
value: "a".into(),
span: swc_core::common::DUMMY_SP,
raw: None,
}),))),
span: swc_core::common::DUMMY_SP,
},)],
span: swc_core::common::DUMMY_SP,
})),
span: swc_core::common::DUMMY_SP,
},))],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support a `Text`",
);
assert_eq!(
serialize(&mut text_ast.module, Some(&text_ast.comments)),
"<>{\"a\"}</>;\n",
"should support a `Text` (serialize)",
);
Ok(())
}
#[test]
fn text_empty() -> Result<(), String> {
let text_ast = hast_util_to_swc(
&hast::Node::Text(hast::Text {
value: String::new(),
position: None,
}),
None,
None,
)?;
assert_eq!(
text_ast,
Program {
path: None,
module: Module {
shebang: None,
body: vec![],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support an empty `Text`",
);
Ok(())
}
#[test]
fn doctype() -> Result<(), String> {
let mut doctype_ast = hast_util_to_swc(
&hast::Node::Doctype(hast::Doctype { position: None }),
None,
None,
)?;
assert_eq!(
doctype_ast,
Program {
path: None,
module: Module {
shebang: None,
body: vec![],
span: swc_core::common::DUMMY_SP,
},
comments: vec![],
},
"should support a `Doctype`",
);
assert_eq!(
serialize(&mut doctype_ast.module, Some(&doctype_ast.comments)),
"",
"should support a `Doctype` (serialize)",
);
Ok(())
}
}