aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_utils
diff options
context:
space:
mode:
authorLibravatar Titus Wormer <tituswormer@gmail.com>2022-09-30 18:17:54 +0200
committerLibravatar Titus Wormer <tituswormer@gmail.com>2022-09-30 18:18:58 +0200
commit117cfc10c6d4a0a9346a29353860d1185d1ea224 (patch)
treea5ae0b1f36b7955016b83477b3f4dd455669d2af /tests/test_utils
parent9dd7a2ba67b7c35e9cd5c9105fdf36aec764fa6e (diff)
downloadmarkdown-rs-117cfc10c6d4a0a9346a29353860d1185d1ea224.tar.gz
markdown-rs-117cfc10c6d4a0a9346a29353860d1185d1ea224.tar.bz2
markdown-rs-117cfc10c6d4a0a9346a29353860d1185d1ea224.zip
Add support for turning hast into swc
Diffstat (limited to '')
-rw-r--r--tests/test_utils/hast.rs25
-rw-r--r--tests/test_utils/mod.rs1
-rw-r--r--tests/test_utils/swc.rs101
-rw-r--r--tests/test_utils/to_hast.rs46
-rw-r--r--tests/test_utils/to_swc.rs714
5 files changed, 836 insertions, 51 deletions
diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs
index 4adf0ca..1ad8789 100644
--- a/tests/test_utils/hast.rs
+++ b/tests/test_utils/hast.rs
@@ -9,10 +9,11 @@ use alloc::{
string::{String, ToString},
vec::Vec,
};
-use micromark::{mdast::AttributeContent, unist::Position};
+pub use micromark::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute};
+use micromark::unist::Position;
/// Nodes.
-#[derive(Clone, PartialEq)]
+#[derive(Clone, PartialEq, Eq)]
pub enum Node {
/// Root.
Root(Root),
@@ -142,7 +143,7 @@ impl Node {
/// > | a
/// ^
/// ```
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Root {
// Parent.
/// Content model.
@@ -157,8 +158,7 @@ pub struct Root {
/// > | <!doctype html>
/// ^^^^^^^^^^^^^^^
/// ```
-// To do: clone.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Element {
pub tag_name: String,
pub properties: Vec<(String, PropertyValue)>,
@@ -168,19 +168,12 @@ pub struct Element {
pub position: Option<Position>,
}
-#[derive(Clone, Debug, PartialEq)]
-pub enum PropertyItem {
- Number(f32),
- String(String),
-}
-
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum PropertyValue {
- Number(f32),
Boolean(bool),
String(String),
- CommaSeparated(Vec<PropertyItem>),
- SpaceSeparated(Vec<PropertyItem>),
+ CommaSeparated(Vec<String>),
+ SpaceSeparated(Vec<String>),
}
/// Document type.
@@ -232,7 +225,7 @@ pub struct Text {
/// > | <a />
/// ^^^^^
/// ```
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MdxJsxElement {
// Parent.
/// Content model.
diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs
index 111118f..87c1e1e 100644
--- a/tests/test_utils/mod.rs
+++ b/tests/test_utils/mod.rs
@@ -1,3 +1,4 @@
pub mod hast;
pub mod swc;
pub mod to_hast;
+pub mod to_swc;
diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs
index f08fe38..c0fffa0 100644
--- a/tests/test_utils/swc.rs
+++ b/tests/test_utils/swc.rs
@@ -13,7 +13,7 @@ use swc_ecma_parser::{
// Use lexer in the future:
// <https://docs.rs/swc_ecma_parser/0.99.1/swc_ecma_parser/lexer/index.html>
-/// Parse ESM in MDX with SWC.
+/// Lex ESM in MDX with SWC.
#[allow(dead_code)]
pub fn parse_esm(value: &str) -> MdxSignal {
let (file, syntax, version) = create_config(value.to_string());
@@ -35,7 +35,31 @@ pub fn parse_esm(value: &str) -> MdxSignal {
}
}
-/// Parse expressions in MDX with SWC.
+/// Parse ESM in MDX with SWC.
+/// To do: figure out how to fix positional info.
+#[allow(dead_code)]
+pub fn parse_esm_to_tree(value: &str) -> Result<swc_ecma_ast::Module, String> {
+ let (file, syntax, version) = create_config(value.to_string());
+ let mut errors = vec![];
+
+ let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
+
+ match result {
+ Err(error) => Err(swc_error_to_string(&error)),
+ Ok(module) => {
+ if errors.is_empty() {
+ Ok(module)
+ } else {
+ if errors.len() > 1 {
+ println!("parse_esm_to_tree: todo: multiple errors? {:?}", errors);
+ }
+ Err(swc_error_to_string(&errors[0]))
+ }
+ }
+ }
+}
+
+/// Lex expressions in MDX with SWC.
#[allow(dead_code)]
pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal {
// Empty expressions are OK.
@@ -79,6 +103,67 @@ pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal {
}
}
+/// Parse ESM in MDX with SWC.
+/// To do: figure out how to fix positional info.
+#[allow(dead_code)]
+pub fn parse_expression_to_tree(
+ value: &str,
+ kind: &MdxExpressionKind,
+) -> Result<Box<swc_ecma_ast::Expr>, String> {
+ // For attribute expression, a spread is needed, for which we have to prefix
+ // and suffix the input.
+ // See `check_expression_ast` for how the AST is verified.
+ let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) {
+ ("({", "})")
+ } else {
+ ("", "")
+ };
+
+ let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix));
+ let mut errors = vec![];
+ let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
+
+ match result {
+ Err(error) => Err(swc_error_to_string(&error)),
+ Ok(expr) => {
+ if errors.is_empty() {
+ if matches!(kind, MdxExpressionKind::AttributeExpression) {
+ let mut obj = None;
+
+ if let swc_ecma_ast::Expr::Paren(d) = *expr {
+ if let swc_ecma_ast::Expr::Object(d) = *d.expr {
+ obj = Some(d)
+ }
+ };
+
+ if let Some(mut obj) = obj {
+ if obj.props.len() > 1 {
+ Err("Unexpected extra content in spread: only a single spread is supported".into())
+ } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop()
+ {
+ Ok(d.expr)
+ } else {
+ Err("Unexpected prop in spread: only a spread is supported".into())
+ }
+ } else {
+ Err("Expected an object spread (`{...spread}`)".into())
+ }
+ } else {
+ Ok(expr)
+ }
+ } else {
+ if errors.len() > 1 {
+ println!(
+ "parse_expression_to_tree: todo: multiple errors? {:?}",
+ errors
+ );
+ }
+ Err(swc_error_to_string(&errors[0]))
+ }
+ }
+ }
+}
+
/// Check that the resulting AST of ESM is OK.
///
/// This checks that only module declarations (import/exports) are used, not
@@ -138,9 +223,12 @@ fn swc_error_to_signal(
prefix_len: usize,
name: &str,
) -> MdxSignal {
- let message = error.kind().msg().to_string();
let place = fix_swc_position(error.span().hi.to_usize(), prefix_len);
- let message = format!("Could not parse {} with swc: {}", name, message);
+ let message = format!(
+ "Could not parse {} with swc: {}",
+ name,
+ swc_error_to_string(error)
+ );
if place >= value_len {
MdxSignal::Eof(message)
@@ -149,6 +237,11 @@ fn swc_error_to_signal(
}
}
+/// Turn an SWC error into a string.
+fn swc_error_to_string(error: &SwcError) -> String {
+ error.kind().msg().into()
+}
+
/// Move past JavaScript whitespace (well, actually ASCII whitespace) and
/// comments.
///
diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs
index 821931c..6396831 100644
--- a/tests/test_utils/to_hast.rs
+++ b/tests/test_utils/to_hast.rs
@@ -39,10 +39,6 @@ pub fn to_hast(mdast: &mdast::Node) -> hast::Node {
}
});
- // - footnoteById: Record<string, Node>
- // - footnoteOrder: Vec<string>
- // - footnoteCounts: Record<string, usize>
-
let (result, mut state) = one(
mdast,
None,
@@ -153,9 +149,9 @@ pub fn to_hast(mdast: &mdast::Node) -> hast::Node {
),
(
"className".into(),
- hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
- "data-footnote-backref".into(),
- )]),
+ hast::PropertyValue::SpaceSeparated(vec![
+ "data-footnote-backref".into()
+ ]),
),
],
children: backref_children,
@@ -212,9 +208,7 @@ pub fn to_hast(mdast: &mdast::Node) -> hast::Node {
("dataFootnotes".into(), hast::PropertyValue::Boolean(true)),
(
"className".into(),
- hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
- "footnotes".into(),
- )]),
+ hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]),
),
],
children: vec![
@@ -227,9 +221,7 @@ pub fn to_hast(mdast: &mdast::Node) -> hast::Node {
),
(
"className".into(),
- hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
- "sr-only".into(),
- )]),
+ hast::PropertyValue::SpaceSeparated(vec!["sr-only".into()]),
),
],
children: vec![hast::Node::Text(hast::Text {
@@ -360,7 +352,6 @@ fn transform_code(node: &mdast::Node, state: State) -> (Result, State) {
if let Some(lang) = code.lang.as_ref() {
let mut value = "language-".to_string();
value.push_str(lang);
- let value = hast::PropertyItem::String(value);
properties.push((
"className".into(),
hast::PropertyValue::SpaceSeparated(vec![value]),
@@ -650,8 +641,8 @@ fn transform_inline_math(node: &mdast::Node, state: State) -> (Result, State) {
properties: vec![(
"className".into(),
hast::PropertyValue::SpaceSeparated(vec![
- hast::PropertyItem::String("language-math".into()),
- hast::PropertyItem::String("math-inline".into()),
+ "language-math".into(),
+ "math-inline".into(),
]),
)],
children: vec![hast::Node::Text(hast::Text {
@@ -762,9 +753,7 @@ fn transform_list_item(
// See: <https://github.com/sindresorhus/github-markdown-css>.
properties.push((
"className".into(),
- hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
- "task-list-item".into(),
- )]),
+ hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]),
));
let mut input = Some(hast::Node::Element(hast::Element {
@@ -898,7 +887,10 @@ fn transform_list(node: &mdast::Node, state: State) -> (Result, State) {
// Add start.
if let Some(start) = list.start {
if list.ordered && start != 1 {
- properties.push(("start".into(), hast::PropertyValue::Number(start.into())));
+ properties.push((
+ "start".into(),
+ hast::PropertyValue::String(start.to_string()),
+ ));
}
}
@@ -906,9 +898,7 @@ fn transform_list(node: &mdast::Node, state: State) -> (Result, State) {
if contains_task_list {
properties.push((
"className".into(),
- hast::PropertyValue::SpaceSeparated(vec![hast::PropertyItem::String(
- "contains-task-list".into(),
- )]),
+ hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]),
));
}
@@ -950,8 +940,8 @@ fn transform_math(node: &mdast::Node, state: State) -> (Result, State) {
properties: vec![(
"className".into(),
hast::PropertyValue::SpaceSeparated(vec![
- hast::PropertyItem::String("language-math".into()),
- hast::PropertyItem::String("math-display".into()),
+ "language-math".into(),
+ "math-display".into(),
]),
)],
children: vec![hast::Node::Text(hast::Text {
@@ -1158,12 +1148,6 @@ fn transform_table_row(
fn transform_table(node: &mdast::Node, mut state: State) -> (Result, State) {
if let mdast::Node::Table(table) = node {
let mut rows = vec![];
- // let body = hast::Element {
- // tag_name: "tbody".into(),
- // properties: vec![],
- // children: vec![],
- // position: None,
- // };
let mut index = 0;
while index < table.children.len() {
diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs
new file mode 100644
index 0000000..1d25f47
--- /dev/null
+++ b/tests/test_utils/to_swc.rs
@@ -0,0 +1,714 @@
+extern crate swc_common;
+extern crate swc_ecma_ast;
+use crate::test_utils::hast;
+use crate::test_utils::swc::{parse_esm_to_tree, parse_expression_to_tree};
+use core::str;
+use micromark::{unist::Position, MdxExpressionKind};
+
+/// Result.
+#[derive(Debug, PartialEq, Eq)]
+pub struct Program {
+ /// 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 {
+ /// 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>,
+}
+
+impl Context {
+ /// Create a new context.
+ fn new() -> Context {
+ Context {
+ space: Space::Html,
+ comments: vec![],
+ esm: vec![],
+ }
+ }
+}
+
+#[allow(dead_code)]
+pub fn to_swc(tree: &hast::Node) -> Result<Program, String> {
+ let mut context = Context::new();
+ 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 {
+ 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)) => {
+ Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer(
+ swc_ecma_ast::JSXExprContainer {
+ expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree(
+ value,
+ &MdxExpressionKind::AttributeValueExpression,
+ )?),
+ 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) => {
+ let expr =
+ parse_expression_to_tree(value, &MdxExpressionKind::AttributeExpression)?;
+ 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,
+ )?),
+ 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)?;
+ 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 children = all(context, node)?;
+ // To do: remove whitespace?
+ // To do: return a single child if there is one?
+ Ok(Some(swc_ecma_ast::JSXElementChild::JSXFragment(
+ create_fragment(children, 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 an ident.
+fn create_ident(sym: &str) -> swc_ecma_ast::Ident {
+ swc_ecma_ast::Ident {
+ sym: sym.into(),
+ optional: false,
+ span: swc_common::DUMMY_SP,
+ }
+}
+
+/// 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)),
+ }
+}
+
+/// Turn a unist positions into an SWC span.
+fn position_to_span(position: Option<&Position>) -> swc_common::Span {
+ position.map_or(swc_common::DUMMY_SP, |d| swc_common::Span {
+ lo: swc_common::BytePos(d.start.offset as u32),
+ hi: swc_common::BytePos(d.end.offset as u32),
+ ctxt: swc_common::SyntaxContext::empty(),
+ })
+}
+
+/// 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"),
+];