use crate::hast_util_to_swc::Program;
use crate::mdx_plugin_recma_document::JsxRuntime;
use crate::swc_utils::{
bytepos_to_point, create_bool_expression, create_call_expression, create_ident,
create_ident_expression, create_member_expression_from_str, create_null_expression,
create_num_expression, create_object_expression, create_prop_name, create_str,
create_str_expression, jsx_attribute_name_to_prop_name, jsx_element_name_to_expression,
span_to_position,
};
use core::str;
use markdown::{message::Message, Location};
use swc_core::common::SyntaxContext;
use swc_core::common::{
comments::{Comment, CommentKind},
util::take::Take,
};
use swc_core::ecma::ast::{
ArrayLit, CallExpr, Callee, Expr, ExprOrSpread, ImportDecl, ImportNamedSpecifier, ImportPhase,
ImportSpecifier, JSXAttrName, JSXAttrOrSpread, JSXAttrValue, JSXElement, JSXElementChild,
JSXExpr, JSXFragment, KeyValueProp, Lit, ModuleDecl, ModuleExportName, ModuleItem, Prop,
PropName, PropOrSpread, ThisExpr,
};
use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith};
#[derive(Debug, Default, Clone)]
pub struct Options {
pub development: bool,
}
pub fn swc_util_build_jsx(
program: &mut Program,
options: &Options,
location: Option<&Location>,
) -> Result<(), markdown::message::Message> {
let directives = find_directives(&program.comments, location)?;
let mut state = State {
development: options.development,
filepath: program.path.clone(),
location,
automatic: !matches!(directives.runtime, Some(JsxRuntime::Classic)),
import_fragment: false,
import_jsx: false,
import_jsxs: false,
import_jsx_dev: false,
create_element_expression: create_member_expression_from_str(
&directives
.pragma
.unwrap_or_else(|| "React.createElement".into()),
),
fragment_expression: create_member_expression_from_str(
&directives
.pragma_frag
.unwrap_or_else(|| "React.Fragment".into()),
),
error: None,
};
program.module.visit_mut_with(&mut state);
if let Some(err) = state.error.take() {
return Err(err);
}
let mut specifiers = vec![];
if state.import_fragment {
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
local: create_ident("_Fragment").into(),
imported: Some(ModuleExportName::Ident(create_ident("Fragment").into())),
span: swc_core::common::DUMMY_SP,
is_type_only: false,
}));
}
if state.import_jsx {
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
local: create_ident("_jsx").into(),
imported: Some(ModuleExportName::Ident(create_ident("jsx").into())),
span: swc_core::common::DUMMY_SP,
is_type_only: false,
}));
}
if state.import_jsxs {
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
local: create_ident("_jsxs").into(),
imported: Some(ModuleExportName::Ident(create_ident("jsxs").into())),
span: swc_core::common::DUMMY_SP,
is_type_only: false,
}));
}
if state.import_jsx_dev {
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
local: create_ident("_jsxDEV").into(),
imported: Some(ModuleExportName::Ident(create_ident("jsxDEV").into())),
span: swc_core::common::DUMMY_SP,
is_type_only: false,
}));
}
if !specifiers.is_empty() {
program.module.body.insert(
0,
ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl {
specifiers,
src: Box::new(create_str(&format!(
"{}{}",
directives.import_source.unwrap_or_else(|| "react".into()),
if options.development {
"/jsx-dev-runtime"
} else {
"/jsx-runtime"
}
))),
type_only: false,
with: None,
phase: ImportPhase::default(),
span: swc_core::common::DUMMY_SP,
})),
);
}
Ok(())
}
#[derive(Debug, Default, Clone)]
struct Directives {
runtime: Option<JsxRuntime>,
import_source: Option<String>,
pragma: Option<String>,
pragma_frag: Option<String>,
}
#[derive(Debug, Clone)]
struct State<'a> {
location: Option<&'a Location>,
error: Option<Message>,
filepath: Option<String>,
development: bool,
import_fragment: bool,
import_jsx: bool,
import_jsxs: bool,
import_jsx_dev: bool,
automatic: bool,
create_element_expression: Expr,
fragment_expression: Expr,
}
impl State<'_> {
fn jsx_attribute_value_to_expression(
&mut self,
value: Option<JSXAttrValue>,
) -> Result<Expr, markdown::message::Message> {
match value {
None => Ok(create_bool_expression(true)),
Some(JSXAttrValue::JSXExprContainer(expression_container)) => {
match expression_container.expr {
JSXExpr::JSXEmptyExpr(_) => {
unreachable!("Cannot use empty JSX expressions in attribute values");
}
JSXExpr::Expr(expression) => Ok(*expression),
}
}
Some(JSXAttrValue::Lit(mut literal)) => {
if let Lit::Str(string_literal) = &mut literal {
string_literal.raw = None;
}
Ok(Expr::Lit(literal))
}
Some(JSXAttrValue::JSXFragment(fragment)) => self.jsx_fragment_to_expression(fragment),
Some(JSXAttrValue::JSXElement(element)) => self.jsx_element_to_expression(*element),
}
}
fn jsx_children_to_expressions(
&mut self,
mut children: Vec<JSXElementChild>,
) -> Result<Vec<Expr>, markdown::message::Message> {
let mut result = vec![];
children.reverse();
while let Some(child) = children.pop() {
match child {
JSXElementChild::JSXSpreadChild(child) => {
let lo = child.span.lo;
return Err(
markdown::message::Message {
reason: "Unexpected spread child, which is not supported in Babel, SWC, or React".into(),
place: bytepos_to_point(lo, self.location).map(|p| Box::new(markdown::message::Place::Point(p))),
source: Box::new("mdxjs-rs".into()),
rule_id: Box::new("spread".into()),
}
);
}
JSXElementChild::JSXExprContainer(container) => {
if let JSXExpr::Expr(expression) = container.expr {
result.push(*expression);
}
}
JSXElementChild::JSXText(text) => {
let value = jsx_text_to_value(text.value.as_ref());
if !value.is_empty() {
result.push(create_str_expression(&value));
}
}
JSXElementChild::JSXElement(element) => {
result.push(self.jsx_element_to_expression(*element)?);
}
JSXElementChild::JSXFragment(fragment) => {
result.push(self.jsx_fragment_to_expression(fragment)?);
}
}
}
Ok(result)
}
fn jsx_attributes_to_expressions(
&mut self,
attributes: Option<Vec<JSXAttrOrSpread>>,
children: Option<Vec<Expr>>,
) -> Result<(Option<Expr>, Option<Expr>), markdown::message::Message> {
let mut objects = vec![];
let mut fields = vec![];
let mut spread = false;
let mut key = None;
if let Some(mut attributes) = attributes {
attributes.reverse();
while let Some(attribute) = attributes.pop() {
match attribute {
JSXAttrOrSpread::SpreadElement(spread_element) => {
if !fields.is_empty() {
objects.push(create_object_expression(fields));
fields = vec![];
}
objects.push(*spread_element.expr);
spread = true;
}
JSXAttrOrSpread::JSXAttr(jsx_attribute) => {
let value = self.jsx_attribute_value_to_expression(jsx_attribute.value)?;
let mut value = Some(value);
if let JSXAttrName::Ident(ident) = &jsx_attribute.name {
if self.automatic && &ident.sym == "key" {
if spread {
let lo = jsx_attribute.span.lo;
return Err(markdown::message::Message {
reason:
"Expected `key` to come before any spread expressions"
.into(),
place: bytepos_to_point(lo, self.location)
.map(|p| Box::new(markdown::message::Place::Point(p))),
source: Box::new("mdxjs-rs".into()),
rule_id: Box::new("key".into()),
});
}
key = value.take();
}
}
if let Some(value) = value {
fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(
KeyValueProp {
key: jsx_attribute_name_to_prop_name(jsx_attribute.name),
value: Box::new(value),
},
))));
}
}
}
}
}
if let Some(mut children) = children {
let value = if children.is_empty() {
None
} else if children.len() == 1 {
Some(children.pop().unwrap())
} else {
let mut elements = vec![];
children.reverse();
while let Some(child) = children.pop() {
elements.push(Some(ExprOrSpread {
spread: None,
expr: Box::new(child),
}));
}
let lit = ArrayLit {
elems: elements,
span: swc_core::common::DUMMY_SP,
};
Some(Expr::Array(lit))
};
if let Some(value) = value {
fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: create_prop_name("children"),
value: Box::new(value),
}))));
}
}
if !fields.is_empty() {
objects.push(create_object_expression(fields));
}
let props = if objects.is_empty() {
None
} else if objects.len() == 1 {
Some(objects.pop().unwrap())
} else {
let mut args = vec![];
objects.reverse();
if !matches!(objects.last(), Some(Expr::Object(_))) {
objects.push(create_object_expression(vec![]));
}
while let Some(object) = objects.pop() {
args.push(ExprOrSpread {
spread: None,
expr: Box::new(object),
});
}
let callee = Callee::Expr(Box::new(create_member_expression_from_str("Object.assign")));
Some(create_call_expression(callee, args))
};
Ok((props, key))
}
fn jsx_expressions_to_call(
&mut self,
span: swc_core::common::Span,
name: Expr,
attributes: Option<Vec<JSXAttrOrSpread>>,
mut children: Vec<Expr>,
) -> Result<Expr, markdown::message::Message> {
let (callee, parameters) = if self.automatic {
let is_static_children = children.len() > 1;
let (props, key) = self.jsx_attributes_to_expressions(attributes, Some(children))?;
let mut parameters = vec![
ExprOrSpread {
spread: None,
expr: Box::new(name),
},
ExprOrSpread {
spread: None,
expr: Box::new(props.unwrap_or_else(|| create_object_expression(vec![]))),
},
];
if let Some(key) = key {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(key),
});
} else if self.development {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(create_ident_expression("undefined")),
});
}
if self.development {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(create_bool_expression(is_static_children)),
});
let filename = if let Some(value) = &self.filepath {
create_str_expression(value)
} else {
create_str_expression("<source.js>")
};
let prop = PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: PropName::Ident(create_ident("fileName")),
value: Box::new(filename),
})));
let mut meta_fields = vec![prop];
if let Some(position) = span_to_position(span, self.location) {
meta_fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: create_prop_name("lineNumber"),
value: Box::new(create_num_expression(position.start.line as f64)),
}))));
meta_fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key: create_prop_name("columnNumber"),
value: Box::new(create_num_expression(position.start.column as f64)),
}))));
}
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(create_object_expression(meta_fields)),
});
let this_expression = ThisExpr {
span: swc_core::common::DUMMY_SP,
};
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(Expr::This(this_expression)),
});
}
let callee = if self.development {
self.import_jsx_dev = true;
"_jsxDEV"
} else if is_static_children {
self.import_jsxs = true;
"_jsxs"
} else {
self.import_jsx = true;
"_jsx"
};
(create_ident_expression(callee), parameters)
} else {
let (props, key) = self.jsx_attributes_to_expressions(attributes, None)?;
debug_assert!(key.is_none(), "key should not be extracted");
let mut parameters = vec![
ExprOrSpread {
spread: None,
expr: Box::new(name),
},
];
if let Some(props) = props {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(props),
});
} else if !children.is_empty() {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(create_null_expression()),
});
}
children.reverse();
while let Some(child) = children.pop() {
parameters.push(ExprOrSpread {
spread: None,
expr: Box::new(child),
});
}
(self.create_element_expression.clone(), parameters)
};
let call_expression = CallExpr {
callee: Callee::Expr(Box::new(callee)),
args: parameters,
type_args: None,
span,
ctxt: SyntaxContext::empty(),
};
Ok(Expr::Call(call_expression))
}
fn jsx_element_to_expression(
&mut self,
element: JSXElement,
) -> Result<Expr, markdown::message::Message> {
let children = self.jsx_children_to_expressions(element.children)?;
let mut name = jsx_element_name_to_expression(element.opening.name);
if let Expr::Ident(ident) = &name {
let head = ident.as_ref().as_bytes();
if matches!(head.first(), Some(b'a'..=b'z')) {
name = create_str_expression(&ident.sym);
}
}
self.jsx_expressions_to_call(element.span, name, Some(element.opening.attrs), children)
}
fn jsx_fragment_to_expression(
&mut self,
fragment: JSXFragment,
) -> Result<Expr, markdown::message::Message> {
let name = if self.automatic {
self.import_fragment = true;
create_ident_expression("_Fragment")
} else {
self.fragment_expression.clone()
};
let children = self.jsx_children_to_expressions(fragment.children)?;
self.jsx_expressions_to_call(fragment.span, name, None, children)
}
}
impl VisitMut for State<'_> {
noop_visit_mut_type!();
fn visit_mut_expr(&mut self, expr: &mut Expr) {
let result = match expr {
Expr::JSXElement(element) => Some(self.jsx_element_to_expression(*element.take())),
Expr::JSXFragment(fragment) => Some(self.jsx_fragment_to_expression(fragment.take())),
_ => None,
};
if let Some(result) = result {
match result {
Ok(expression) => {
*expr = expression;
expr.visit_mut_children_with(self);
}
Err(err) => {
self.error = Some(err);
}
}
} else {
expr.visit_mut_children_with(self);
}
}
}
fn find_directives(
comments: &Vec<Comment>,
location: Option<&Location>,
) -> Result<Directives, markdown::message::Message> {
let mut directives = Directives::default();
for comment in comments {
if comment.kind != CommentKind::Block {
continue;
}
let lines = comment.text.lines();
for line in lines {
let bytes = line.as_bytes();
let mut index = 0;
while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
if index < bytes.len() && bytes[index] == b'*' {
index += 1;
while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
}
if !(index + 4 < bytes.len()
&& bytes[index] == b'@'
&& bytes[index + 1] == b'j'
&& bytes[index + 2] == b's'
&& bytes[index + 3] == b'x')
{
continue;
}
loop {
let mut key_range = (index, index);
while index < bytes.len() && !matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
key_range.1 = index;
while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
let mut value_range = (index, index);
while index < bytes.len() && !matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
value_range.1 = index;
let key = String::from_utf8_lossy(&bytes[key_range.0..key_range.1]);
let value = String::from_utf8_lossy(&bytes[value_range.0..value_range.1]);
match key.as_ref() {
"@jsxRuntime" => match value.as_ref() {
"automatic" => directives.runtime = Some(JsxRuntime::Automatic),
"classic" => directives.runtime = Some(JsxRuntime::Classic),
"" => {}
value => {
return Err(markdown::message::Message {
reason: format!(
"Runtime must be either `automatic` or `classic`, not {}",
value
),
place: bytepos_to_point(comment.span.lo, location)
.map(|p| Box::new(markdown::message::Place::Point(p))),
source: Box::new("mdxjs-rs".into()),
rule_id: Box::new("runtime".into()),
});
}
},
"@jsxImportSource" => {
match value.as_ref() {
"" => {}
value => {
directives.runtime = Some(JsxRuntime::Automatic);
directives.import_source = Some(value.into());
}
}
}
"@jsxFrag" => match value.as_ref() {
"" => {}
value => directives.pragma_frag = Some(value.into()),
},
"@jsx" => match value.as_ref() {
"" => {}
value => directives.pragma = Some(value.into()),
},
"" => {
break;
}
_ => {}
}
while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') {
index += 1;
}
}
}
}
Ok(directives)
}
fn jsx_text_to_value(value: &str) -> String {
let mut result = String::with_capacity(value.len());
let value = value.replace('\t', " ");
let bytes = value.as_bytes();
let mut index = 0;
let mut start = 0;
while index < bytes.len() {
if !matches!(bytes[index], b'\r' | b'\n') {
index += 1;
continue;
}
let mut before = index;
while before > start && bytes[before - 1] == b' ' {
before -= 1;
}
if start != before {
if !result.is_empty() {
result.push(' ');
}
result.push_str(str::from_utf8(&bytes[start..before]).unwrap());
}
index += 1;
while index < bytes.len() && bytes[index] == b' ' {
index += 1;
}
start = index;
}
if start != bytes.len() {
if result.is_empty() {
index = 0;
while index < bytes.len() && bytes[index] == b' ' {
index += 1;
}
if index == bytes.len() {
return result;
}
} else {
result.push(' ');
}
result.push_str(str::from_utf8(&bytes[start..]).unwrap());
}
result
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hast_util_to_swc::Program;
use crate::swc::{flat_comments, serialize};
use pretty_assertions::assert_eq;
use swc_core::common::Spanned;
use swc_core::common::{
comments::SingleThreadedComments, source_map::SmallPos, BytePos, FileName, SourceFile,
};
use swc_core::ecma::ast::{
EsVersion, ExprStmt, JSXClosingElement, JSXElementName, JSXOpeningElement, JSXSpreadChild,
Module, Stmt,
};
use swc_core::ecma::parser::{parse_file_as_module, EsSyntax, Syntax};
fn compile(value: &str, options: &Options) -> Result<String, markdown::message::Message> {
let location = Location::new(value.as_bytes());
let mut errors = vec![];
let comments = SingleThreadedComments::default();
let result = parse_file_as_module(
&SourceFile::new(
FileName::Anon.into(),
false,
FileName::Anon.into(),
value.to_string().into(),
BytePos::from_usize(1),
),
Syntax::Es(EsSyntax {
jsx: true,
..EsSyntax::default()
}),
EsVersion::Es2022,
Some(&comments),
&mut errors,
);
match result {
Err(error) => Err(markdown::message::Message {
reason: error.kind().msg().into(),
place: bytepos_to_point(error.span().lo, Some(&location))
.map(|p| Box::new(markdown::message::Place::Point(p))),
source: Box::new("mdxjs-rs".into()),
rule_id: Box::new("swc".into()),
}),
Ok(module) => {
let mut program = Program {
path: Some("example.jsx".into()),
module,
comments: flat_comments(comments),
};
swc_util_build_jsx(&mut program, options, Some(&location))?;
Ok(serialize(&mut program.module, Some(&program.comments)))
}
}
}
#[test]
fn small_default() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("let a = <b />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n",
"should compile JSX away"
);
Ok(())
}
#[test]
fn directive_runtime_automatic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime automatic */\nlet a = <b />",
&Options::default()
)?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n",
"should support a `@jsxRuntime automatic` directive"
);
Ok(())
}
#[test]
fn directive_runtime_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic */\nlet a = <b />",
&Options::default()
)?,
"let a = React.createElement(\"b\");\n",
"should support a `@jsxRuntime classic` directive"
);
Ok(())
}
#[test]
fn directive_runtime_empty() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("/* @jsxRuntime */\nlet a = <b />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n",
"should support an empty `@jsxRuntime` directive"
);
Ok(())
}
#[test]
fn directive_runtime_invalid() {
assert_eq!(
compile(
"/* @jsxRuntime unknown */\nlet a = <b />",
&Options::default()
)
.err()
.unwrap()
.to_string(),
"1:1: Runtime must be either `automatic` or `classic`, not unknown (mdxjs-rs:runtime)",
"should crash on a non-automatic, non-classic `@jsxRuntime` directive"
);
}
#[test]
fn directive_import_source() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxImportSource aaa */\nlet a = <b />",
&Options::default()
)?,
"import { jsx as _jsx } from \"aaa/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n",
"should support a `@jsxImportSource` directive"
);
Ok(())
}
#[test]
fn directive_jsx() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic @jsx a */\nlet b = <c />",
&Options::default()
)?,
"let b = a(\"c\");\n",
"should support a `@jsx` directive"
);
Ok(())
}
#[test]
fn directive_jsx_empty() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic @jsx */\nlet a = <b />",
&Options::default()
)?,
"let a = React.createElement(\"b\");\n",
"should support an empty `@jsx` directive"
);
Ok(())
}
#[test]
fn directive_jsx_non_identifier() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic @jsx a.b-c.d! */\n<x />",
&Options::default()
)?,
"a[\"b-c\"][\"d!\"](\"x\");\n",
"should support an `@jsx` directive set to an invalid identifier"
);
Ok(())
}
#[test]
fn directive_jsx_frag() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic @jsxFrag a */\nlet b = <></>",
&Options::default()
)?,
"let b = React.createElement(a);\n",
"should support a `@jsxFrag` directive"
);
Ok(())
}
#[test]
fn directive_jsx_frag_empty() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic @jsxFrag */\nlet a = <></>",
&Options::default()
)?,
"let a = React.createElement(React.Fragment);\n",
"should support an empty `@jsxFrag` directive"
);
Ok(())
}
#[test]
fn directive_non_first_line() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/*\n first line\n @jsxRuntime classic\n */\n<b />",
&Options::default()
)?,
"React.createElement(\"b\");\n",
"should support a directive on a non-first line"
);
Ok(())
}
#[test]
fn directive_asterisked_line() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/*\n * first line\n * @jsxRuntime classic\n */\n<b />",
&Options::default()
)?,
"React.createElement(\"b\");\n",
"should support a directive on an asterisk’ed line"
);
Ok(())
}
#[test]
fn jsx_element_self_closing() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {});\n",
"should support a self-closing element"
);
Ok(())
}
#[test]
fn jsx_element_self_closing_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("/* @jsxRuntime classic */\n<a />", &Options::default())?,
"React.createElement(\"a\");\n",
"should support a self-closing element (classic)"
);
Ok(())
}
#[test]
fn jsx_element_closed() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"<a>b</a>",
&Options::default()
)?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n children: \"b\"\n});\n",
"should support a closed element"
);
Ok(())
}
#[test]
fn jsx_element_member_name() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a.b.c />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a.b.c, {});\n",
"should support an element with a member name"
);
Ok(())
}
#[test]
fn jsx_element_member_name_dashes() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a.b-c />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a[\"b-c\"], {});\n",
"should support an element with a member name and dashes"
);
Ok(())
}
#[test]
fn jsx_element_member_name_many() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a.b.c.d />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a.b.c.d, {});\n",
"should support an element with a member name of lots of names"
);
Ok(())
}
#[test]
fn jsx_element_namespace_name() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a:b />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a:b\", {});\n",
"should support an element with a namespace name"
);
Ok(())
}
#[test]
fn jsx_element_name_dashes() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a-b />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a-b\", {});\n",
"should support an element with a dash in the name"
);
Ok(())
}
#[test]
fn jsx_element_name_capital() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<Abc />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(Abc, {});\n",
"should support an element with a non-lowercase first character in the name"
);
Ok(())
}
#[test]
fn jsx_element_attribute_boolean() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: true\n});\n",
"should support an element with a boolean attribute"
);
Ok(())
}
#[test]
fn jsx_element_attribute_boolean_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("/* @jsxRuntime classic */\n<a b />", &Options::default())?,
"React.createElement(\"a\", {\n b: true\n});\n",
"should support an element with a boolean attribute (classic"
);
Ok(())
}
#[test]
fn jsx_element_attribute_name_namespace() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b:c />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n \"b:c\": true\n});\n",
"should support an element with colons in an attribute name"
);
Ok(())
}
#[test]
fn jsx_element_attribute_name_non_identifier() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b-c />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n \"b-c\": true\n});\n",
"should support an element with non-identifier characters in an attribute name"
);
Ok(())
}
#[test]
fn jsx_element_attribute_value() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b='c' />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: \"c\"\n});\n",
"should support an element with an attribute with a value"
);
Ok(())
}
#[test]
fn jsx_element_attribute_value_expression() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b={c} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: c\n});\n",
"should support an element with an attribute with a value expression"
);
Ok(())
}
#[test]
fn jsx_element_attribute_value_fragment() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b=<>c</> />", &Options::default())?,
"import { Fragment as _Fragment, jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: _jsx(_Fragment, {\n children: \"c\"\n })\n});\n",
"should support an element with an attribute with a fragment as value"
);
Ok(())
}
#[test]
fn jsx_element_attribute_value_element() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b=<c /> />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: _jsx(\"c\", {})\n});\n",
"should support an element with an attribute with an element as value"
);
Ok(())
}
#[test]
fn jsx_element_spread_attribute() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a {...b} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", b);\n",
"should support an element with a spread attribute"
);
Ok(())
}
#[test]
fn jsx_element_spread_attribute_then_prop() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a {...b} c />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({}, b, {\n c: true\n}));\n",
"should support an element with a spread attribute and then a prop"
);
Ok(())
}
#[test]
fn jsx_element_prop_then_spread_attribute() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b {...c} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({\n b: true\n}, c));\n",
"should support an element with a prop and then a spread attribute"
);
Ok(())
}
#[test]
fn jsx_element_two_spread_attributes() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a {...b} {...c} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({}, b, c));\n",
"should support an element two spread attributes"
);
Ok(())
}
#[test]
fn jsx_element_complex_spread_attribute() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a {...{b:1,...c,d:2}} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
b: 1,
...c,
d: 2
});
",
"should support more complex spreads"
);
Ok(())
}
#[test]
fn jsx_element_child_expression() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a>{1}</a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: 1
});
",
"should support a child expression"
);
Ok(())
}
#[test]
fn jsx_element_child_expression_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("/* @jsxRuntime classic */\n<a>{1}</a>", &Options::default())?,
"React.createElement(\"a\", null, 1);\n",
"should support a child expression (classic)"
);
Ok(())
}
#[test]
fn jsx_element_child_expression_empty() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a>{}</a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {});
",
"should support an empty child expression"
);
Ok(())
}
#[test]
fn jsx_element_child_expression_empty_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("/* @jsxRuntime classic */\n<a>{}</a>", &Options::default())?,
"React.createElement(\"a\");\n",
"should support an empty child expression (classic)"
);
Ok(())
}
#[test]
fn jsx_element_child_fragment() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a><>b</></a>", &Options::default())?,
"import { Fragment as _Fragment, jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: _jsx(_Fragment, {
children: \"b\"
})
});
",
"should support a fragment as a child"
);
Ok(())
}
#[test]
fn jsx_element_child_spread() {
let mut program = Program {
path: None,
comments: vec![],
module: Module {
span: swc_core::common::DUMMY_SP,
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: swc_core::common::DUMMY_SP,
expr: Box::new(Expr::JSXElement(Box::new(JSXElement {
span: swc_core::common::DUMMY_SP,
opening: JSXOpeningElement {
name: JSXElementName::Ident(create_ident("a").into()),
attrs: vec![],
self_closing: false,
type_args: None,
span: swc_core::common::DUMMY_SP,
},
closing: Some(JSXClosingElement {
name: JSXElementName::Ident(create_ident("a").into()),
span: swc_core::common::DUMMY_SP,
}),
children: vec![JSXElementChild::JSXSpreadChild(JSXSpreadChild {
expr: Box::new(create_ident_expression("a")),
span: swc_core::common::DUMMY_SP,
})],
}))),
}))],
},
};
assert_eq!(
swc_util_build_jsx(&mut program, &Options::default(), None)
.err()
.unwrap()
.to_string(),
"Unexpected spread child, which is not supported in Babel, SWC, or React (mdxjs-rs:spread)",
"should not support a spread child"
);
}
#[test]
fn jsx_element_child_text_padded_start() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a> b</a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: \" b\"
});
",
"should support initial spaces in content"
);
Ok(())
}
#[test]
fn jsx_element_child_text_padded_end() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a>b </a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: \"b \"
});
",
"should support final spaces in content"
);
Ok(())
}
#[test]
fn jsx_element_child_text_padded() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a> b </a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: \" b \"
});
",
"should support initial and final spaces in content"
);
Ok(())
}
#[test]
fn jsx_element_child_text_line_endings_padded() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a> b \r c \n d \n </a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: \" b c d\"
});
",
"should support spaces around line endings in content"
);
Ok(())
}
#[test]
fn jsx_element_child_text_blank_lines() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a> b \r \n c \n\n d \n </a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
children: \" b c d\"
});
",
"should support blank lines in content"
);
Ok(())
}
#[test]
fn jsx_element_child_whitespace_only() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a> \t\n </a>", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {});
",
"should support whitespace-only in content"
);
Ok(())
}
#[test]
fn jsx_element_key_automatic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a b key='c' d />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", {
b: true,
d: true
}, \"c\");
",
"should support a key in the automatic runtime"
);
Ok(())
}
#[test]
fn jsx_element_key_classic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile(
"/* @jsxRuntime classic */\n<a b key='c' d />",
&Options::default()
)?,
"React.createElement(\"a\", {
b: true,
key: \"c\",
d: true
});
",
"should support a key in the classic runtime"
);
Ok(())
}
#[test]
fn jsx_element_key_after_spread_automatic() {
assert_eq!(
compile("<a {...b} key='c' />", &Options::default())
.err()
.unwrap()
.to_string(),
"1:11: Expected `key` to come before any spread expressions (mdxjs-rs:key)",
"should crash on a key after a spread in the automatic runtime"
);
}
#[test]
fn jsx_element_key_before_spread_automatic() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<a key='b' {...c} />", &Options::default())?,
"import { jsx as _jsx } from \"react/jsx-runtime\";
_jsx(\"a\", c, \"b\");
",
"should support a key before a spread in the automatic runtime"
);
Ok(())
}
#[test]
fn jsx_element_development() -> Result<(), markdown::message::Message> {
assert_eq!(
compile("<><a /></>", &Options { development: true })?,
"import { Fragment as _Fragment, jsxDEV as _jsxDEV } from \"react/jsx-dev-runtime\";
_jsxDEV(_Fragment, {
children: _jsxDEV(\"a\", {}, undefined, false, {
fileName: \"example.jsx\",
lineNumber: 1,
columnNumber: 3
}, this)
}, undefined, false, {
fileName: \"example.jsx\",
lineNumber: 1,
columnNumber: 1
}, this);
",
"should support the automatic development runtime if `development` is on"
);
Ok(())
}
#[test]
fn jsx_element_development_no_filepath() -> Result<(), markdown::message::Message> {
let mut program = Program {
path: None,
comments: vec![],
module: Module {
span: swc_core::common::DUMMY_SP,
shebang: None,
body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt {
span: swc_core::common::DUMMY_SP,
expr: Box::new(Expr::JSXElement(Box::new(JSXElement {
span: swc_core::common::DUMMY_SP,
opening: JSXOpeningElement {
name: JSXElementName::Ident(create_ident("a").into()),
attrs: vec![],
self_closing: true,
type_args: None,
span: swc_core::common::DUMMY_SP,
},
closing: None,
children: vec![],
}))),
}))],
},
};
swc_util_build_jsx(&mut program, &Options { development: true }, None)?;
assert_eq!(
serialize(&mut program.module, Some(&program.comments)),
"import { jsxDEV as _jsxDEV } from \"react/jsx-dev-runtime\";
_jsxDEV(\"a\", {}, undefined, false, {
fileName: \"<source.js>\"
}, this);
",
"should support the automatic development runtime without a file path"
);
Ok(())
}
#[test]
fn jsx_text() {
assert_eq!(jsx_text_to_value("a"), "a", "should support jsx text");
assert_eq!(
jsx_text_to_value(" a\t"),
" a ",
"should support jsx text w/ initial, final whitespace"
);
assert_eq!(
jsx_text_to_value(" \t"),
"",
"should support jsx text that’s just whitespace"
);
assert_eq!(
jsx_text_to_value("a\r\r\n\nb"),
"a b",
"should support jsx text with line endings"
);
assert_eq!(
jsx_text_to_value(" a \n b \n c "),
" a b c ",
"should support jsx text with line endings with white space"
);
assert_eq!(
jsx_text_to_value(" \n a \n "),
"a",
"should support jsx text with blank initial and final lines"
);
assert_eq!(
jsx_text_to_value(" a \n \n \t \n b "),
" a b ",
"should support jsx text with blank lines in between"
);
assert_eq!(
jsx_text_to_value(" \n \n \t \n "),
"",
"should support jsx text with only spaces, tabs, and line endings"
);
}
}