From 117cfc10c6d4a0a9346a29353860d1185d1ea224 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Fri, 30 Sep 2022 18:17:54 +0200 Subject: Add support for turning hast into swc --- tests/test_utils/hast.rs | 25 +- tests/test_utils/mod.rs | 1 + tests/test_utils/swc.rs | 101 ++++++- tests/test_utils/to_hast.rs | 46 +-- tests/test_utils/to_swc.rs | 714 ++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 836 insertions(+), 51 deletions(-) create mode 100644 tests/test_utils/to_swc.rs (limited to 'tests/test_utils') 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 { /// > | /// ^^^^^^^^^^^^^^^ /// ``` -// 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, } -#[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), - SpaceSeparated(Vec), + CommaSeparated(Vec), + SpaceSeparated(Vec), } /// Document type. @@ -232,7 +225,7 @@ pub struct Text { /// > | /// ^^^^^ /// ``` -#[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: // -/// 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 { + 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, 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 - // - footnoteOrder: Vec - // - footnoteCounts: Record - 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: . 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, +} + +/// 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, + /// Declarations and stuff. + esm: Vec, +} + +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 { + 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, 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, 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? + // + 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: + // + 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, 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, 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, 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, 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, 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 { + 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, + children: Vec, + node: &hast::Node, +) -> Box { + 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, + 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) { + // `` + // `` + 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) + } + // `` + JsxName::Namespace(ns, name) => { + swc_ecma_ast::JSXElementName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName { + ns: create_ident(ns), + name: create_ident(name), + }) + } + // `` + 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") + } + // `` + JsxName::Namespace(ns, name) => { + swc_ecma_ast::JSXAttrName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName { + ns: create_ident(ns), + name: create_ident(name), + }) + } + // `` + 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; + } + + // `` + if !parts.is_empty() { + parts.push(&name[start..]); + JsxName::Member(parts) + } + // `` + else if let Some(colon) = bytes.iter().position(|d| matches!(d, b':')) { + JsxName::Namespace(&name[0..colon], &name[(colon + 1)..]) + } + // `` + 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} */ +// let shared = {}; +// /** @type {Record} */ +// let html = {}; +// /** @type {Record} */ +// 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"), +]; -- cgit