From e484d1ecc5e405259767c0fd84072226fee40b71 Mon Sep 17 00:00:00 2001 From: Titus Wormer Date: Tue, 11 Oct 2022 16:27:38 +0200 Subject: Refactor test utilities to improve names --- tests/test_utils/hast.rs | 7 +- tests/test_utils/hast_util_to_swc.rs | 783 +++++++++++++ tests/test_utils/jsx_rewrite.rs | 1164 -------------------- tests/test_utils/mdast_util_to_hast.rs | 1275 ++++++++++++++++++++++ tests/test_utils/mdx_plugin_recma_document.rs | 663 +++++++++++ tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs | 1169 ++++++++++++++++++++ tests/test_utils/micromark_swc_utils.rs | 134 --- tests/test_utils/mod.rs | 9 +- tests/test_utils/swc.rs | 5 +- tests/test_utils/swc_utils.rs | 138 ++- tests/test_utils/to_document.rs | 658 ----------- tests/test_utils/to_hast.rs | 1247 --------------------- tests/test_utils/to_swc.rs | 756 ------------- 13 files changed, 4036 insertions(+), 3972 deletions(-) create mode 100644 tests/test_utils/hast_util_to_swc.rs delete mode 100644 tests/test_utils/jsx_rewrite.rs create mode 100644 tests/test_utils/mdast_util_to_hast.rs create mode 100644 tests/test_utils/mdx_plugin_recma_document.rs create mode 100644 tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs delete mode 100644 tests/test_utils/micromark_swc_utils.rs delete mode 100644 tests/test_utils/to_document.rs delete mode 100644 tests/test_utils/to_hast.rs delete 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 db5326c..bc8f472 100644 --- a/tests/test_utils/hast.rs +++ b/tests/test_utils/hast.rs @@ -1,6 +1,9 @@ -#![allow(dead_code)] +//! HTML syntax tree: [hast][]. +//! +//! [hast]: https://github.com/syntax-tree/hast -// ^-- To do: fix later +#![allow(dead_code)] +// ^-- To do: externalize. extern crate alloc; extern crate micromark; diff --git a/tests/test_utils/hast_util_to_swc.rs b/tests/test_utils/hast_util_to_swc.rs new file mode 100644 index 0000000..a4bb9b9 --- /dev/null +++ b/tests/test_utils/hast_util_to_swc.rs @@ -0,0 +1,783 @@ +//! Turn an HTML AST into a JavaScript AST. +//! +//! Port of , by the same +//! author: +//! +//! (The MIT License) +//! +//! Copyright (c) 2016 Titus Wormer +//! +//! Permission is hereby granted, free of charge, to any person obtaining +//! a copy of this software and associated documentation files (the +//! 'Software'), to deal in the Software without restriction, including +//! without limitation the rights to use, copy, modify, merge, publish, +//! distribute, sublicense, and/or sell copies of the Software, and to +//! permit persons to whom the Software is furnished to do so, subject to +//! the following conditions: +//! +//! The above copyright notice and this permission notice shall be +//! included in all copies or substantial portions of the Software. +//! +//! THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +//! EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +//! MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +//! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +//! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +//! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +//! SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +extern crate swc_common; +extern crate swc_ecma_ast; +use crate::test_utils::{ + hast, + swc::{parse_esm_to_tree, parse_expression_to_tree}, + swc_utils::{create_ident, position_to_span}, +}; +use core::str; +use micromark::{Location, MdxExpressionKind}; + +/// Result. +#[derive(Debug, PartialEq, Eq)] +pub struct Program { + pub path: Option, + /// 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<'a> { + /// 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, + /// Optional way to turn relative positions into points. + location: Option<&'a Location>, +} + +#[allow(dead_code)] +pub fn hast_util_to_swc( + tree: &hast::Node, + path: Option, + location: Option<&Location>, +) -> Result { + let mut context = Context { + space: Space::Html, + comments: vec![], + esm: vec![], + location, + }; + let expr = match one(&mut context, tree)? { + Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { + Some(swc_ecma_ast::Expr::JSXFragment(x)) + } + Some(swc_ecma_ast::JSXElementChild::JSXElement(x)) => { + Some(swc_ecma_ast::Expr::JSXElement(x)) + } + Some(child) => Some(swc_ecma_ast::Expr::JSXFragment(create_fragment( + vec![child], + tree, + ))), + None => None, + }; + + // Add the ESM. + let mut module = swc_ecma_ast::Module { + shebang: None, + body: context.esm, + span: position_to_span(tree.position()), + }; + + // We have some content, wrap it. + if let Some(expr) = expr { + module + .body + .push(swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( + swc_ecma_ast::ExprStmt { + expr: Box::new(expr), + span: swc_common::DUMMY_SP, + }, + ))); + } + + Ok(Program { + path, + module, + comments: context.comments, + }) +} + +/// Transform one node. +fn one( + context: &mut Context, + node: &hast::Node, +) -> Result, 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, stops)) => { + Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( + swc_ecma_ast::JSXExprContainer { + expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( + value, + &MdxExpressionKind::AttributeValueExpression, + stops, + context.location, + )?), + span: swc_common::DUMMY_SP, + }, + )) + } + None => None, + }; + + swc_ecma_ast::JSXAttrOrSpread::JSXAttr(swc_ecma_ast::JSXAttr { + span: swc_common::DUMMY_SP, + name: create_jsx_attr_name(&prop.name), + value, + }) + } + hast::AttributeContent::Expression(value, stops) => { + let expr = parse_expression_to_tree( + value, + &MdxExpressionKind::AttributeExpression, + stops, + context.location, + )?; + swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { + dot3_token: swc_common::DUMMY_SP, + expr, + }) + } + }; + + attrs.push(attr); + index += 1; + } + + Ok(Some(if let Some(name) = &element.name { + swc_ecma_ast::JSXElementChild::JSXElement(create_element(name, attrs, children, node)) + } else { + swc_ecma_ast::JSXElementChild::JSXFragment(create_fragment(children, node)) + })) +} + +/// [`MdxExpression`][hast::MdxExpression]. +fn transform_mdx_expression( + context: &mut Context, + node: &hast::Node, + expression: &hast::MdxExpression, +) -> Result, String> { + Ok(Some(swc_ecma_ast::JSXElementChild::JSXExprContainer( + swc_ecma_ast::JSXExprContainer { + expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( + &expression.value, + &MdxExpressionKind::Expression, + &expression.stops, + context.location, + )?), + span: position_to_span(node.position()), + }, + ))) +} + +/// [`MdxjsEsm`][hast::MdxjsEsm]. +fn transform_mdxjs_esm( + context: &mut Context, + _node: &hast::Node, + esm: &hast::MdxjsEsm, +) -> Result, String> { + let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; + let mut index = 0; + + // To do: check that identifiers are not duplicated across esm blocks. + while index < module.body.len() { + if !matches!(module.body[index], swc_ecma_ast::ModuleItem::ModuleDecl(_)) { + return Err("Unexpected `statement` in code: only import/exports are supported".into()); + } + index += 1; + } + + context.esm.append(&mut module.body); + Ok(None) +} + +/// [`Root`][hast::Root]. +fn transform_root( + context: &mut Context, + node: &hast::Node, + _root: &hast::Root, +) -> Result, String> { + let mut children = all(context, node)?; + let mut queue = vec![]; + let mut nodes = vec![]; + let mut seen = false; + + children.reverse(); + + // Remove initial/final whitespace. + while let Some(child) = children.pop() { + let mut stash = false; + + if let swc_ecma_ast::JSXElementChild::JSXExprContainer(container) = &child { + if let swc_ecma_ast::JSXExpr::Expr(expr) = &container.expr { + if let swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(str)) = (*expr).as_ref() { + if inter_element_whitespace(str.value.as_ref()) { + stash = true; + } + } + } + } + + if stash { + if seen { + queue.push(child); + } + } else { + if !queue.is_empty() { + nodes.append(&mut queue); + } + nodes.push(child); + seen = true; + } + } + + Ok(Some(swc_ecma_ast::JSXElementChild::JSXFragment( + create_fragment(nodes, node), + ))) +} + +/// [`Text`][hast::Text]. +fn transform_text( + _context: &mut Context, + node: &hast::Node, + text: &hast::Text, +) -> Option { + 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 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)), + } +} + +fn inter_element_whitespace(value: &str) -> bool { + let bytes = value.as_bytes(); + let mut index = 0; + + while index < bytes.len() { + match bytes[index] { + b'\t' | 0x0C | b'\r' | b'\n' | b' ' => {} + _ => return false, + } + index += 1; + } + + true +} + +/// Different kinds of JSX names. +enum JsxName<'a> { + // `a.b.c` + Member(Vec<&'a str>), + // `a:b` + Namespace(&'a str, &'a str), + // `a` + Normal(&'a str), +} + +/// Parse a JSX name from a string. +fn parse_jsx_name(name: &str) -> JsxName { + let bytes = name.as_bytes(); + let mut index = 0; + let mut start = 0; + let mut parts = vec![]; + + while index < bytes.len() { + if bytes[index] == b'.' { + parts.push(&name[start..index]); + start = index + 1; + } + + index += 1; + } + + // `` + 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"), +]; diff --git a/tests/test_utils/jsx_rewrite.rs b/tests/test_utils/jsx_rewrite.rs deleted file mode 100644 index 33879b0..0000000 --- a/tests/test_utils/jsx_rewrite.rs +++ /dev/null @@ -1,1164 +0,0 @@ -extern crate swc_common; -extern crate swc_ecma_ast; -use crate::test_utils::{ - micromark_swc_utils::{position_to_string, span_to_position}, - swc_utils::{ - create_binary_expression, create_ident, create_ident_expression, create_member_expression, - }, - to_swc::Program, -}; -use micromark::{id_cont, id_start, unist::Position, Location}; -use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; - -/// Configuration. -#[derive(Debug, Default, Clone)] -pub struct Options { - /// Place to import a provider from. - /// - /// See [MDX provider](https://mdxjs.com/docs/using-mdx/#mdx-provider) - /// on the MDX website for more info. - pub provider_import_source: Option, - /// Whether to add extra information to error messages in generated code. - /// This is not yet supported. - pub development: bool, -} - -/// Rewrite JSX in an MDX file so that components can be passed in and provided. -#[allow(dead_code)] -pub fn jsx_rewrite( - mut program: Program, - options: &Options, - location: Option<&Location>, -) -> Program { - let mut state = State { - scopes: vec![], - location, - provider: options.provider_import_source.is_some(), - path: program.path.clone(), - development: options.development, - create_provider_import: false, - create_error_helper: false, - }; - state.enter(Some(Info::default())); - program.module.visit_mut_with(&mut state); - - // If a provider is used (and can be used), import it. - if let Some(source) = &options.provider_import_source { - if state.create_provider_import { - program - .module - .body - .insert(0, create_import_provider(source)) - } - } - - // If potentially missing components are used, add the helper used for - // errors. - if state.create_error_helper { - program - .module - .body - .push(create_error_helper(state.development, state.path)); - } - - program -} - -/// Collection of different SWC functions. -#[derive(Debug)] -enum Func<'a> { - /// Function declaration. - Decl(&'a mut swc_ecma_ast::FnDecl), - /// Function expression. - Expr(&'a mut swc_ecma_ast::FnExpr), - /// Arrow function. - Arrow(&'a mut swc_ecma_ast::ArrowExpr), -} - -/// Info for a function scope. -#[derive(Debug, Default, Clone)] -struct Info { - /// Function name. - name: Option, - /// Used objects (`a` in ``). - objects: Vec, - /// Used components (``). - components: Vec, - /// Used literals (``). - tags: Vec, - /// List of JSX identifiers of literal tags that are not valid JS - /// identifiers in the shape of `Vec<(invalid, valid)>`. - /// - /// Example: - /// - /// ``` - /// vec![("a-b".into(), "_component0".into())] - /// ``` - aliases: Vec<(String, String)>, - /// Non-literal references in the shape of `Vec<(name, is_component)>`. - /// - /// Example: - /// - /// ``` - /// vec![("a".into(), false), ("a.b".into(), true)] - /// ``` - references: Vec<(String, bool, Option)>, -} - -/// Scope (block or function/global). -#[derive(Debug, Clone)] -struct Scope { - /// If this is a function (or global) scope, we track info. - info: Option, - /// Things that are defined in this scope. - defined: Vec, -} - -/// Context. -#[derive(Debug, Default, Clone)] -struct State<'a> { - location: Option<&'a Location>, - /// Path to file. - path: Option, - /// List of current scopes. - scopes: Vec, - /// Whether the user is in development mode. - development: bool, - /// Whether the user uses a provider. - provider: bool, - /// Whether a provider is referenced. - create_provider_import: bool, - /// Whether a missing component helper is referenced. - /// - /// When things are referenced that might not be defined, we reference a - /// helper function to throw when they are missing. - create_error_helper: bool, -} - -impl<'a> State<'a> { - /// Open a new scope. - fn enter(&mut self, info: Option) { - self.scopes.push(Scope { - info, - defined: vec![], - }); - } - - /// Close the current scope. - fn exit(&mut self) -> Scope { - self.scopes.pop().expect("expected scope") - } - - /// Close a function. - fn exit_func(&mut self, func: Func) { - let mut scope = self.exit(); - let mut defaults = vec![]; - let mut info = scope.info.take().unwrap(); - let mut index = 0; - - // Create defaults for tags. - // - // ```jsx - // {h1: 'h1'} - // ``` - while index < info.tags.len() { - let name = &info.tags[index]; - - defaults.push(swc_ecma_ast::PropOrSpread::Prop(Box::new( - swc_ecma_ast::Prop::KeyValue(swc_ecma_ast::KeyValueProp { - key: if is_identifier_name(name) { - swc_ecma_ast::PropName::Ident(create_ident(name)) - } else { - swc_ecma_ast::PropName::Str(swc_ecma_ast::Str { - value: name.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }) - }, - value: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: name.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }), - ))); - - index += 1; - } - - let mut actual = info.components.split_off(0); - let mut index = 0; - - // In some cases, a component is used directly (``) but it’s also - // used as an object (``). - while index < info.objects.len() { - if !actual.contains(&info.objects[index]) { - actual.push(info.objects[index].clone()); - } - index += 1; - } - - let mut statements = vec![]; - - if !defaults.is_empty() || !actual.is_empty() || !info.aliases.is_empty() { - let mut parameters = vec![]; - - // Use a provider, if configured. - // - // ```jsx - // _provideComponents() - // ``` - if self.provider { - self.create_provider_import = true; - parameters.push(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( - "_provideComponents", - ))), - args: vec![], - type_args: None, - span: swc_common::DUMMY_SP, - })); - } - - // Accept `components` as a prop if this is the `MDXContent` or - // `_createMdxContent` function. - // - // ```jsx - // props.components - // ``` - if is_props_receiving_fn(&info.name) { - parameters.push(swc_ecma_ast::Expr::Member(swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("props")), - prop: swc_ecma_ast::MemberProp::Ident(create_ident("components")), - span: swc_common::DUMMY_SP, - })); - } - - // Inject an object at the start, when: - // - there are defaults, - // - there are two sources - // - // ```jsx - // (_provideComponents(), props.components) - // () - // ``` - // - // To: - // - // ```jsx - // ({}, _provideComponents(), props.components) - // ({h1: 'h1'}) - // ``` - if !defaults.is_empty() || parameters.len() > 1 { - parameters.insert( - 0, - swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: defaults, - span: swc_common::DUMMY_SP, - }), - ); - } - - // Merge things and prevent errors. - // - // ```jsx - // {}, _provideComponents(), props.components - // props.components - // _provideComponents() - // ``` - // - // To: - // - // ```jsx - // Object.assign({}, _provideComponents(), props.components) - // props.components || {} - // _provideComponents() - // ``` - let mut components_init = if parameters.len() > 1 { - let mut args = vec![]; - parameters.reverse(); - while let Some(param) = parameters.pop() { - args.push(swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(param), - }); - } - swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Member( - swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("Object")), - prop: swc_ecma_ast::MemberProp::Ident(create_ident("assign")), - span: swc_common::DUMMY_SP, - }, - ))), - args, - type_args: None, - span: swc_common::DUMMY_SP, - }) - } else { - // Always one. - let param = parameters.pop().unwrap(); - - if let swc_ecma_ast::Expr::Member(_) = param { - create_binary_expression( - vec![ - param, - swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: vec![], - span: swc_common::DUMMY_SP, - }), - ], - swc_ecma_ast::BinaryOp::LogicalOr, - ) - } else { - param - } - }; - - // Add components to scope. - // - // For `['MyComponent', 'MDXLayout']` this generates: - // - // ```js - // const {MyComponent, wrapper: MDXLayout} = _components - // ``` - // - // Note that MDXLayout is special as it’s taken from - // `_components.wrapper`. - let components_pattern = if actual.is_empty() { - None - } else { - let mut props = vec![]; - actual.reverse(); - while let Some(key) = actual.pop() { - // `wrapper: MDXLayout` - if key == "MDXLayout" { - props.push(swc_ecma_ast::ObjectPatProp::KeyValue( - swc_ecma_ast::KeyValuePatProp { - key: swc_ecma_ast::PropName::Ident(create_ident("wrapper")), - value: Box::new(swc_ecma_ast::Pat::Ident( - swc_ecma_ast::BindingIdent { - id: create_ident(&key), - type_ann: None, - }, - )), - }, - )) - } - // `MyComponent` - else { - props.push(swc_ecma_ast::ObjectPatProp::Assign( - swc_ecma_ast::AssignPatProp { - key: create_ident(&key), - value: None, - span: swc_common::DUMMY_SP, - }, - )) - } - } - - Some(swc_ecma_ast::ObjectPat { - props, - optional: false, - span: swc_common::DUMMY_SP, - type_ann: None, - }) - }; - - let mut declarators = vec![]; - - // If there are tags, they take them from `_components`, so we need - // to make it defined. - if !info.tags.is_empty() { - declarators.push(swc_ecma_ast::VarDeclarator { - span: swc_common::DUMMY_SP, - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("_components"), - type_ann: None, - }), - init: Some(Box::new(components_init)), - definite: false, - }); - components_init = create_ident_expression("_components"); - } - - // For JSX IDs that can’t be represented as JavaScript IDs (as in, - // those with dashes, such as `custom-element`), we generated a - // separate variable that is a valid JS ID (such as `_component0`), - // and here we take it from components: - // ```js - // const _component0 = _components['custom-element'] - // ``` - if !info.aliases.is_empty() { - info.aliases.reverse(); - - while let Some((id, name)) = info.aliases.pop() { - declarators.push(swc_ecma_ast::VarDeclarator { - span: swc_common::DUMMY_SP, - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident(&name), - type_ann: None, - }), - init: Some(Box::new(swc_ecma_ast::Expr::Member( - swc_ecma_ast::MemberExpr { - obj: Box::new(create_ident_expression("_components")), - prop: swc_ecma_ast::MemberProp::Computed( - swc_ecma_ast::ComputedPropName { - expr: Box::new(swc_ecma_ast::Expr::Lit( - swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: id.into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - )), - span: swc_common::DUMMY_SP, - }, - ), - span: swc_common::DUMMY_SP, - }, - ))), - definite: false, - }); - } - } - - if let Some(pat) = components_pattern { - declarators.push(swc_ecma_ast::VarDeclarator { - name: swc_ecma_ast::Pat::Object(pat), - init: Some(Box::new(components_init)), - span: swc_common::DUMMY_SP, - definite: false, - }); - } - - // Add the variable declaration. - statements.push(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( - swc_ecma_ast::VarDecl { - kind: swc_ecma_ast::VarDeclKind::Const, - decls: declarators, - span: swc_common::DUMMY_SP, - declare: false, - }, - )))); - } - - // Add checks at runtime to verify that object/components are passed. - // - // ```js - // if (!a) _missingMdxReference("a", false); - // if (!a.b) _missingMdxReference("a.b", true); - // ``` - for (id, component, position) in info.references { - self.create_error_helper = true; - - let mut args = vec![ - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: id.clone().into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }, - swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( - swc_ecma_ast::Bool { - value: component, - span: swc_common::DUMMY_SP, - }, - ))), - }, - ]; - - // Add the source location if it exists and if `development` is on. - if let Some(position) = position.as_ref() { - if self.development { - args.push(swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: position_to_string(position).into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - }) - } - } - - statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { - test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { - op: swc_ecma_ast::UnaryOp::Bang, - arg: Box::new(create_member_expression(&id)), - span: swc_common::DUMMY_SP, - })), - cons: Box::new(swc_ecma_ast::Stmt::Expr(swc_ecma_ast::ExprStmt { - span: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( - "_missingMdxReference", - ))), - args, - type_args: None, - span: swc_common::DUMMY_SP, - })), - })), - alt: None, - span: swc_common::DUMMY_SP, - })); - } - - // Add statements to functions. - if !statements.is_empty() { - let mut body: &mut swc_ecma_ast::BlockStmt = match func { - Func::Expr(expr) => { - if expr.function.body.is_none() { - expr.function.body = Some(swc_ecma_ast::BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }); - } - expr.function.body.as_mut().unwrap() - } - Func::Decl(decl) => { - if decl.function.body.is_none() { - decl.function.body = Some(swc_ecma_ast::BlockStmt { - stmts: vec![], - span: swc_common::DUMMY_SP, - }); - } - decl.function.body.as_mut().unwrap() - } - Func::Arrow(arr) => { - if let swc_ecma_ast::BlockStmtOrExpr::Expr(expr) = &mut arr.body { - arr.body = - swc_ecma_ast::BlockStmtOrExpr::BlockStmt(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - // To do: figure out non-clone. - arg: Some(expr.clone()), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }); - } - arr.body.as_mut_block_stmt().unwrap() - } - }; - - statements.append(&mut body.stmts.split_off(0)); - body.stmts = statements; - } - } - - /// Get the current function scope. - fn current_fn_scope_mut(&mut self) -> &mut Scope { - let mut index = self.scopes.len(); - - while index > 0 { - index -= 1; - if self.scopes[index].info.is_some() { - return &mut self.scopes[index]; - } - } - - unreachable!("expected scope") - } - - /// Get the current scope. - fn current_scope_mut(&mut self) -> &mut Scope { - self.scopes.last_mut().expect("expected scope") - } - - /// Get the top-level scope’s info. - fn current_top_level_info(&self) -> Option<&Info> { - if let Some(scope) = self.scopes.get(1) { - scope.info.as_ref() - } else { - None - } - } - - /// Get the top-level scope’s info, mutably. - fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { - if let Some(scope) = self.scopes.get_mut(1) { - scope.info.as_mut() - } else { - None - } - } - - /// Check if `id` is in scope. - fn in_scope(&self, id: &String) -> bool { - let mut index = self.scopes.len(); - - while index > 0 { - index -= 1; - if self.scopes[index].defined.contains(id) { - return true; - } - } - - false - } - - /// Add an identifier to a scope. - fn add_id(&mut self, id: String, block: bool) { - let scope = if block { - self.current_scope_mut() - } else { - self.current_fn_scope_mut() - }; - scope.defined.push(id); - } - - // Add a pattern to a scope. - fn add_pat(&mut self, pat: &swc_ecma_ast::Pat, block: bool) { - match pat { - // `x` - swc_ecma_ast::Pat::Ident(d) => self.add_id(d.id.sym.to_string(), block), - // `...x` - swc_ecma_ast::Pat::Array(d) => { - let mut index = 0; - while index < d.elems.len() { - if let Some(d) = &d.elems[index] { - self.add_pat(d, block); - } - index += 1; - } - } - // `...x` - swc_ecma_ast::Pat::Rest(d) => self.add_pat(&d.arg, block), - // `{x=y}` - swc_ecma_ast::Pat::Assign(d) => self.add_pat(&d.left, block), - swc_ecma_ast::Pat::Object(d) => { - let mut index = 0; - while index < d.props.len() { - match &d.props[index] { - // `{...x}` - swc_ecma_ast::ObjectPatProp::Rest(d) => { - self.add_pat(&d.arg, block); - } - // `{key: value}` - swc_ecma_ast::ObjectPatProp::KeyValue(d) => { - self.add_pat(&d.value, block); - } - // `{key}` or `{key = value}` - swc_ecma_ast::ObjectPatProp::Assign(d) => { - self.add_id(d.key.to_string(), block); - } - } - index += 1; - } - } - // Ignore `Invalid` / `Expr`. - _ => {} - } - } -} - -impl<'a> VisitMut for State<'a> { - noop_visit_mut_type!(); - - /// Rewrite JSX identifiers. - fn visit_mut_jsx_element(&mut self, node: &mut swc_ecma_ast::JSXElement) { - // If there is a top-level, non-global, scope which is a function. - if let Some(info) = self.current_top_level_info() { - // Rewrite only if we can rewrite. - if is_props_receiving_fn(&info.name) || self.provider { - let position = span_to_position(&node.span, self.location); - match &node.opening.name { - // ``, ``, ``. - swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { - let mut ids = vec![]; - let mut mem = d; - loop { - ids.push(mem.prop.sym.to_string()); - match &mem.obj { - swc_ecma_ast::JSXObject::Ident(d) => { - ids.push(d.sym.to_string()); - break; - } - swc_ecma_ast::JSXObject::JSXMemberExpr(d) => { - mem = d; - } - } - } - ids.reverse(); - let primary_id = ids.first().unwrap().clone(); - let in_scope = self.in_scope(&primary_id); - - if !in_scope { - let info_mut = self.current_top_level_info_mut().unwrap(); - - let mut index = 1; - while index <= ids.len() { - let full_id = ids[0..index].join("."); - let component = index == ids.len(); - if let Some(reference) = - info_mut.references.iter_mut().find(|d| d.0 == full_id) - { - if component { - reference.1 = true; - } - } else { - info_mut - .references - .push((full_id, component, position.clone())) - } - index += 1; - } - - if !info_mut.objects.contains(&primary_id) { - info_mut.objects.push(primary_id); - } - } - } - // ``, ``, `<$>`, `<_bar>`, ``. - swc_ecma_ast::JSXElementName::Ident(d) => { - // If the name is a valid ES identifier, and it doesn’t - // start with a lowercase letter, it’s a component. - // For example, `$foo`, `_bar`, `Baz` are all component - // names. - // But `foo` and `b-ar` are tag names. - let id = d.sym.to_string(); - - if is_literal_name(&id) { - // To do: ignore explicit JSX? - - let mut invalid = None; - - let name = if is_identifier_name(&id) { - swc_ecma_ast::JSXElementName::JSXMemberExpr( - swc_ecma_ast::JSXMemberExpr { - obj: swc_ecma_ast::JSXObject::Ident(create_ident( - "_components", - )), - prop: create_ident(&id), - }, - ) - } else { - let name = if let Some(invalid_ref) = - info.aliases.iter().find(|d| d.0 == id) - { - invalid_ref.1.clone() - } else { - let name = format!("_component{}", info.aliases.len()); - invalid = Some((id.clone(), name.clone())); - name - }; - - swc_ecma_ast::JSXElementName::Ident(create_ident(&name)) - }; - - let info_mut = self.current_top_level_info_mut().unwrap(); - - if !info_mut.tags.contains(&id) { - info_mut.tags.push(id); - } - - if let Some(invalid) = invalid { - info_mut.aliases.push(invalid) - } - - if let Some(closing) = node.closing.as_mut() { - closing.name = name.clone(); - } - - node.opening.name = name; - } else { - let mut is_layout = false; - - // The MDXLayout is wrapped in a - if let Some(name) = &info.name { - if name == "MDXContent" && id == "MDXLayout" { - is_layout = true; - } - } - - if !self.in_scope(&id) { - let info_mut = self.current_top_level_info_mut().unwrap(); - - if !is_layout { - if let Some(reference) = - info_mut.references.iter_mut().find(|d| d.0 == id) - { - reference.1 = true; - } else { - info_mut.references.push((id.clone(), true, position)) - } - } - - if !info_mut.components.contains(&id) { - info_mut.components.push(id); - } - } - } - } - // ``. - swc_ecma_ast::JSXElementName::JSXNamespacedName(_) => { - // Ignore. - } - } - } - } - - node.visit_mut_children_with(self); - } - - /// Add specifiers of import declarations. - fn visit_mut_import_decl(&mut self, node: &mut swc_ecma_ast::ImportDecl) { - let mut index = 0; - while index < node.specifiers.len() { - let ident = match &node.specifiers[index] { - swc_ecma_ast::ImportSpecifier::Default(x) => &x.local.sym, - swc_ecma_ast::ImportSpecifier::Namespace(x) => &x.local.sym, - swc_ecma_ast::ImportSpecifier::Named(x) => &x.local.sym, - }; - self.add_id(ident.to_string(), false); - index += 1; - } - - node.visit_mut_children_with(self); - } - - /// Add patterns of variable declarations. - fn visit_mut_var_decl(&mut self, node: &mut swc_ecma_ast::VarDecl) { - let block = node.kind != swc_ecma_ast::VarDeclKind::Var; - let mut index = 0; - while index < node.decls.len() { - self.add_pat(&node.decls[index].name, block); - index += 1; - } - node.visit_mut_children_with(self); - } - - /// Add identifier of class declaration. - fn visit_mut_class_decl(&mut self, node: &mut swc_ecma_ast::ClassDecl) { - self.add_id(node.ident.sym.to_string(), false); - node.visit_mut_children_with(self); - } - - /// On function declarations, add name, create scope, add parameters. - fn visit_mut_fn_decl(&mut self, node: &mut swc_ecma_ast::FnDecl) { - let id = node.ident.sym.to_string(); - self.add_id(id.clone(), false); - self.enter(Some(Info { - name: Some(id), - ..Default::default() - })); - let mut index = 0; - while index < node.function.params.len() { - self.add_pat(&node.function.params[index].pat, false); - index += 1; - } - node.visit_mut_children_with(self); - // Rewrite. - self.exit_func(Func::Decl(node)); - } - - /// On function expressions, add name, create scope, add parameters. - fn visit_mut_fn_expr(&mut self, node: &mut swc_ecma_ast::FnExpr) { - // Note: `periscopic` adds the ID to the newly generated scope, for - // fn expressions. - // That seems wrong? - let name = if let Some(ident) = &node.ident { - let id = ident.sym.to_string(); - self.add_id(id.clone(), false); - Some(id) - } else { - None - }; - - self.enter(Some(Info { - name, - ..Default::default() - })); - let mut index = 0; - while index < node.function.params.len() { - self.add_pat(&node.function.params[index].pat, false); - index += 1; - } - node.visit_mut_children_with(self); - self.exit_func(Func::Expr(node)); - } - - /// On arrow functions, create scope, add parameters. - fn visit_mut_arrow_expr(&mut self, node: &mut swc_ecma_ast::ArrowExpr) { - self.enter(Some(Info::default())); - let mut index = 0; - while index < node.params.len() { - self.add_pat(&node.params[index], false); - index += 1; - } - node.visit_mut_children_with(self); - self.exit_func(Func::Arrow(node)); - } - - // Blocks. - // Not sure why `periscopic` only does `For`/`ForIn`/`ForOf`/`Block`. - // I added `While`/`DoWhile` here just to be sure. - // But there are more. - /// On for statements, create scope. - fn visit_mut_for_stmt(&mut self, node: &mut swc_ecma_ast::ForStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On for/in statements, create scope. - fn visit_mut_for_in_stmt(&mut self, node: &mut swc_ecma_ast::ForInStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On for/of statements, create scope. - fn visit_mut_for_of_stmt(&mut self, node: &mut swc_ecma_ast::ForOfStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On while statements, create scope. - fn visit_mut_while_stmt(&mut self, node: &mut swc_ecma_ast::WhileStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On do/while statements, create scope. - fn visit_mut_do_while_stmt(&mut self, node: &mut swc_ecma_ast::DoWhileStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - /// On block statements, create scope. - fn visit_mut_block_stmt(&mut self, node: &mut swc_ecma_ast::BlockStmt) { - self.enter(None); - node.visit_mut_children_with(self); - self.exit(); - } - - /// On catch clauses, create scope, add param. - fn visit_mut_catch_clause(&mut self, node: &mut swc_ecma_ast::CatchClause) { - self.enter(None); - if let Some(pat) = &node.param { - self.add_pat(pat, true); - } - node.visit_mut_children_with(self); - self.exit(); - } -} - -/// Generate an import provider. -/// -/// ```js -/// import { useMDXComponents as _provideComponents } from "x" -/// ``` -fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import( - swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: create_ident("_provideComponents"), - imported: Some(swc_ecma_ast::ModuleExportName::Ident(create_ident( - "useMDXComponents", - ))), - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: Box::new(swc_ecma_ast::Str { - value: source.into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }, - )) -} - -/// Generate an error helper. -/// -/// ```js -/// function _missingMdxReference(id, component) { -/// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); -/// } -/// ``` -fn create_error_helper(development: bool, path: Option) -> swc_ecma_ast::ModuleItem { - let mut parameters = vec![ - swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("id"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }, - swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("component"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }, - ]; - - // Accept a source location (which might be undefiend). - if development { - parameters.push(swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: create_ident("place"), - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }) - } - - let mut message = vec![ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "Expected ".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - // `component ? "component" : "object"` - swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { - expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(create_ident_expression("component")), - cons: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "component".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "object".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: " `".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - create_ident_expression("id"), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "` to be defined: you likely forgot to import, pass, or provide it.".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - ]; - - // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` - if development { - message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { - expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(create_ident_expression("place")), - cons: Box::new(create_binary_expression( - vec![ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: "\nIt’s referenced in your code at `".into(), - span: swc_common::DUMMY_SP, - raw: None, - })), - create_ident_expression("place"), - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { - value: if let Some(path) = path { - format!("` in `{}`", path).into() - } else { - "`".into() - }, - span: swc_common::DUMMY_SP, - raw: None, - })), - ], - swc_ecma_ast::BinaryOp::Add, - )), - alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( - swc_ecma_ast::Str { - value: "".into(), - span: swc_common::DUMMY_SP, - raw: None, - }, - ))), - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - })) - } - - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( - swc_ecma_ast::FnDecl { - ident: create_ident("_missingMdxReference"), - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: parameters, - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Throw(swc_ecma_ast::ThrowStmt { - arg: Box::new(swc_ecma_ast::Expr::New(swc_ecma_ast::NewExpr { - callee: Box::new(create_ident_expression("Error")), - args: Some(vec![swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(create_binary_expression( - message, - swc_ecma_ast::BinaryOp::Add, - )), - }]), - span: swc_common::DUMMY_SP, - type_args: None, - })), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }, - ))) -} - -/// Check if this function is a props receiving component: it’s one of ours. -fn is_props_receiving_fn(name: &Option) -> bool { - if let Some(name) = name { - name == "_createMdxContent" || name == "MDXContent" - } else { - false - } -} - -/// Check if a name is a literal tag name or an identifier to a component. -fn is_literal_name(name: &str) -> bool { - matches!(name.as_bytes().first(), Some(b'a'..=b'z')) || !is_identifier_name(name) -} - -// Check if a name is a valid identifier name. -fn is_identifier_name(name: &str) -> bool { - for (index, char) in name.chars().enumerate() { - if if index == 0 { - !id_start(char) - } else { - !id_cont(char, false) - } { - return false; - } - } - - true -} diff --git a/tests/test_utils/mdast_util_to_hast.rs b/tests/test_utils/mdast_util_to_hast.rs new file mode 100644 index 0000000..c07d15b --- /dev/null +++ b/tests/test_utils/mdast_util_to_hast.rs @@ -0,0 +1,1275 @@ +//! Turn a markdown AST into an HTML AST. +//! +//! Port of , by the same +//! author: +//! +//! (The MIT License) +//! +//! Copyright (c) 2016 Titus Wormer +//! +//! Permission is hereby granted, free of charge, to any person obtaining +//! a copy of this software and associated documentation files (the +//! 'Software'), to deal in the Software without restriction, including +//! without limitation the rights to use, copy, modify, merge, publish, +//! distribute, sublicense, and/or sell copies of the Software, and to +//! permit persons to whom the Software is furnished to do so, subject to +//! the following conditions: +//! +//! The above copyright notice and this permission notice shall be +//! included in all copies or substantial portions of the Software. +//! +//! THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +//! EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +//! MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +//! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +//! CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +//! TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +//! SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +use crate::test_utils::hast; +use micromark::{mdast, sanitize, unist::Position}; + +// To do: support these compile options: +// ``` +// pub gfm_footnote_label: Option, +// pub gfm_footnote_label_tag_name: Option, +// pub gfm_footnote_label_attributes: Option, +// pub gfm_footnote_back_label: Option, +// pub gfm_footnote_clobber_prefix: Option, +// ``` +// +// Maybe also: +// * option to persist `meta`? +// * option to generate a `style` attribute instead of `align`? +// * support `Raw` nodes for HTML? +// +// To do: +// * revert references when undefined? +// + +#[derive(Debug)] +struct State { + definitions: Vec<(String, String, Option)>, + footnote_definitions: Vec<(String, Vec)>, + footnote_calls: Vec<(String, usize)>, +} + +#[derive(Debug)] +enum Result { + Fragment(Vec), + Node(hast::Node), + None, +} + +#[allow(dead_code)] +pub fn mdast_util_to_hast(mdast: &mdast::Node) -> hast::Node { + let mut definitions = vec![]; + + // Collect definitions. + // Calls take info from their definition. + // Calls can come come before definitions. + // Footnote calls can also come before footnote definitions, but those + // calls *do not* take info from their definitions, so we don’t care + // about footnotes here. + visit(mdast, |node| { + if let mdast::Node::Definition(definition) = node { + definitions.push(( + definition.identifier.clone(), + definition.url.clone(), + definition.title.clone(), + )); + } + }); + + let mut state = State { + definitions, + footnote_definitions: vec![], + footnote_calls: vec![], + }; + + let result = one(&mut state, mdast, None); + + if state.footnote_calls.is_empty() { + if let Result::Node(node) = result { + return node; + } + } + + // We either have to generate a footer, or we don’t have a single node. + // So we need a root. + let mut root = hast::Root { + children: vec![], + position: None, + }; + + match result { + Result::Fragment(children) => root.children = children, + Result::Node(node) => { + if let hast::Node::Root(existing) = node { + root = existing; + } else { + root.children.push(node); + } + } + Result::None => {} + } + + if !state.footnote_calls.is_empty() { + let mut items = vec![]; + + let mut index = 0; + while index < state.footnote_calls.len() { + let (id, count) = &state.footnote_calls[index]; + let safe_id = sanitize(&id.to_lowercase()); + + // Find definition: we’ll always find it. + let mut definition_index = 0; + while definition_index < state.footnote_definitions.len() { + if &state.footnote_definitions[definition_index].0 == id { + break; + } + definition_index += 1; + } + debug_assert_ne!( + definition_index, + state.footnote_definitions.len(), + "expected definition" + ); + + // We’ll find each used definition once, so we can split off to take the content. + let mut content = state.footnote_definitions[definition_index].1.split_off(0); + + let mut reference_index = 0; + let mut backreferences = vec![]; + while reference_index < *count { + let mut backref_children = vec![hast::Node::Text(hast::Text { + value: "↩".into(), + position: None, + })]; + + if reference_index != 0 { + backreferences.push(hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + })); + + backref_children.push(hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: (reference_index + 1).to_string(), + position: None, + })], + position: None, + })); + } + + backreferences.push(hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String(format!( + "#fnref-{}{}", + safe_id, + if reference_index == 0 { + "".into() + } else { + format!("-{}", &(reference_index + 1).to_string()) + } + )), + ), + ( + "dataFootnoteBackref".into(), + hast::PropertyValue::Boolean(true), + ), + ( + "ariaLabel".into(), + hast::PropertyValue::String("Back to content".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + "data-footnote-backref".into() + ]), + ), + ], + children: backref_children, + position: None, + })); + + reference_index += 1; + } + + let mut backreference_opt = Some(backreferences); + + if let Some(hast::Node::Element(tail_element)) = content.last_mut() { + if tail_element.tag_name == "p" { + if let Some(hast::Node::Text(text)) = tail_element.children.last_mut() { + text.value.push(' '); + } else { + tail_element.children.push(hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + })); + } + + tail_element + .children + .append(&mut backreference_opt.take().unwrap()); + } + } + + // No paragraph, just push them. + if let Some(mut backreference) = backreference_opt { + content.append(&mut backreference); + } + + items.push(hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties: vec![( + "id".into(), + hast::PropertyValue::String(format!("#fn-{}", safe_id)), + )], + children: wrap(content, true), + position: None, + })); + index += 1; + } + + root.children.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + root.children.push(hast::Node::Element(hast::Element { + tag_name: "section".into(), + properties: vec![ + ("dataFootnotes".into(), hast::PropertyValue::Boolean(true)), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]), + ), + ], + children: vec![ + hast::Node::Element(hast::Element { + tag_name: "h2".into(), + properties: vec![ + ( + "id".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["sr-only".into()]), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: "Footnotes".into(), + position: None, + })], + position: None, + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + hast::Node::Element(hast::Element { + tag_name: "ol".into(), + properties: vec![], + children: wrap(items, true), + position: None, + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + ], + position: None, + })); + root.children.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + hast::Node::Root(root) +} + +fn one(state: &mut State, node: &mdast::Node, parent: Option<&mdast::Node>) -> Result { + match node { + mdast::Node::BlockQuote(d) => transform_block_quote(state, node, d), + mdast::Node::Break(d) => transform_break(state, node, d), + mdast::Node::Code(d) => transform_code(state, node, d), + mdast::Node::Delete(d) => transform_delete(state, node, d), + mdast::Node::Emphasis(d) => transform_emphasis(state, node, d), + mdast::Node::FootnoteDefinition(d) => transform_footnote_definition(state, node, d), + mdast::Node::FootnoteReference(d) => transform_footnote_reference(state, node, d), + mdast::Node::Heading(d) => transform_heading(state, node, d), + mdast::Node::Image(d) => transform_image(state, node, d), + mdast::Node::ImageReference(d) => transform_image_reference(state, node, d), + mdast::Node::InlineCode(d) => transform_inline_code(state, node, d), + mdast::Node::InlineMath(d) => transform_inline_math(state, node, d), + mdast::Node::Link(d) => transform_link(state, node, d), + mdast::Node::LinkReference(d) => transform_link_reference(state, node, d), + mdast::Node::ListItem(d) => transform_list_item(state, node, parent, d), + mdast::Node::List(d) => transform_list(state, node, d), + mdast::Node::Math(d) => transform_math(state, node, d), + mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => { + transform_mdx_expression(state, node) + } + mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => { + transform_mdx_jsx_element(state, node) + } + mdast::Node::MdxjsEsm(d) => transform_mdxjs_esm(state, node, d), + mdast::Node::Paragraph(d) => transform_paragraph(state, node, d), + mdast::Node::Root(d) => transform_root(state, node, d), + mdast::Node::Strong(d) => transform_strong(state, node, d), + // Note: this is only called here if there is a single cell passed, not when one is found in a table. + mdast::Node::TableCell(d) => { + transform_table_cell(state, node, false, mdast::AlignKind::None, d) + } + // Note: this is only called here if there is a single row passed, not when one is found in a table. + mdast::Node::TableRow(d) => transform_table_row(state, node, false, None, d), + mdast::Node::Table(d) => transform_table(state, node, d), + mdast::Node::Text(d) => transform_text(state, node, d), + mdast::Node::ThematicBreak(d) => transform_thematic_break(state, node, d), + // Ignore. + mdast::Node::Definition(_) + | mdast::Node::Html(_) + | mdast::Node::Yaml(_) + | mdast::Node::Toml(_) => Result::None, + } +} + +/// [`BlockQuote`][mdast::BlockQuote]. +fn transform_block_quote( + state: &mut State, + node: &mdast::Node, + block_quote: &mdast::BlockQuote, +) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "blockquote".into(), + properties: vec![], + children: wrap(all(state, node), true), + position: block_quote.position.clone(), + })) +} + +/// [`Break`][mdast::Break]. +fn transform_break(_state: &mut State, _node: &mdast::Node, break_: &mdast::Break) -> Result { + Result::Fragment(vec![ + hast::Node::Element(hast::Element { + tag_name: "br".into(), + properties: vec![], + children: vec![], + position: break_.position.clone(), + }), + hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + }), + ]) +} + +/// [`Code`][mdast::Code]. +fn transform_code(_state: &mut State, _node: &mdast::Node, code: &mdast::Code) -> Result { + let mut value = code.value.clone(); + value.push('\n'); + let mut properties = vec![]; + + if let Some(lang) = code.lang.as_ref() { + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![format!("language-{}", lang)]), + )); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties, + children: vec![hast::Node::Text(hast::Text { + value, + position: None, + })], + position: code.position.clone(), + })], + position: code.position.clone(), + })) +} + +/// [`Delete`][mdast::Delete]. +fn transform_delete(state: &mut State, node: &mdast::Node, delete: &mdast::Delete) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "del".into(), + properties: vec![], + children: all(state, node), + position: delete.position.clone(), + })) +} + +/// [`Emphasis`][mdast::Emphasis]. +fn transform_emphasis(state: &mut State, node: &mdast::Node, emphasis: &mdast::Emphasis) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "em".into(), + properties: vec![], + children: all(state, node), + position: emphasis.position.clone(), + })) +} + +/// [`FootnoteDefinition`][mdast::FootnoteDefinition]. +fn transform_footnote_definition( + state: &mut State, + node: &mdast::Node, + footnote_definition: &mdast::FootnoteDefinition, +) -> Result { + let children = all(state, node); + // Set aside. + state + .footnote_definitions + .push((footnote_definition.identifier.clone(), children)); + Result::None +} + +/// [`FootnoteReference`][mdast::FootnoteReference]. +fn transform_footnote_reference( + state: &mut State, + _node: &mdast::Node, + footnote_reference: &mdast::FootnoteReference, +) -> Result { + let safe_id = sanitize(&footnote_reference.identifier.to_lowercase()); + let mut call_index = 0; + + // See if this has been called before. + while call_index < state.footnote_calls.len() { + if state.footnote_calls[call_index].0 == footnote_reference.identifier { + break; + } + call_index += 1; + } + + // New. + if call_index == state.footnote_calls.len() { + state + .footnote_calls + .push((footnote_reference.identifier.clone(), 0)); + } + + // Increment. + state.footnote_calls[call_index].1 += 1; + + let reuse_counter = state.footnote_calls[call_index].1; + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "sup".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties: vec![ + ( + "href".into(), + hast::PropertyValue::String(format!("#fn-{}", safe_id)), + ), + ( + "id".into(), + hast::PropertyValue::String(format!( + "fnref-{}{}", + safe_id, + if reuse_counter > 1 { + format!("-{}", reuse_counter) + } else { + "".into() + } + )), + ), + ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true)), + ( + "ariaDescribedBy".into(), + hast::PropertyValue::String("footnote-label".into()), + ), + ], + children: vec![hast::Node::Text(hast::Text { + value: (call_index + 1).to_string(), + position: None, + })], + position: None, + })], + position: footnote_reference.position.clone(), + })) +} + +/// [`Heading`][mdast::Heading]. +fn transform_heading(state: &mut State, node: &mdast::Node, heading: &mdast::Heading) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: format!("h{}", heading.depth), + properties: vec![], + children: all(state, node), + position: heading.position.clone(), + })) +} + +/// [`Image`][mdast::Image]. +fn transform_image(_state: &mut State, _node: &mdast::Node, image: &mdast::Image) -> Result { + let mut properties = vec![]; + + properties.push(( + "src".into(), + hast::PropertyValue::String(sanitize(&image.url)), + )); + + properties.push(("alt".into(), hast::PropertyValue::String(image.alt.clone()))); + + if let Some(value) = image.title.as_ref() { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties, + children: vec![], + position: image.position.clone(), + })) +} + +/// [`ImageReference`][mdast::ImageReference]. +fn transform_image_reference( + state: &mut State, + _node: &mdast::Node, + image_reference: &mdast::ImageReference, +) -> Result { + let mut properties = vec![]; + + let definition = state + .definitions + .iter() + .find(|d| d.0 == image_reference.identifier); + + let (_, url, title) = + definition.expect("expected reference to have a corresponding definition"); + + properties.push(("src".into(), hast::PropertyValue::String(sanitize(url)))); + + properties.push(( + "alt".into(), + hast::PropertyValue::String(image_reference.alt.clone()), + )); + + if let Some(value) = title { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "img".into(), + properties, + children: vec![], + position: image_reference.position.clone(), + })) +} + +/// [`InlineCode`][mdast::InlineCode]. +fn transform_inline_code( + _state: &mut State, + _node: &mdast::Node, + inline_code: &mdast::InlineCode, +) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![], + children: vec![hast::Node::Text(hast::Text { + value: replace_eols_with_spaces(&inline_code.value), + position: None, + })], + position: inline_code.position.clone(), + })) +} + +/// [`InlineMath`][mdast::InlineMath]. +fn transform_inline_math( + _state: &mut State, + _node: &mdast::Node, + inline_math: &mdast::InlineMath, +) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["language-math".into(), "math-inline".into()]), + )], + children: vec![hast::Node::Text(hast::Text { + value: replace_eols_with_spaces(&inline_math.value), + position: None, + })], + position: inline_math.position.clone(), + })) +} + +/// [`Link`][mdast::Link]. +fn transform_link(state: &mut State, node: &mdast::Node, link: &mdast::Link) -> Result { + let mut properties = vec![]; + + properties.push(( + "href".into(), + hast::PropertyValue::String(sanitize(&link.url)), + )); + + if let Some(value) = link.title.as_ref() { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties, + children: all(state, node), + position: link.position.clone(), + })) +} + +/// [`LinkReference`][mdast::LinkReference]. +fn transform_link_reference( + state: &mut State, + node: &mdast::Node, + link_reference: &mdast::LinkReference, +) -> Result { + let mut properties = vec![]; + + let definition = state + .definitions + .iter() + .find(|d| d.0 == link_reference.identifier); + + let (_, url, title) = + definition.expect("expected reference to have a corresponding definition"); + + properties.push(("href".into(), hast::PropertyValue::String(sanitize(url)))); + + if let Some(value) = title { + properties.push(("title".into(), hast::PropertyValue::String(value.into()))); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "a".into(), + properties, + children: all(state, node), + position: link_reference.position.clone(), + })) +} + +/// [`ListItem`][mdast::ListItem]. +fn transform_list_item( + state: &mut State, + node: &mdast::Node, + parent: Option<&mdast::Node>, + list_item: &mdast::ListItem, +) -> Result { + let mut children = all(state, node); + let mut loose = list_item_loose(node); + + if let Some(parent) = parent { + if matches!(parent, mdast::Node::List(_)) { + loose = list_loose(parent); + } + }; + + let mut properties = vec![]; + + // Inject a checkbox. + if let Some(checked) = list_item.checked { + // According to github-markdown-css, this class hides bullet. + // See: . + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]), + )); + + let mut input = Some(hast::Node::Element(hast::Element { + tag_name: "input".into(), + properties: vec![ + ( + "type".into(), + hast::PropertyValue::String("checkbox".into()), + ), + ("checked".into(), hast::PropertyValue::Boolean(checked)), + ("disabled".into(), hast::PropertyValue::Boolean(true)), + ], + children: vec![], + position: None, + })); + + if let Some(hast::Node::Element(x)) = children.first_mut() { + if x.tag_name == "p" { + if !x.children.is_empty() { + x.children.insert( + 0, + hast::Node::Text(hast::Text { + value: " ".into(), + position: None, + }), + ); + } + + x.children.insert(0, input.take().unwrap()); + } + } + + // If the input wasn‘t injected yet, inject a paragraph. + if let Some(input) = input { + children.insert( + 0, + hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: vec![input], + position: None, + }), + ); + } + } + + children.reverse(); + let mut result = vec![]; + let mut head = true; + let empty = children.is_empty(); + let mut tail_p = false; + + while let Some(child) = children.pop() { + let mut is_p = false; + if let hast::Node::Element(el) = &child { + if el.tag_name == "p" { + is_p = true; + } + } + + // Add eols before nodes, except if this is a tight, first paragraph. + if loose || !head || !is_p { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + if is_p && !loose { + // Unwrap the paragraph. + if let hast::Node::Element(mut el) = child { + result.append(&mut el.children); + } + } else { + result.push(child); + } + + head = false; + tail_p = is_p; + } + + // Add eol after last node, except if it is tight or a paragraph. + if !empty && (loose || !tail_p) { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "li".into(), + properties, + children: result, + position: list_item.position.clone(), + })) +} + +/// [`List`][mdast::List]. +fn transform_list(state: &mut State, node: &mdast::Node, list: &mdast::List) -> Result { + let mut contains_task_list = false; + let mut index = 0; + + while index < list.children.len() { + if let mdast::Node::ListItem(item) = &list.children[index] { + if item.checked.is_some() { + contains_task_list = true; + } + } + + index += 1; + } + + let mut properties = vec![]; + + // Add start. + if let Some(start) = list.start { + if list.ordered && start != 1 { + properties.push(( + "start".into(), + hast::PropertyValue::String(start.to_string()), + )); + } + } + + // Like GitHub, add a class for custom styling. + if contains_task_list { + properties.push(( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]), + )); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: if list.ordered { + "ol".into() + } else { + "ul".into() + }, + properties, + children: wrap(all(state, node), true), + position: list.position.clone(), + })) +} + +/// [`Math`][mdast::Math]. +fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) -> Result { + let mut value = math.value.clone(); + value.push('\n'); + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "pre".into(), + properties: vec![], + children: vec![hast::Node::Element(hast::Element { + tag_name: "code".into(), + properties: vec![( + "className".into(), + hast::PropertyValue::SpaceSeparated(vec![ + "language-math".into(), + "math-display".into(), + ]), + )], + children: vec![hast::Node::Text(hast::Text { + value, + position: None, + })], + position: math.position.clone(), + })], + position: math.position.clone(), + })) +} + +/// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. +fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { + match node { + mdast::Node::MdxFlowExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + mdast::Node::MdxTextExpression(node) => { + Result::Node(hast::Node::MdxExpression(hast::MdxExpression { + value: node.value.clone(), + position: node.position.clone(), + stops: node.stops.clone(), + })) + } + _ => unreachable!("expected expression"), + } +} + +/// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. +fn transform_mdx_jsx_element(state: &mut State, node: &mdast::Node) -> Result { + let (name, attributes) = match node { + mdast::Node::MdxJsxFlowElement(n) => (&n.name, &n.attributes), + mdast::Node::MdxJsxTextElement(n) => (&n.name, &n.attributes), + _ => unreachable!("expected mdx jsx element"), + }; + + Result::Node(hast::Node::MdxJsxElement(hast::MdxJsxElement { + name: name.clone(), + attributes: attributes.clone(), + children: all(state, node), + position: node.position().cloned(), + })) +} + +/// [`MdxjsEsm`][mdast::MdxjsEsm]. +fn transform_mdxjs_esm( + _state: &mut State, + _node: &mdast::Node, + mdxjs_esm: &mdast::MdxjsEsm, +) -> Result { + Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { + value: mdxjs_esm.value.clone(), + position: mdxjs_esm.position.clone(), + stops: mdxjs_esm.stops.clone(), + })) +} + +/// [`Paragraph`][mdast::Paragraph]. +fn transform_paragraph( + state: &mut State, + node: &mdast::Node, + paragraph: &mdast::Paragraph, +) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "p".into(), + properties: vec![], + children: all(state, node), + position: paragraph.position.clone(), + })) +} + +/// [`Root`][mdast::Root]. +fn transform_root(state: &mut State, node: &mdast::Node, root: &mdast::Root) -> Result { + Result::Node(hast::Node::Root(hast::Root { + children: wrap(all(state, node), false), + position: root.position.clone(), + })) +} + +/// [`Strong`][mdast::Strong]. +fn transform_strong(state: &mut State, node: &mdast::Node, strong: &mdast::Strong) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "strong".into(), + properties: vec![], + children: all(state, node), + position: strong.position.clone(), + })) +} + +/// [`TableCell`][mdast::TableCell]. +fn transform_table_cell( + state: &mut State, + node: &mdast::Node, + head: bool, + align: mdast::AlignKind, + table_cell: &mdast::TableCell, +) -> Result { + let align_value = match align { + mdast::AlignKind::None => None, + mdast::AlignKind::Left => Some("left"), + mdast::AlignKind::Right => Some("right"), + mdast::AlignKind::Center => Some("center"), + }; + + let mut properties = vec![]; + + if let Some(value) = align_value { + properties.push(("align".into(), hast::PropertyValue::String(value.into()))); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: if head { "th".into() } else { "td".into() }, + properties, + children: all(state, node), + position: table_cell.position.clone(), + })) +} + +/// [`TableRow`][mdast::TableRow]. +fn transform_table_row( + state: &mut State, + _node: &mdast::Node, + head: bool, + align: Option<&[mdast::AlignKind]>, + table_row: &mdast::TableRow, +) -> Result { + let mut children = vec![]; + let mut index = 0; + #[allow(clippy::redundant_closure_for_method_calls)] + let len = align.map_or(table_row.children.len(), |d| d.len()); + let empty_cell = mdast::Node::TableCell(mdast::TableCell { + children: vec![], + position: None, + }); + + while index < len { + let align_value = align + .and_then(|d| d.get(index)) + .unwrap_or(&mdast::AlignKind::None); + + let child = table_row.children.get(index).unwrap_or(&empty_cell); + + let result = if let mdast::Node::TableCell(table_cell) = child { + transform_table_cell(state, child, head, *align_value, table_cell) + } else { + unreachable!("expected tale cell in table row") + }; + + append_result(&mut children, result); + index += 1; + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "tr".into(), + properties: vec![], + children: wrap(children, true), + position: table_row.position.clone(), + })) +} + +/// [`Table`][mdast::Table]. +fn transform_table(state: &mut State, _node: &mdast::Node, table: &mdast::Table) -> Result { + let mut rows = vec![]; + let mut index = 0; + + while index < table.children.len() { + let child = &table.children[index]; + let result = if let mdast::Node::TableRow(table_row) = child { + transform_table_row( + state, + &table.children[index], + index == 0, + Some(&table.align), + table_row, + ) + } else { + unreachable!("expected table row as child of table") + }; + + append_result(&mut rows, result); + index += 1; + } + + let body_rows = rows.split_off(1); + let head_row = rows.pop(); + let mut children = vec![]; + + if let Some(row) = head_row { + let position = row.position().cloned(); + children.push(hast::Node::Element(hast::Element { + tag_name: "thead".into(), + properties: vec![], + children: wrap(vec![row], true), + position, + })); + } + + if !body_rows.is_empty() { + let mut position = None; + + if let Some(position_start) = body_rows.first().and_then(hast::Node::position) { + if let Some(position_end) = body_rows.last().and_then(hast::Node::position) { + position = Some(Position { + start: position_start.start.clone(), + end: position_end.end.clone(), + }); + } + } + + children.push(hast::Node::Element(hast::Element { + tag_name: "tbody".into(), + properties: vec![], + children: wrap(body_rows, true), + position, + })); + } + + Result::Node(hast::Node::Element(hast::Element { + tag_name: "table".into(), + properties: vec![], + children: wrap(children, true), + position: table.position.clone(), + })) +} + +/// [`Text`][mdast::Text]. +fn transform_text(_state: &mut State, _node: &mdast::Node, text: &mdast::Text) -> Result { + Result::Node(hast::Node::Text(hast::Text { + value: text.value.clone(), + position: text.position.clone(), + })) +} + +/// [`ThematicBreak`][mdast::ThematicBreak]. +fn transform_thematic_break( + _state: &mut State, + _node: &mdast::Node, + thematic_break: &mdast::ThematicBreak, +) -> Result { + Result::Node(hast::Node::Element(hast::Element { + tag_name: "hr".into(), + properties: vec![], + children: vec![], + position: thematic_break.position.clone(), + })) +} + +// Transform children of `parent`. +fn all(state: &mut State, parent: &mdast::Node) -> Vec { + let mut nodes = vec![]; + if let Some(children) = parent.children() { + let mut index = 0; + while index < children.len() { + let child = &children[index]; + let result = one(state, child, Some(parent)); + append_result(&mut nodes, result); + index += 1; + } + } + + nodes +} + +/// Wrap `nodes` with line feeds between each entry. +/// Optionally adds line feeds at the start and end. +fn wrap(mut nodes: Vec, loose: bool) -> Vec { + let mut result = vec![]; + let was_empty = nodes.is_empty(); + let mut head = true; + + nodes.reverse(); + + if loose { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + while let Some(item) = nodes.pop() { + // Inject when there’s more: + if !head { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + head = false; + result.push(item); + } + + if loose && !was_empty { + result.push(hast::Node::Text(hast::Text { + value: "\n".into(), + position: None, + })); + } + + result +} + +/// Visit. +fn visit(node: &mdast::Node, visitor: Visitor) +where + Visitor: FnMut(&mdast::Node), +{ + visit_impl(node, visitor); +} + +/// Visit, mutably. +// Probably useful later: +#[allow(dead_code)] +fn visit_mut(node: &mut mdast::Node, visitor: Visitor) +where + Visitor: FnMut(&mut mdast::Node), +{ + visit_mut_impl(node, visitor); +} + +/// Internal implementation to visit. +fn visit_impl(node: &mdast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mdast::Node), +{ + visitor(node); + + if let Some(children) = node.children() { + let mut index = 0; + while index < children.len() { + let child = &children[index]; + visitor = visit_impl(child, visitor); + index += 1; + } + } + + visitor +} + +/// Internal implementation to visit, mutably. +fn visit_mut_impl(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mut mdast::Node), +{ + visitor(node); + + if let Some(children) = node.children_mut() { + let mut index = 0; + while let Some(child) = children.get_mut(index) { + visitor = visit_mut_impl(child, visitor); + index += 1; + } + } + + visitor +} + +// To do: trim arounds breaks: . +/// Append an (optional, variadic) result. +fn append_result(list: &mut Vec, result: Result) { + match result { + Result::Fragment(mut fragment) => list.append(&mut fragment), + Result::Node(node) => list.push(node), + Result::None => {} + }; +} + +/// Replace line endings (CR, LF, CRLF) with spaces. +/// +/// Used for inline code and inline math. +fn replace_eols_with_spaces(value: &str) -> String { + // It’ll grow a bit small for each CR+LF. + let mut result = String::with_capacity(value.len()); + let bytes = value.as_bytes(); + let mut index = 0; + let mut start = 0; + + while index < bytes.len() { + let byte = bytes[index]; + + if byte == b'\r' || byte == b'\n' { + result.push_str(&value[start..index]); + result.push(' '); + + if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' { + index += 1; + } + + start = index + 1; + } + + index += 1; + } + + result.push_str(&value[start..]); + + result +} + +/// Check if a list is loose. +fn list_loose(node: &mdast::Node) -> bool { + if let mdast::Node::List(list) = node { + if list.spread { + return true; + } + + if let Some(children) = node.children() { + let mut index = 0; + while index < children.len() { + if list_item_loose(&children[index]) { + return true; + } + index += 1; + } + } + } + + false +} + +/// Check if a list item is loose. +fn list_item_loose(node: &mdast::Node) -> bool { + if let mdast::Node::ListItem(item) = node { + item.spread + } else { + false + } +} diff --git a/tests/test_utils/mdx_plugin_recma_document.rs b/tests/test_utils/mdx_plugin_recma_document.rs new file mode 100644 index 0000000..a62862c --- /dev/null +++ b/tests/test_utils/mdx_plugin_recma_document.rs @@ -0,0 +1,663 @@ +//! Turn a JavaScript AST, coming from MD(X), into a component. +//! +//! Port of , +//! by the same author. + +extern crate swc_ecma_ast; +use crate::test_utils::{ + hast_util_to_swc::Program, + swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, +}; +use micromark::{ + unist::{Point, Position}, + Location, +}; + +/// JSX runtimes. +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum JsxRuntime { + /// Automatic runtime. + /// + /// With the automatic runtime, some module is expected to exist somewhere. + /// That modules is expected to expose a certain API. + /// The compiler adds an import of that module and compiles JSX away to + /// function calls that use that API. + #[default] + Automatic, + /// Classic runtime. + /// + /// With the classic runtime, you define two values yourself in each file, + /// which are expected to work a certain way. + /// The compiler compiles JSX away to function calls using those two values. + Classic, +} + +/// Configuration. +#[derive(Debug, PartialEq, Eq)] +pub struct Options { + /// Pragma for JSX (used in classic runtime). + /// + /// Default: `React.createElement`. + pub pragma: Option, + /// Pragma for JSX fragments (used in classic runtime). + /// + /// Default: `React.Fragment`. + pub pragma_frag: Option, + /// Where to import the identifier of `pragma` from (used in classic runtime). + /// + /// Default: `react`. + pub pragma_import_source: Option, + /// Place to import automatic JSX runtimes from (used in automatic runtime). + /// + /// Default: `react`. + pub jsx_import_source: Option, + /// JSX runtime to use. + /// + /// Default: `automatic`. + pub jsx_runtime: Option, +} + +impl Default for Options { + /// Use the automatic JSX runtime with React. + fn default() -> Self { + Self { + pragma: None, + pragma_frag: None, + pragma_import_source: None, + jsx_import_source: None, + jsx_runtime: Some(JsxRuntime::default()), + } + } +} + +#[allow(dead_code)] +pub fn mdx_plugin_recma_document( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Result { + // New body children. + let mut replacements = vec![]; + + // Inject JSX configuration comment. + if let Some(runtime) = &options.jsx_runtime { + let mut pragmas = vec![]; + let react = &"react".into(); + let create_element = &"React.createElement".into(); + let fragment = &"React.Fragment".into(); + + if *runtime == JsxRuntime::Automatic { + pragmas.push("@jsxRuntime automatic".into()); + pragmas.push(format!( + "@jsxImportSource {}", + if let Some(jsx_import_source) = &options.jsx_import_source { + jsx_import_source + } else { + react + } + )); + } else { + pragmas.push("@jsxRuntime classic".into()); + pragmas.push(format!( + "@jsx {}", + if let Some(pragma) = &options.pragma { + pragma + } else { + create_element + } + )); + pragmas.push(format!( + "@jsxFrag {}", + if let Some(pragma_frag) = &options.pragma_frag { + pragma_frag + } else { + fragment + } + )); + } + + if !pragmas.is_empty() { + program.comments.insert( + 0, + swc_common::comments::Comment { + kind: swc_common::comments::CommentKind::Block, + text: pragmas.join(" ").into(), + span: swc_common::DUMMY_SP, + }, + ); + } + } + + // Inject an import in the classic runtime for the pragma (and presumably, + // fragment). + if options.jsx_runtime == Some(JsxRuntime::Classic) { + let pragma = if let Some(pragma) = &options.pragma { + pragma + } else { + "React" + }; + let sym = pragma.split('.').next().expect("first item always exists"); + + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { + specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( + swc_ecma_ast::ImportNamedSpecifier { + local: swc_ecma_ast::Ident { + sym: sym.into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + imported: None, + span: swc_common::DUMMY_SP, + is_type_only: false, + }, + )], + src: Box::new(swc_ecma_ast::Str { + value: (if let Some(source) = &options.pragma_import_source { + source.clone() + } else { + "react".into() + }) + .into(), + span: swc_common::DUMMY_SP, + raw: None, + }), + type_only: false, + asserts: None, + span: swc_common::DUMMY_SP, + }), + )); + } + + // Find the `export default`, the JSX expression, and leave the rest as it + // is. + let mut input = program.module.body.split_off(0); + input.reverse(); + let mut layout = false; + let mut layout_position = None; + let content = true; + + while let Some(module_item) = input.pop() { + match module_item { + // ```js + // export default props => <>{props.children} + // ``` + // + // Treat it as an inline layout declaration. + // + // In estree, the below two are the same node (`ExportDefault`). + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( + decl, + )) => { + if layout { + return Err(create_double_layout_message( + bytepos_to_point(&decl.span.lo, location).as_ref(), + layout_position.as_ref(), + )); + } + + layout = true; + layout_position = span_to_position(&decl.span, location); + match decl.decl { + swc_ecma_ast::DefaultDecl::Class(cls) => { + replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) + } + swc_ecma_ast::DefaultDecl::Fn(func) => { + replacements.push(create_layout_decl(swc_ecma_ast::Expr::Fn(func))) + } + swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { + return Err( + prefix_error_with_point( + "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), + bytepos_to_point(&decl.span.lo, location).as_ref() + ) + ); + } + } + } + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( + expr, + )) => { + if layout { + return Err(create_double_layout_message( + bytepos_to_point(&expr.span.lo, location).as_ref(), + layout_position.as_ref(), + )); + } + + layout = true; + layout_position = span_to_position(&expr.span, location); + replacements.push(create_layout_decl(*expr.expr)); + } + // ```js + // export {a, b as c} from 'd' + // export {a, b as c} + // ``` + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed( + mut named_export, + )) => { + // SWC is currently crashing when generating code, w/o source + // map, if an actual location is set on this node. + named_export.span = swc_common::DUMMY_SP; + + let mut index = 0; + let mut id = None; + + while index < named_export.specifiers.len() { + let mut take = false; + // Note: the `swc_ecma_ast::ExportSpecifier::Default` + // branch of this looks interesting, but as far as I + // understand it *is not* valid ES. + // `export a from 'b'` is a syntax error, even in SWC. + if let swc_ecma_ast::ExportSpecifier::Named(named) = + &named_export.specifiers[index] + { + if let Some(swc_ecma_ast::ModuleExportName::Ident(ident)) = &named.exported + { + if ident.sym.as_ref() == "default" { + // For some reason the AST supports strings + // instead of identifiers. + // Looks like some TC39 proposal. Ignore for now + // and only do things if this is an ID. + if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { + if layout { + return Err(create_double_layout_message( + bytepos_to_point(&ident.span.lo, location).as_ref(), + layout_position.as_ref(), + )); + } + layout = true; + layout_position = span_to_position(&ident.span, location); + take = true; + id = Some(ident.clone()); + } + } + } + } + + if take { + named_export.specifiers.remove(index); + } else { + index += 1; + } + } + + if let Some(id) = id { + let source = named_export.src.clone(); + + // If there was just a default export, we can drop the original node. + if !named_export.specifiers.is_empty() { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::ExportNamed(named_export), + )); + } + + // It’s an `export {x} from 'y'`, so generate an import. + if let Some(source) = source { + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { + specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( + swc_ecma_ast::ImportNamedSpecifier { + local: swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + imported: Some(swc_ecma_ast::ModuleExportName::Ident(id)), + span: swc_common::DUMMY_SP, + is_type_only: false, + }, + )], + src: source, + type_only: false, + asserts: None, + span: swc_common::DUMMY_SP, + }), + )) + } + // It’s an `export {x}`, so generate a variable declaration. + else { + replacements.push(create_layout_decl(swc_ecma_ast::Expr::Ident(id))); + } + } else { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::ExportNamed(named_export), + )); + } + } + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(mut x)) => { + // SWC is currently crashing when generating code, w/o source + // map, if an actual location is set on this node. + x.span = swc_common::DUMMY_SP; + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::Import(x), + )); + } + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDecl(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportAll(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsImportEquals(_)) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsExportAssignment( + _, + )) + | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsNamespaceExport( + _, + )) => { + // Pass through. + replacements.push(module_item); + } + swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr(expr_stmt)) => { + match *expr_stmt.expr { + swc_ecma_ast::Expr::JSXElement(elem) => { + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXElement(elem)), + layout, + )); + } + swc_ecma_ast::Expr::JSXFragment(mut frag) => { + // Unwrap if possible. + if frag.children.len() == 1 { + let item = frag.children.pop().unwrap(); + + if let swc_ecma_ast::JSXElementChild::JSXElement(elem) = item { + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXElement(elem)), + layout, + )); + continue; + } + + frag.children.push(item) + } + + replacements.append(&mut create_mdx_content( + Some(swc_ecma_ast::Expr::JSXFragment(frag)), + layout, + )); + } + _ => { + // Pass through. + replacements.push(swc_ecma_ast::ModuleItem::Stmt( + swc_ecma_ast::Stmt::Expr(expr_stmt), + )); + } + } + } + swc_ecma_ast::ModuleItem::Stmt(stmt) => { + replacements.push(swc_ecma_ast::ModuleItem::Stmt(stmt)); + } + } + } + + // Generate an empty component. + if !content { + replacements.append(&mut create_mdx_content(None, layout)); + } + + // ```jsx + // export default MDXContent + // ``` + replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( + swc_ecma_ast::ModuleDecl::ExportDefaultExpr(swc_ecma_ast::ExportDefaultExpr { + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "MDXContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + }), + )); + + program.module.body = replacements; + + Ok(program) +} + +/// Create a content component. +fn create_mdx_content( + expr: Option, + has_internal_layout: bool, +) -> Vec { + // ```jsx + // xxx + // ``` + let mut result = swc_ecma_ast::Expr::JSXElement(Box::new(swc_ecma_ast::JSXElement { + opening: swc_ecma_ast::JSXOpeningElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( + swc_ecma_ast::SpreadElement { + dot3_token: swc_common::DUMMY_SP, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }, + )], + self_closing: false, + type_args: None, + span: swc_common::DUMMY_SP, + }, + closing: Some(swc_ecma_ast::JSXClosingElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + span: swc_common::DUMMY_SP, + }), + // ```jsx + // <_createMdxContent {...props} /> + // ``` + children: vec![swc_ecma_ast::JSXElementChild::JSXElement(Box::new( + swc_ecma_ast::JSXElement { + opening: swc_ecma_ast::JSXOpeningElement { + name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }), + attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( + swc_ecma_ast::SpreadElement { + dot3_token: swc_common::DUMMY_SP, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }, + )], + self_closing: true, + type_args: None, + span: swc_common::DUMMY_SP, + }, + closing: None, + children: vec![], + span: swc_common::DUMMY_SP, + }, + ))], + span: swc_common::DUMMY_SP, + })); + + if !has_internal_layout { + // ```jsx + // MDXLayout ? xxx : _createMdxContent(props) + // ``` + result = swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + cons: Box::new(result), + alt: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { + callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Ident( + swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + ))), + args: vec![swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + })), + }], + type_args: None, + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + }); + } + + // ```jsx + // function _createMdxContent(props) { + // return xxx + // } + // ``` + let create_mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( + swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { + ident: swc_ecma_ast::Ident { + sym: "_createMdxContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + declare: false, + function: Box::new(swc_ecma_ast::Function { + params: vec![swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }], + decorators: vec![], + body: Some(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { + arg: Some(Box::new(expr.unwrap_or({ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Null(swc_ecma_ast::Null { + span: swc_common::DUMMY_SP, + })) + }))), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + span: swc_common::DUMMY_SP, + }), + }), + )); + + // ```jsx + // function MDXContent(props = {}) { + // return xxx + // } + // ``` + let mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( + swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { + ident: swc_ecma_ast::Ident { + sym: "MDXContent".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + declare: false, + function: Box::new(swc_ecma_ast::Function { + params: vec![swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Assign(swc_ecma_ast::AssignPat { + left: Box::new(swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "props".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + })), + right: Box::new(swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { + props: vec![], + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }], + decorators: vec![], + body: Some(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { + arg: Some(Box::new(result)), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + span: swc_common::DUMMY_SP, + }), + }), + )); + + vec![create_mdx_content, mdx_content] +} + +/// Create a layout, inside the document. +fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { + // ```jsx + // const MDXLayout = xxx + // ``` + swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( + swc_ecma_ast::VarDecl { + kind: swc_ecma_ast::VarDeclKind::Const, + declare: false, + decls: vec![swc_ecma_ast::VarDeclarator { + name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: swc_ecma_ast::Ident { + sym: "MDXLayout".into(), + optional: false, + span: swc_common::DUMMY_SP, + }, + type_ann: None, + }), + init: Some(Box::new(expr)), + span: swc_common::DUMMY_SP, + definite: false, + }], + span: swc_common::DUMMY_SP, + }, + )))) +} + +/// Create an error message about multiple layouts. +fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { + prefix_error_with_point( + format!( + "Cannot specify multiple layouts{}", + if let Some(previous) = previous { + format!(" (previous: {:?})", previous) + } else { + "".into() + } + ), + at, + ) +} diff --git a/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs b/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs new file mode 100644 index 0000000..6a4d451 --- /dev/null +++ b/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs @@ -0,0 +1,1169 @@ +//! Rewrite JSX tags to accept them from props and an optional provider. +//! +//! Port of , +//! by the same author. + +extern crate swc_common; +extern crate swc_ecma_ast; +use crate::test_utils::{ + hast_util_to_swc::Program, + swc_utils::{ + create_binary_expression, create_ident, create_ident_expression, create_member_expression, + position_to_string, span_to_position, + }, +}; +use micromark::{id_cont, id_start, unist::Position, Location}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; + +/// Configuration. +#[derive(Debug, Default, Clone)] +pub struct Options { + /// Place to import a provider from. + /// + /// See [MDX provider](https://mdxjs.com/docs/using-mdx/#mdx-provider) + /// on the MDX website for more info. + pub provider_import_source: Option, + /// Whether to add extra information to error messages in generated code. + /// This is not yet supported. + pub development: bool, +} + +/// Rewrite JSX in an MDX file so that components can be passed in and provided. +#[allow(dead_code)] +pub fn mdx_plugin_recma_jsx_rewrite( + mut program: Program, + options: &Options, + location: Option<&Location>, +) -> Program { + let mut state = State { + scopes: vec![], + location, + provider: options.provider_import_source.is_some(), + path: program.path.clone(), + development: options.development, + create_provider_import: false, + create_error_helper: false, + }; + state.enter(Some(Info::default())); + program.module.visit_mut_with(&mut state); + + // If a provider is used (and can be used), import it. + if let Some(source) = &options.provider_import_source { + if state.create_provider_import { + program + .module + .body + .insert(0, create_import_provider(source)) + } + } + + // If potentially missing components are used, add the helper used for + // errors. + if state.create_error_helper { + program + .module + .body + .push(create_error_helper(state.development, state.path)); + } + + program +} + +/// Collection of different SWC functions. +#[derive(Debug)] +enum Func<'a> { + /// Function declaration. + Decl(&'a mut swc_ecma_ast::FnDecl), + /// Function expression. + Expr(&'a mut swc_ecma_ast::FnExpr), + /// Arrow function. + Arrow(&'a mut swc_ecma_ast::ArrowExpr), +} + +/// Info for a function scope. +#[derive(Debug, Default, Clone)] +struct Info { + /// Function name. + name: Option, + /// Used objects (`a` in ``). + objects: Vec, + /// Used components (``). + components: Vec, + /// Used literals (``). + tags: Vec, + /// List of JSX identifiers of literal tags that are not valid JS + /// identifiers in the shape of `Vec<(invalid, valid)>`. + /// + /// Example: + /// + /// ``` + /// vec![("a-b".into(), "_component0".into())] + /// ``` + aliases: Vec<(String, String)>, + /// Non-literal references in the shape of `Vec<(name, is_component)>`. + /// + /// Example: + /// + /// ``` + /// vec![("a".into(), false), ("a.b".into(), true)] + /// ``` + references: Vec<(String, bool, Option)>, +} + +/// Scope (block or function/global). +#[derive(Debug, Clone)] +struct Scope { + /// If this is a function (or global) scope, we track info. + info: Option, + /// Things that are defined in this scope. + defined: Vec, +} + +/// Context. +#[derive(Debug, Default, Clone)] +struct State<'a> { + location: Option<&'a Location>, + /// Path to file. + path: Option, + /// List of current scopes. + scopes: Vec, + /// Whether the user is in development mode. + development: bool, + /// Whether the user uses a provider. + provider: bool, + /// Whether a provider is referenced. + create_provider_import: bool, + /// Whether a missing component helper is referenced. + /// + /// When things are referenced that might not be defined, we reference a + /// helper function to throw when they are missing. + create_error_helper: bool, +} + +impl<'a> State<'a> { + /// Open a new scope. + fn enter(&mut self, info: Option) { + self.scopes.push(Scope { + info, + defined: vec![], + }); + } + + /// Close the current scope. + fn exit(&mut self) -> Scope { + self.scopes.pop().expect("expected scope") + } + + /// Close a function. + fn exit_func(&mut self, func: Func) { + let mut scope = self.exit(); + let mut defaults = vec![]; + let mut info = scope.info.take().unwrap(); + let mut index = 0; + + // Create defaults for tags. + // + // ```jsx + // {h1: 'h1'} + // ``` + while index < info.tags.len() { + let name = &info.tags[index]; + + defaults.push(swc_ecma_ast::PropOrSpread::Prop(Box::new( + swc_ecma_ast::Prop::KeyValue(swc_ecma_ast::KeyValueProp { + key: if is_identifier_name(name) { + swc_ecma_ast::PropName::Ident(create_ident(name)) + } else { + swc_ecma_ast::PropName::Str(swc_ecma_ast::Str { + value: name.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }) + }, + value: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: name.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }), + ))); + + index += 1; + } + + let mut actual = info.components.split_off(0); + let mut index = 0; + + // In some cases, a component is used directly (``) but it’s also + // used as an object (``). + while index < info.objects.len() { + if !actual.contains(&info.objects[index]) { + actual.push(info.objects[index].clone()); + } + index += 1; + } + + let mut statements = vec![]; + + if !defaults.is_empty() || !actual.is_empty() || !info.aliases.is_empty() { + let mut parameters = vec![]; + + // Use a provider, if configured. + // + // ```jsx + // _provideComponents() + // ``` + if self.provider { + self.create_provider_import = true; + parameters.push(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { + callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( + "_provideComponents", + ))), + args: vec![], + type_args: None, + span: swc_common::DUMMY_SP, + })); + } + + // Accept `components` as a prop if this is the `MDXContent` or + // `_createMdxContent` function. + // + // ```jsx + // props.components + // ``` + if is_props_receiving_fn(&info.name) { + parameters.push(swc_ecma_ast::Expr::Member(swc_ecma_ast::MemberExpr { + obj: Box::new(create_ident_expression("props")), + prop: swc_ecma_ast::MemberProp::Ident(create_ident("components")), + span: swc_common::DUMMY_SP, + })); + } + + // Inject an object at the start, when: + // - there are defaults, + // - there are two sources + // + // ```jsx + // (_provideComponents(), props.components) + // () + // ``` + // + // To: + // + // ```jsx + // ({}, _provideComponents(), props.components) + // ({h1: 'h1'}) + // ``` + if !defaults.is_empty() || parameters.len() > 1 { + parameters.insert( + 0, + swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { + props: defaults, + span: swc_common::DUMMY_SP, + }), + ); + } + + // Merge things and prevent errors. + // + // ```jsx + // {}, _provideComponents(), props.components + // props.components + // _provideComponents() + // ``` + // + // To: + // + // ```jsx + // Object.assign({}, _provideComponents(), props.components) + // props.components || {} + // _provideComponents() + // ``` + let mut components_init = if parameters.len() > 1 { + let mut args = vec![]; + parameters.reverse(); + while let Some(param) = parameters.pop() { + args.push(swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(param), + }); + } + swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { + callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Member( + swc_ecma_ast::MemberExpr { + obj: Box::new(create_ident_expression("Object")), + prop: swc_ecma_ast::MemberProp::Ident(create_ident("assign")), + span: swc_common::DUMMY_SP, + }, + ))), + args, + type_args: None, + span: swc_common::DUMMY_SP, + }) + } else { + // Always one. + let param = parameters.pop().unwrap(); + + if let swc_ecma_ast::Expr::Member(_) = param { + create_binary_expression( + vec![ + param, + swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { + props: vec![], + span: swc_common::DUMMY_SP, + }), + ], + swc_ecma_ast::BinaryOp::LogicalOr, + ) + } else { + param + } + }; + + // Add components to scope. + // + // For `['MyComponent', 'MDXLayout']` this generates: + // + // ```js + // const {MyComponent, wrapper: MDXLayout} = _components + // ``` + // + // Note that MDXLayout is special as it’s taken from + // `_components.wrapper`. + let components_pattern = if actual.is_empty() { + None + } else { + let mut props = vec![]; + actual.reverse(); + while let Some(key) = actual.pop() { + // `wrapper: MDXLayout` + if key == "MDXLayout" { + props.push(swc_ecma_ast::ObjectPatProp::KeyValue( + swc_ecma_ast::KeyValuePatProp { + key: swc_ecma_ast::PropName::Ident(create_ident("wrapper")), + value: Box::new(swc_ecma_ast::Pat::Ident( + swc_ecma_ast::BindingIdent { + id: create_ident(&key), + type_ann: None, + }, + )), + }, + )) + } + // `MyComponent` + else { + props.push(swc_ecma_ast::ObjectPatProp::Assign( + swc_ecma_ast::AssignPatProp { + key: create_ident(&key), + value: None, + span: swc_common::DUMMY_SP, + }, + )) + } + } + + Some(swc_ecma_ast::ObjectPat { + props, + optional: false, + span: swc_common::DUMMY_SP, + type_ann: None, + }) + }; + + let mut declarators = vec![]; + + // If there are tags, they take them from `_components`, so we need + // to make it defined. + if !info.tags.is_empty() { + declarators.push(swc_ecma_ast::VarDeclarator { + span: swc_common::DUMMY_SP, + name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("_components"), + type_ann: None, + }), + init: Some(Box::new(components_init)), + definite: false, + }); + components_init = create_ident_expression("_components"); + } + + // For JSX IDs that can’t be represented as JavaScript IDs (as in, + // those with dashes, such as `custom-element`), we generated a + // separate variable that is a valid JS ID (such as `_component0`), + // and here we take it from components: + // ```js + // const _component0 = _components['custom-element'] + // ``` + if !info.aliases.is_empty() { + info.aliases.reverse(); + + while let Some((id, name)) = info.aliases.pop() { + declarators.push(swc_ecma_ast::VarDeclarator { + span: swc_common::DUMMY_SP, + name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident(&name), + type_ann: None, + }), + init: Some(Box::new(swc_ecma_ast::Expr::Member( + swc_ecma_ast::MemberExpr { + obj: Box::new(create_ident_expression("_components")), + prop: swc_ecma_ast::MemberProp::Computed( + swc_ecma_ast::ComputedPropName { + expr: Box::new(swc_ecma_ast::Expr::Lit( + swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: id.into(), + span: swc_common::DUMMY_SP, + raw: None, + }), + )), + span: swc_common::DUMMY_SP, + }, + ), + span: swc_common::DUMMY_SP, + }, + ))), + definite: false, + }); + } + } + + if let Some(pat) = components_pattern { + declarators.push(swc_ecma_ast::VarDeclarator { + name: swc_ecma_ast::Pat::Object(pat), + init: Some(Box::new(components_init)), + span: swc_common::DUMMY_SP, + definite: false, + }); + } + + // Add the variable declaration. + statements.push(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( + swc_ecma_ast::VarDecl { + kind: swc_ecma_ast::VarDeclKind::Const, + decls: declarators, + span: swc_common::DUMMY_SP, + declare: false, + }, + )))); + } + + // Add checks at runtime to verify that object/components are passed. + // + // ```js + // if (!a) _missingMdxReference("a", false); + // if (!a.b) _missingMdxReference("a.b", true); + // ``` + for (id, component, position) in info.references { + self.create_error_helper = true; + + let mut args = vec![ + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: id.clone().into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }, + swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Bool( + swc_ecma_ast::Bool { + value: component, + span: swc_common::DUMMY_SP, + }, + ))), + }, + ]; + + // Add the source location if it exists and if `development` is on. + if let Some(position) = position.as_ref() { + if self.development { + args.push(swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: position_to_string(position).into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + }) + } + } + + statements.push(swc_ecma_ast::Stmt::If(swc_ecma_ast::IfStmt { + test: Box::new(swc_ecma_ast::Expr::Unary(swc_ecma_ast::UnaryExpr { + op: swc_ecma_ast::UnaryOp::Bang, + arg: Box::new(create_member_expression(&id)), + span: swc_common::DUMMY_SP, + })), + cons: Box::new(swc_ecma_ast::Stmt::Expr(swc_ecma_ast::ExprStmt { + span: swc_common::DUMMY_SP, + expr: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { + callee: swc_ecma_ast::Callee::Expr(Box::new(create_ident_expression( + "_missingMdxReference", + ))), + args, + type_args: None, + span: swc_common::DUMMY_SP, + })), + })), + alt: None, + span: swc_common::DUMMY_SP, + })); + } + + // Add statements to functions. + if !statements.is_empty() { + let mut body: &mut swc_ecma_ast::BlockStmt = match func { + Func::Expr(expr) => { + if expr.function.body.is_none() { + expr.function.body = Some(swc_ecma_ast::BlockStmt { + stmts: vec![], + span: swc_common::DUMMY_SP, + }); + } + expr.function.body.as_mut().unwrap() + } + Func::Decl(decl) => { + if decl.function.body.is_none() { + decl.function.body = Some(swc_ecma_ast::BlockStmt { + stmts: vec![], + span: swc_common::DUMMY_SP, + }); + } + decl.function.body.as_mut().unwrap() + } + Func::Arrow(arr) => { + if let swc_ecma_ast::BlockStmtOrExpr::Expr(expr) = &mut arr.body { + arr.body = + swc_ecma_ast::BlockStmtOrExpr::BlockStmt(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { + // To do: figure out non-clone. + arg: Some(expr.clone()), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }); + } + arr.body.as_mut_block_stmt().unwrap() + } + }; + + statements.append(&mut body.stmts.split_off(0)); + body.stmts = statements; + } + } + + /// Get the current function scope. + fn current_fn_scope_mut(&mut self) -> &mut Scope { + let mut index = self.scopes.len(); + + while index > 0 { + index -= 1; + if self.scopes[index].info.is_some() { + return &mut self.scopes[index]; + } + } + + unreachable!("expected scope") + } + + /// Get the current scope. + fn current_scope_mut(&mut self) -> &mut Scope { + self.scopes.last_mut().expect("expected scope") + } + + /// Get the top-level scope’s info. + fn current_top_level_info(&self) -> Option<&Info> { + if let Some(scope) = self.scopes.get(1) { + scope.info.as_ref() + } else { + None + } + } + + /// Get the top-level scope’s info, mutably. + fn current_top_level_info_mut(&mut self) -> Option<&mut Info> { + if let Some(scope) = self.scopes.get_mut(1) { + scope.info.as_mut() + } else { + None + } + } + + /// Check if `id` is in scope. + fn in_scope(&self, id: &String) -> bool { + let mut index = self.scopes.len(); + + while index > 0 { + index -= 1; + if self.scopes[index].defined.contains(id) { + return true; + } + } + + false + } + + /// Add an identifier to a scope. + fn add_id(&mut self, id: String, block: bool) { + let scope = if block { + self.current_scope_mut() + } else { + self.current_fn_scope_mut() + }; + scope.defined.push(id); + } + + // Add a pattern to a scope. + fn add_pat(&mut self, pat: &swc_ecma_ast::Pat, block: bool) { + match pat { + // `x` + swc_ecma_ast::Pat::Ident(d) => self.add_id(d.id.sym.to_string(), block), + // `...x` + swc_ecma_ast::Pat::Array(d) => { + let mut index = 0; + while index < d.elems.len() { + if let Some(d) = &d.elems[index] { + self.add_pat(d, block); + } + index += 1; + } + } + // `...x` + swc_ecma_ast::Pat::Rest(d) => self.add_pat(&d.arg, block), + // `{x=y}` + swc_ecma_ast::Pat::Assign(d) => self.add_pat(&d.left, block), + swc_ecma_ast::Pat::Object(d) => { + let mut index = 0; + while index < d.props.len() { + match &d.props[index] { + // `{...x}` + swc_ecma_ast::ObjectPatProp::Rest(d) => { + self.add_pat(&d.arg, block); + } + // `{key: value}` + swc_ecma_ast::ObjectPatProp::KeyValue(d) => { + self.add_pat(&d.value, block); + } + // `{key}` or `{key = value}` + swc_ecma_ast::ObjectPatProp::Assign(d) => { + self.add_id(d.key.to_string(), block); + } + } + index += 1; + } + } + // Ignore `Invalid` / `Expr`. + _ => {} + } + } +} + +impl<'a> VisitMut for State<'a> { + noop_visit_mut_type!(); + + /// Rewrite JSX identifiers. + fn visit_mut_jsx_element(&mut self, node: &mut swc_ecma_ast::JSXElement) { + // If there is a top-level, non-global, scope which is a function. + if let Some(info) = self.current_top_level_info() { + // Rewrite only if we can rewrite. + if is_props_receiving_fn(&info.name) || self.provider { + let position = span_to_position(&node.span, self.location); + match &node.opening.name { + // ``, ``, ``. + swc_ecma_ast::JSXElementName::JSXMemberExpr(d) => { + let mut ids = vec![]; + let mut mem = d; + loop { + ids.push(mem.prop.sym.to_string()); + match &mem.obj { + swc_ecma_ast::JSXObject::Ident(d) => { + ids.push(d.sym.to_string()); + break; + } + swc_ecma_ast::JSXObject::JSXMemberExpr(d) => { + mem = d; + } + } + } + ids.reverse(); + let primary_id = ids.first().unwrap().clone(); + let in_scope = self.in_scope(&primary_id); + + if !in_scope { + let info_mut = self.current_top_level_info_mut().unwrap(); + + let mut index = 1; + while index <= ids.len() { + let full_id = ids[0..index].join("."); + let component = index == ids.len(); + if let Some(reference) = + info_mut.references.iter_mut().find(|d| d.0 == full_id) + { + if component { + reference.1 = true; + } + } else { + info_mut + .references + .push((full_id, component, position.clone())) + } + index += 1; + } + + if !info_mut.objects.contains(&primary_id) { + info_mut.objects.push(primary_id); + } + } + } + // ``, ``, `<$>`, `<_bar>`, ``. + swc_ecma_ast::JSXElementName::Ident(d) => { + // If the name is a valid ES identifier, and it doesn’t + // start with a lowercase letter, it’s a component. + // For example, `$foo`, `_bar`, `Baz` are all component + // names. + // But `foo` and `b-ar` are tag names. + let id = d.sym.to_string(); + + if is_literal_name(&id) { + // To do: ignore explicit JSX? + + let mut invalid = None; + + let name = if is_identifier_name(&id) { + swc_ecma_ast::JSXElementName::JSXMemberExpr( + swc_ecma_ast::JSXMemberExpr { + obj: swc_ecma_ast::JSXObject::Ident(create_ident( + "_components", + )), + prop: create_ident(&id), + }, + ) + } else { + let name = if let Some(invalid_ref) = + info.aliases.iter().find(|d| d.0 == id) + { + invalid_ref.1.clone() + } else { + let name = format!("_component{}", info.aliases.len()); + invalid = Some((id.clone(), name.clone())); + name + }; + + swc_ecma_ast::JSXElementName::Ident(create_ident(&name)) + }; + + let info_mut = self.current_top_level_info_mut().unwrap(); + + if !info_mut.tags.contains(&id) { + info_mut.tags.push(id); + } + + if let Some(invalid) = invalid { + info_mut.aliases.push(invalid) + } + + if let Some(closing) = node.closing.as_mut() { + closing.name = name.clone(); + } + + node.opening.name = name; + } else { + let mut is_layout = false; + + // The MDXLayout is wrapped in a + if let Some(name) = &info.name { + if name == "MDXContent" && id == "MDXLayout" { + is_layout = true; + } + } + + if !self.in_scope(&id) { + let info_mut = self.current_top_level_info_mut().unwrap(); + + if !is_layout { + if let Some(reference) = + info_mut.references.iter_mut().find(|d| d.0 == id) + { + reference.1 = true; + } else { + info_mut.references.push((id.clone(), true, position)) + } + } + + if !info_mut.components.contains(&id) { + info_mut.components.push(id); + } + } + } + } + // ``. + swc_ecma_ast::JSXElementName::JSXNamespacedName(_) => { + // Ignore. + } + } + } + } + + node.visit_mut_children_with(self); + } + + /// Add specifiers of import declarations. + fn visit_mut_import_decl(&mut self, node: &mut swc_ecma_ast::ImportDecl) { + let mut index = 0; + while index < node.specifiers.len() { + let ident = match &node.specifiers[index] { + swc_ecma_ast::ImportSpecifier::Default(x) => &x.local.sym, + swc_ecma_ast::ImportSpecifier::Namespace(x) => &x.local.sym, + swc_ecma_ast::ImportSpecifier::Named(x) => &x.local.sym, + }; + self.add_id(ident.to_string(), false); + index += 1; + } + + node.visit_mut_children_with(self); + } + + /// Add patterns of variable declarations. + fn visit_mut_var_decl(&mut self, node: &mut swc_ecma_ast::VarDecl) { + let block = node.kind != swc_ecma_ast::VarDeclKind::Var; + let mut index = 0; + while index < node.decls.len() { + self.add_pat(&node.decls[index].name, block); + index += 1; + } + node.visit_mut_children_with(self); + } + + /// Add identifier of class declaration. + fn visit_mut_class_decl(&mut self, node: &mut swc_ecma_ast::ClassDecl) { + self.add_id(node.ident.sym.to_string(), false); + node.visit_mut_children_with(self); + } + + /// On function declarations, add name, create scope, add parameters. + fn visit_mut_fn_decl(&mut self, node: &mut swc_ecma_ast::FnDecl) { + let id = node.ident.sym.to_string(); + self.add_id(id.clone(), false); + self.enter(Some(Info { + name: Some(id), + ..Default::default() + })); + let mut index = 0; + while index < node.function.params.len() { + self.add_pat(&node.function.params[index].pat, false); + index += 1; + } + node.visit_mut_children_with(self); + // Rewrite. + self.exit_func(Func::Decl(node)); + } + + /// On function expressions, add name, create scope, add parameters. + fn visit_mut_fn_expr(&mut self, node: &mut swc_ecma_ast::FnExpr) { + // Note: `periscopic` adds the ID to the newly generated scope, for + // fn expressions. + // That seems wrong? + let name = if let Some(ident) = &node.ident { + let id = ident.sym.to_string(); + self.add_id(id.clone(), false); + Some(id) + } else { + None + }; + + self.enter(Some(Info { + name, + ..Default::default() + })); + let mut index = 0; + while index < node.function.params.len() { + self.add_pat(&node.function.params[index].pat, false); + index += 1; + } + node.visit_mut_children_with(self); + self.exit_func(Func::Expr(node)); + } + + /// On arrow functions, create scope, add parameters. + fn visit_mut_arrow_expr(&mut self, node: &mut swc_ecma_ast::ArrowExpr) { + self.enter(Some(Info::default())); + let mut index = 0; + while index < node.params.len() { + self.add_pat(&node.params[index], false); + index += 1; + } + node.visit_mut_children_with(self); + self.exit_func(Func::Arrow(node)); + } + + // Blocks. + // Not sure why `periscopic` only does `For`/`ForIn`/`ForOf`/`Block`. + // I added `While`/`DoWhile` here just to be sure. + // But there are more. + /// On for statements, create scope. + fn visit_mut_for_stmt(&mut self, node: &mut swc_ecma_ast::ForStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + /// On for/in statements, create scope. + fn visit_mut_for_in_stmt(&mut self, node: &mut swc_ecma_ast::ForInStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + /// On for/of statements, create scope. + fn visit_mut_for_of_stmt(&mut self, node: &mut swc_ecma_ast::ForOfStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + /// On while statements, create scope. + fn visit_mut_while_stmt(&mut self, node: &mut swc_ecma_ast::WhileStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + /// On do/while statements, create scope. + fn visit_mut_do_while_stmt(&mut self, node: &mut swc_ecma_ast::DoWhileStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + /// On block statements, create scope. + fn visit_mut_block_stmt(&mut self, node: &mut swc_ecma_ast::BlockStmt) { + self.enter(None); + node.visit_mut_children_with(self); + self.exit(); + } + + /// On catch clauses, create scope, add param. + fn visit_mut_catch_clause(&mut self, node: &mut swc_ecma_ast::CatchClause) { + self.enter(None); + if let Some(pat) = &node.param { + self.add_pat(pat, true); + } + node.visit_mut_children_with(self); + self.exit(); + } +} + +/// Generate an import provider. +/// +/// ```js +/// import { useMDXComponents as _provideComponents } from "x" +/// ``` +fn create_import_provider(source: &str) -> swc_ecma_ast::ModuleItem { + swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import( + swc_ecma_ast::ImportDecl { + specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( + swc_ecma_ast::ImportNamedSpecifier { + local: create_ident("_provideComponents"), + imported: Some(swc_ecma_ast::ModuleExportName::Ident(create_ident( + "useMDXComponents", + ))), + span: swc_common::DUMMY_SP, + is_type_only: false, + }, + )], + src: Box::new(swc_ecma_ast::Str { + value: source.into(), + span: swc_common::DUMMY_SP, + raw: None, + }), + type_only: false, + asserts: None, + span: swc_common::DUMMY_SP, + }, + )) +} + +/// Generate an error helper. +/// +/// ```js +/// function _missingMdxReference(id, component) { +/// throw new Error("Expected " + (component ? "component" : "object") + " `" + id + "` to be defined: you likely forgot to import, pass, or provide it."); +/// } +/// ``` +fn create_error_helper(development: bool, path: Option) -> swc_ecma_ast::ModuleItem { + let mut parameters = vec![ + swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("id"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }, + swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("component"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }, + ]; + + // Accept a source location (which might be undefiend). + if development { + parameters.push(swc_ecma_ast::Param { + pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { + id: create_ident("place"), + type_ann: None, + }), + decorators: vec![], + span: swc_common::DUMMY_SP, + }) + } + + let mut message = vec![ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "Expected ".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + // `component ? "component" : "object"` + swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { + expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(create_ident_expression("component")), + cons: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "component".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "object".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + }), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: " `".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + create_ident_expression("id"), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "` to be defined: you likely forgot to import, pass, or provide it.".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + ]; + + // `place ? "\nIt’s referenced in your code at `" + place+ "`" : ""` + if development { + message.push(swc_ecma_ast::Expr::Paren(swc_ecma_ast::ParenExpr { + expr: Box::new(swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { + test: Box::new(create_ident_expression("place")), + cons: Box::new(create_binary_expression( + vec![ + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: "\nIt’s referenced in your code at `".into(), + span: swc_common::DUMMY_SP, + raw: None, + })), + create_ident_expression("place"), + swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str { + value: if let Some(path) = path { + format!("` in `{}`", path).into() + } else { + "`".into() + }, + span: swc_common::DUMMY_SP, + raw: None, + })), + ], + swc_ecma_ast::BinaryOp::Add, + )), + alt: Box::new(swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str( + swc_ecma_ast::Str { + value: "".into(), + span: swc_common::DUMMY_SP, + raw: None, + }, + ))), + span: swc_common::DUMMY_SP, + })), + span: swc_common::DUMMY_SP, + })) + } + + swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Fn( + swc_ecma_ast::FnDecl { + ident: create_ident("_missingMdxReference"), + declare: false, + function: Box::new(swc_ecma_ast::Function { + params: parameters, + decorators: vec![], + body: Some(swc_ecma_ast::BlockStmt { + stmts: vec![swc_ecma_ast::Stmt::Throw(swc_ecma_ast::ThrowStmt { + arg: Box::new(swc_ecma_ast::Expr::New(swc_ecma_ast::NewExpr { + callee: Box::new(create_ident_expression("Error")), + args: Some(vec![swc_ecma_ast::ExprOrSpread { + spread: None, + expr: Box::new(create_binary_expression( + message, + swc_ecma_ast::BinaryOp::Add, + )), + }]), + span: swc_common::DUMMY_SP, + type_args: None, + })), + span: swc_common::DUMMY_SP, + })], + span: swc_common::DUMMY_SP, + }), + is_generator: false, + is_async: false, + type_params: None, + return_type: None, + span: swc_common::DUMMY_SP, + }), + }, + ))) +} + +/// Check if this function is a props receiving component: it’s one of ours. +fn is_props_receiving_fn(name: &Option) -> bool { + if let Some(name) = name { + name == "_createMdxContent" || name == "MDXContent" + } else { + false + } +} + +/// Check if a name is a literal tag name or an identifier to a component. +fn is_literal_name(name: &str) -> bool { + matches!(name.as_bytes().first(), Some(b'a'..=b'z')) || !is_identifier_name(name) +} + +// Check if a name is a valid identifier name. +fn is_identifier_name(name: &str) -> bool { + for (index, char) in name.chars().enumerate() { + if if index == 0 { + !id_start(char) + } else { + !id_cont(char, false) + } { + return false; + } + } + + true +} diff --git a/tests/test_utils/micromark_swc_utils.rs b/tests/test_utils/micromark_swc_utils.rs deleted file mode 100644 index 13678d5..0000000 --- a/tests/test_utils/micromark_swc_utils.rs +++ /dev/null @@ -1,134 +0,0 @@ -extern crate swc_common; -use micromark::{ - mdast::Stop, - unist::{Point, Position}, - Location, -}; -use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; -use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; - -/// Turn a unist position, into an SWC span, of two byte positions. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn position_to_span(position: Option<&Position>) -> Span { - position.map_or(DUMMY_SP, |d| Span { - lo: point_to_bytepos(&d.start), - hi: point_to_bytepos(&d.end), - ctxt: SyntaxContext::empty(), - }) -} - -/// Turn an SWC span, of two byte positions, into a unist position. -/// -/// This assumes the span comes from a fixed tree, or is a dummy. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option { - let lo = span.lo.0 as usize; - let hi = span.hi.0 as usize; - - if lo > 0 && hi > 0 { - if let Some(location) = location { - if let Some(start) = location.to_point(lo - 1) { - if let Some(end) = location.to_point(hi - 1) { - return Some(Position { start, end }); - } - } - } - } - - None -} - -/// Turn a unist point into an SWC byte position. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn point_to_bytepos(point: &Point) -> BytePos { - BytePos(point.offset as u32 + 1) -} - -/// Turn an SWC byte position into a unist point. -/// -/// This assumes the byte position comes from a fixed tree, or is a dummy. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option { - let pos = bytepos.0 as usize; - - if pos > 0 { - if let Some(location) = location { - return location.to_point(pos - 1); - } - } - - None -} - -/// Prefix an error message with an optional point. -pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { - if let Some(point) = point { - format!("{}: {}", point_to_string(point), reason) - } else { - reason - } -} - -/// Serialize a unist position for humans. -pub fn position_to_string(position: &Position) -> String { - format!( - "{}-{}", - point_to_string(&position.start), - point_to_string(&position.end) - ) -} - -/// Serialize a unist point for humans. -pub fn point_to_string(point: &Point) -> String { - format!("{}:{}", point.line, point.column) -} - -/// Visitor to fix SWC byte positions. -/// -/// This assumes the byte position comes from an **unfixed** tree. -/// -/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they -/// > are missing or incremented by `1` when valid. -#[derive(Debug, Default, Clone)] -pub struct RewriteContext<'a> { - pub prefix_len: usize, - pub stops: &'a [Stop], - pub location: Option<&'a Location>, -} - -impl<'a> VisitMut for RewriteContext<'a> { - noop_visit_mut_type!(); - - // Rewrite spans. - fn visit_mut_span(&mut self, span: &mut Span) { - let mut result = DUMMY_SP; - let lo_rel = span.lo.0 as usize; - let hi_rel = span.hi.0 as usize; - - if lo_rel > self.prefix_len && hi_rel > self.prefix_len { - if let Some(lo_abs) = - Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) - { - if let Some(hi_abs) = - Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) - { - result = Span { - lo: BytePos(lo_abs as u32 + 1), - hi: BytePos(hi_abs as u32 + 1), - ctxt: SyntaxContext::empty(), - }; - } - } - } - - *span = result; - } -} diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs index 99ded2f..8d1f144 100644 --- a/tests/test_utils/mod.rs +++ b/tests/test_utils/mod.rs @@ -1,8 +1,7 @@ pub mod hast; -pub mod jsx_rewrite; -pub mod micromark_swc_utils; +pub mod hast_util_to_swc; +pub mod mdast_util_to_hast; +pub mod mdx_plugin_recma_document; +pub mod mdx_plugin_recma_jsx_rewrite; pub mod swc; pub mod swc_utils; -pub mod to_document; -pub mod to_hast; -pub mod to_swc; diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs index 7e44898..3c97d28 100644 --- a/tests/test_utils/swc.rs +++ b/tests/test_utils/swc.rs @@ -1,10 +1,9 @@ +//! Bridge between `micromark` and SWC. extern crate micromark; extern crate swc_common; extern crate swc_ecma_ast; extern crate swc_ecma_parser; -use crate::test_utils::micromark_swc_utils::{ - bytepos_to_point, prefix_error_with_point, RewriteContext, -}; +use crate::test_utils::swc_utils::{bytepos_to_point, prefix_error_with_point, RewriteContext}; use micromark::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal}; use swc_common::{ source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned, diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs index 1e1a526..5a45af6 100644 --- a/tests/test_utils/swc_utils.rs +++ b/tests/test_utils/swc_utils.rs @@ -1,8 +1,140 @@ -extern crate swc_common; -extern crate swc_ecma_ast; +//! Lots of helpers for dealing with SWC, particularly from unist. -use swc_common::DUMMY_SP; +use micromark::{ + mdast::Stop, + unist::{Point, Position}, + Location, +}; + +use swc_common::{BytePos, Span, SyntaxContext, DUMMY_SP}; use swc_ecma_ast::{BinExpr, BinaryOp, Expr, Ident, MemberExpr, MemberProp}; +use swc_ecma_visit::{noop_visit_mut_type, VisitMut}; + +/// Turn a unist position, into an SWC span, of two byte positions. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn position_to_span(position: Option<&Position>) -> Span { + position.map_or(DUMMY_SP, |d| Span { + lo: point_to_bytepos(&d.start), + hi: point_to_bytepos(&d.end), + ctxt: SyntaxContext::empty(), + }) +} + +/// Turn an SWC span, of two byte positions, into a unist position. +/// +/// This assumes the span comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn span_to_position(span: &Span, location: Option<&Location>) -> Option { + let lo = span.lo.0 as usize; + let hi = span.hi.0 as usize; + + if lo > 0 && hi > 0 { + if let Some(location) = location { + if let Some(start) = location.to_point(lo - 1) { + if let Some(end) = location.to_point(hi - 1) { + return Some(Position { start, end }); + } + } + } + } + + None +} + +/// Turn a unist point into an SWC byte position. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn point_to_bytepos(point: &Point) -> BytePos { + BytePos(point.offset as u32 + 1) +} + +/// Turn an SWC byte position into a unist point. +/// +/// This assumes the byte position comes from a fixed tree, or is a dummy. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +pub fn bytepos_to_point(bytepos: &BytePos, location: Option<&Location>) -> Option { + let pos = bytepos.0 as usize; + + if pos > 0 { + if let Some(location) = location { + return location.to_point(pos - 1); + } + } + + None +} + +/// Prefix an error message with an optional point. +pub fn prefix_error_with_point(reason: String, point: Option<&Point>) -> String { + if let Some(point) = point { + format!("{}: {}", point_to_string(point), reason) + } else { + reason + } +} + +/// Serialize a unist position for humans. +pub fn position_to_string(position: &Position) -> String { + format!( + "{}-{}", + point_to_string(&position.start), + point_to_string(&position.end) + ) +} + +/// Serialize a unist point for humans. +pub fn point_to_string(point: &Point) -> String { + format!("{}:{}", point.line, point.column) +} + +/// Visitor to fix SWC byte positions. +/// +/// This assumes the byte position comes from an **unfixed** tree. +/// +/// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they +/// > are missing or incremented by `1` when valid. +#[derive(Debug, Default, Clone)] +pub struct RewriteContext<'a> { + pub prefix_len: usize, + pub stops: &'a [Stop], + pub location: Option<&'a Location>, +} + +impl<'a> VisitMut for RewriteContext<'a> { + noop_visit_mut_type!(); + + // Rewrite spans. + fn visit_mut_span(&mut self, span: &mut Span) { + let mut result = DUMMY_SP; + let lo_rel = span.lo.0 as usize; + let hi_rel = span.hi.0 as usize; + + if lo_rel > self.prefix_len && hi_rel > self.prefix_len { + if let Some(lo_abs) = + Location::relative_to_absolute(self.stops, lo_rel - 1 - self.prefix_len) + { + if let Some(hi_abs) = + Location::relative_to_absolute(self.stops, hi_rel - 1 - self.prefix_len) + { + result = Span { + lo: BytePos(lo_abs as u32 + 1), + hi: BytePos(hi_abs as u32 + 1), + ctxt: SyntaxContext::empty(), + }; + } + } + } + + *span = result; + } +} /// Generate an ident. /// diff --git a/tests/test_utils/to_document.rs b/tests/test_utils/to_document.rs deleted file mode 100644 index 938df1b..0000000 --- a/tests/test_utils/to_document.rs +++ /dev/null @@ -1,658 +0,0 @@ -extern crate swc_ecma_ast; -use crate::test_utils::{ - micromark_swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position}, - to_swc::Program, -}; -use micromark::{ - unist::{Point, Position}, - Location, -}; - -/// JSX runtimes. -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub enum JsxRuntime { - /// Automatic runtime. - /// - /// With the automatic runtime, some module is expected to exist somewhere. - /// That modules is expected to expose a certain API. - /// The compiler adds an import of that module and compiles JSX away to - /// function calls that use that API. - #[default] - Automatic, - /// Classic runtime. - /// - /// With the classic runtime, you define two values yourself in each file, - /// which are expected to work a certain way. - /// The compiler compiles JSX away to function calls using those two values. - Classic, -} - -/// Configuration. -#[derive(Debug, PartialEq, Eq)] -pub struct Options { - /// Pragma for JSX (used in classic runtime). - /// - /// Default: `React.createElement`. - pub pragma: Option, - /// Pragma for JSX fragments (used in classic runtime). - /// - /// Default: `React.Fragment`. - pub pragma_frag: Option, - /// Where to import the identifier of `pragma` from (used in classic runtime). - /// - /// Default: `react`. - pub pragma_import_source: Option, - /// Place to import automatic JSX runtimes from (used in automatic runtime). - /// - /// Default: `react`. - pub jsx_import_source: Option, - /// JSX runtime to use. - /// - /// Default: `automatic`. - pub jsx_runtime: Option, -} - -impl Default for Options { - /// Use the automatic JSX runtime with React. - fn default() -> Self { - Self { - pragma: None, - pragma_frag: None, - pragma_import_source: None, - jsx_import_source: None, - jsx_runtime: Some(JsxRuntime::default()), - } - } -} - -#[allow(dead_code)] -pub fn to_document( - mut program: Program, - options: &Options, - location: Option<&Location>, -) -> Result { - // New body children. - let mut replacements = vec![]; - - // Inject JSX configuration comment. - if let Some(runtime) = &options.jsx_runtime { - let mut pragmas = vec![]; - let react = &"react".into(); - let create_element = &"React.createElement".into(); - let fragment = &"React.Fragment".into(); - - if *runtime == JsxRuntime::Automatic { - pragmas.push("@jsxRuntime automatic".into()); - pragmas.push(format!( - "@jsxImportSource {}", - if let Some(jsx_import_source) = &options.jsx_import_source { - jsx_import_source - } else { - react - } - )); - } else { - pragmas.push("@jsxRuntime classic".into()); - pragmas.push(format!( - "@jsx {}", - if let Some(pragma) = &options.pragma { - pragma - } else { - create_element - } - )); - pragmas.push(format!( - "@jsxFrag {}", - if let Some(pragma_frag) = &options.pragma_frag { - pragma_frag - } else { - fragment - } - )); - } - - if !pragmas.is_empty() { - program.comments.insert( - 0, - swc_common::comments::Comment { - kind: swc_common::comments::CommentKind::Block, - text: pragmas.join(" ").into(), - span: swc_common::DUMMY_SP, - }, - ); - } - } - - // Inject an import in the classic runtime for the pragma (and presumably, - // fragment). - if options.jsx_runtime == Some(JsxRuntime::Classic) { - let pragma = if let Some(pragma) = &options.pragma { - pragma - } else { - "React" - }; - let sym = pragma.split('.').next().expect("first item always exists"); - - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: swc_ecma_ast::Ident { - sym: sym.into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - imported: None, - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: Box::new(swc_ecma_ast::Str { - value: (if let Some(source) = &options.pragma_import_source { - source.clone() - } else { - "react".into() - }) - .into(), - span: swc_common::DUMMY_SP, - raw: None, - }), - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }), - )); - } - - // Find the `export default`, the JSX expression, and leave the rest as it - // is. - let mut input = program.module.body.split_off(0); - input.reverse(); - let mut layout = false; - let mut layout_position = None; - let content = true; - - while let Some(module_item) = input.pop() { - match module_item { - // ```js - // export default props => <>{props.children} - // ``` - // - // Treat it as an inline layout declaration. - // - // In estree, the below two are the same node (`ExportDefault`). - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultDecl( - decl, - )) => { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&decl.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - - layout = true; - layout_position = span_to_position(&decl.span, location); - match decl.decl { - swc_ecma_ast::DefaultDecl::Class(cls) => { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Class(cls))) - } - swc_ecma_ast::DefaultDecl::Fn(func) => { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Fn(func))) - } - swc_ecma_ast::DefaultDecl::TsInterfaceDecl(_) => { - return Err( - prefix_error_with_point( - "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component".into(), - bytepos_to_point(&decl.span.lo, location).as_ref() - ) - ); - } - } - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDefaultExpr( - expr, - )) => { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&expr.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - - layout = true; - layout_position = span_to_position(&expr.span, location); - replacements.push(create_layout_decl(*expr.expr)); - } - // ```js - // export {a, b as c} from 'd' - // export {a, b as c} - // ``` - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportNamed( - mut named_export, - )) => { - // SWC is currently crashing when generating code, w/o source - // map, if an actual location is set on this node. - named_export.span = swc_common::DUMMY_SP; - - let mut index = 0; - let mut id = None; - - while index < named_export.specifiers.len() { - let mut take = false; - // Note: the `swc_ecma_ast::ExportSpecifier::Default` - // branch of this looks interesting, but as far as I - // understand it *is not* valid ES. - // `export a from 'b'` is a syntax error, even in SWC. - if let swc_ecma_ast::ExportSpecifier::Named(named) = - &named_export.specifiers[index] - { - if let Some(swc_ecma_ast::ModuleExportName::Ident(ident)) = &named.exported - { - if ident.sym.as_ref() == "default" { - // For some reason the AST supports strings - // instead of identifiers. - // Looks like some TC39 proposal. Ignore for now - // and only do things if this is an ID. - if let swc_ecma_ast::ModuleExportName::Ident(ident) = &named.orig { - if layout { - return Err(create_double_layout_message( - bytepos_to_point(&ident.span.lo, location).as_ref(), - layout_position.as_ref(), - )); - } - layout = true; - layout_position = span_to_position(&ident.span, location); - take = true; - id = Some(ident.clone()); - } - } - } - } - - if take { - named_export.specifiers.remove(index); - } else { - index += 1; - } - } - - if let Some(id) = id { - let source = named_export.src.clone(); - - // If there was just a default export, we can drop the original node. - if !named_export.specifiers.is_empty() { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportNamed(named_export), - )); - } - - // It’s an `export {x} from 'y'`, so generate an import. - if let Some(source) = source { - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(swc_ecma_ast::ImportDecl { - specifiers: vec![swc_ecma_ast::ImportSpecifier::Named( - swc_ecma_ast::ImportNamedSpecifier { - local: swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - imported: Some(swc_ecma_ast::ModuleExportName::Ident(id)), - span: swc_common::DUMMY_SP, - is_type_only: false, - }, - )], - src: source, - type_only: false, - asserts: None, - span: swc_common::DUMMY_SP, - }), - )) - } - // It’s an `export {x}`, so generate a variable declaration. - else { - replacements.push(create_layout_decl(swc_ecma_ast::Expr::Ident(id))); - } - } else { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportNamed(named_export), - )); - } - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::Import(mut x)) => { - // SWC is currently crashing when generating code, w/o source - // map, if an actual location is set on this node. - x.span = swc_common::DUMMY_SP; - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::Import(x), - )); - } - swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportDecl(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::ExportAll(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsImportEquals(_)) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsExportAssignment( - _, - )) - | swc_ecma_ast::ModuleItem::ModuleDecl(swc_ecma_ast::ModuleDecl::TsNamespaceExport( - _, - )) => { - // Pass through. - replacements.push(module_item); - } - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr(expr_stmt)) => { - match *expr_stmt.expr { - swc_ecma_ast::Expr::JSXElement(elem) => { - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXElement(elem)), - layout, - )); - } - swc_ecma_ast::Expr::JSXFragment(mut frag) => { - // Unwrap if possible. - if frag.children.len() == 1 { - let item = frag.children.pop().unwrap(); - - if let swc_ecma_ast::JSXElementChild::JSXElement(elem) = item { - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXElement(elem)), - layout, - )); - continue; - } - - frag.children.push(item) - } - - replacements.append(&mut create_mdx_content( - Some(swc_ecma_ast::Expr::JSXFragment(frag)), - layout, - )); - } - _ => { - // Pass through. - replacements.push(swc_ecma_ast::ModuleItem::Stmt( - swc_ecma_ast::Stmt::Expr(expr_stmt), - )); - } - } - } - swc_ecma_ast::ModuleItem::Stmt(stmt) => { - replacements.push(swc_ecma_ast::ModuleItem::Stmt(stmt)); - } - } - } - - // Generate an empty component. - if !content { - replacements.append(&mut create_mdx_content(None, layout)); - } - - // ```jsx - // export default MDXContent - // ``` - replacements.push(swc_ecma_ast::ModuleItem::ModuleDecl( - swc_ecma_ast::ModuleDecl::ExportDefaultExpr(swc_ecma_ast::ExportDefaultExpr { - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "MDXContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }), - )); - - program.module.body = replacements; - - Ok(program) -} - -/// Create a content component. -fn create_mdx_content( - expr: Option, - has_internal_layout: bool, -) -> Vec { - // ```jsx - // xxx - // ``` - let mut result = swc_ecma_ast::Expr::JSXElement(Box::new(swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( - swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }, - )], - self_closing: false, - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: Some(swc_ecma_ast::JSXClosingElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - span: swc_common::DUMMY_SP, - }), - // ```jsx - // <_createMdxContent {...props} /> - // ``` - children: vec![swc_ecma_ast::JSXElementChild::JSXElement(Box::new( - swc_ecma_ast::JSXElement { - opening: swc_ecma_ast::JSXOpeningElement { - name: swc_ecma_ast::JSXElementName::Ident(swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }), - attrs: vec![swc_ecma_ast::JSXAttrOrSpread::SpreadElement( - swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }, - )], - self_closing: true, - type_args: None, - span: swc_common::DUMMY_SP, - }, - closing: None, - children: vec![], - span: swc_common::DUMMY_SP, - }, - ))], - span: swc_common::DUMMY_SP, - })); - - if !has_internal_layout { - // ```jsx - // MDXLayout ? xxx : _createMdxContent(props) - // ``` - result = swc_ecma_ast::Expr::Cond(swc_ecma_ast::CondExpr { - test: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - cons: Box::new(result), - alt: Box::new(swc_ecma_ast::Expr::Call(swc_ecma_ast::CallExpr { - callee: swc_ecma_ast::Callee::Expr(Box::new(swc_ecma_ast::Expr::Ident( - swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - ))), - args: vec![swc_ecma_ast::ExprOrSpread { - spread: None, - expr: Box::new(swc_ecma_ast::Expr::Ident(swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - })), - }], - type_args: None, - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - }); - } - - // ```jsx - // function _createMdxContent(props) { - // return xxx - // } - // ``` - let create_mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( - swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { - ident: swc_ecma_ast::Ident { - sym: "_createMdxContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: vec![swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }], - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - arg: Some(Box::new(expr.unwrap_or({ - swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Null(swc_ecma_ast::Null { - span: swc_common::DUMMY_SP, - })) - }))), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }), - )); - - // ```jsx - // function MDXContent(props = {}) { - // return xxx - // } - // ``` - let mdx_content = swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl( - swc_ecma_ast::Decl::Fn(swc_ecma_ast::FnDecl { - ident: swc_ecma_ast::Ident { - sym: "MDXContent".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - declare: false, - function: Box::new(swc_ecma_ast::Function { - params: vec![swc_ecma_ast::Param { - pat: swc_ecma_ast::Pat::Assign(swc_ecma_ast::AssignPat { - left: Box::new(swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "props".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - })), - right: Box::new(swc_ecma_ast::Expr::Object(swc_ecma_ast::ObjectLit { - props: vec![], - span: swc_common::DUMMY_SP, - })), - span: swc_common::DUMMY_SP, - type_ann: None, - }), - decorators: vec![], - span: swc_common::DUMMY_SP, - }], - decorators: vec![], - body: Some(swc_ecma_ast::BlockStmt { - stmts: vec![swc_ecma_ast::Stmt::Return(swc_ecma_ast::ReturnStmt { - arg: Some(Box::new(result)), - span: swc_common::DUMMY_SP, - })], - span: swc_common::DUMMY_SP, - }), - is_generator: false, - is_async: false, - type_params: None, - return_type: None, - span: swc_common::DUMMY_SP, - }), - }), - )); - - vec![create_mdx_content, mdx_content] -} - -/// Create a layout, inside the document. -fn create_layout_decl(expr: swc_ecma_ast::Expr) -> swc_ecma_ast::ModuleItem { - // ```jsx - // const MDXLayout = xxx - // ``` - swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Decl(swc_ecma_ast::Decl::Var(Box::new( - swc_ecma_ast::VarDecl { - kind: swc_ecma_ast::VarDeclKind::Const, - declare: false, - decls: vec![swc_ecma_ast::VarDeclarator { - name: swc_ecma_ast::Pat::Ident(swc_ecma_ast::BindingIdent { - id: swc_ecma_ast::Ident { - sym: "MDXLayout".into(), - optional: false, - span: swc_common::DUMMY_SP, - }, - type_ann: None, - }), - init: Some(Box::new(expr)), - span: swc_common::DUMMY_SP, - definite: false, - }], - span: swc_common::DUMMY_SP, - }, - )))) -} - -/// Create an error message about multiple layouts. -fn create_double_layout_message(at: Option<&Point>, previous: Option<&Position>) -> String { - prefix_error_with_point( - format!( - "Cannot specify multiple layouts{}", - if let Some(previous) = previous { - format!(" (previous: {:?})", previous) - } else { - "".into() - } - ), - at, - ) -} diff --git a/tests/test_utils/to_hast.rs b/tests/test_utils/to_hast.rs deleted file mode 100644 index 1ba8d35..0000000 --- a/tests/test_utils/to_hast.rs +++ /dev/null @@ -1,1247 +0,0 @@ -use crate::test_utils::hast; -use micromark::{mdast, sanitize, unist::Position}; - -// To do: support these compile options: -// ``` -// pub gfm_footnote_label: Option, -// pub gfm_footnote_label_tag_name: Option, -// pub gfm_footnote_label_attributes: Option, -// pub gfm_footnote_back_label: Option, -// pub gfm_footnote_clobber_prefix: Option, -// ``` -// -// Maybe also: -// * option to persist `meta`? -// * option to generate a `style` attribute instead of `align`? -// * support `Raw` nodes for HTML? -// -// To do: -// * revert references when undefined? -// - -#[derive(Debug)] -struct State { - definitions: Vec<(String, String, Option)>, - footnote_definitions: Vec<(String, Vec)>, - footnote_calls: Vec<(String, usize)>, -} - -#[derive(Debug)] -enum Result { - Fragment(Vec), - Node(hast::Node), - None, -} - -#[allow(dead_code)] -pub fn to_hast(mdast: &mdast::Node) -> hast::Node { - let mut definitions = vec![]; - - // Collect definitions. - // Calls take info from their definition. - // Calls can come come before definitions. - // Footnote calls can also come before footnote definitions, but those - // calls *do not* take info from their definitions, so we don’t care - // about footnotes here. - visit(mdast, |node| { - if let mdast::Node::Definition(definition) = node { - definitions.push(( - definition.identifier.clone(), - definition.url.clone(), - definition.title.clone(), - )); - } - }); - - let mut state = State { - definitions, - footnote_definitions: vec![], - footnote_calls: vec![], - }; - - let result = one(&mut state, mdast, None); - - if state.footnote_calls.is_empty() { - if let Result::Node(node) = result { - return node; - } - } - - // We either have to generate a footer, or we don’t have a single node. - // So we need a root. - let mut root = hast::Root { - children: vec![], - position: None, - }; - - match result { - Result::Fragment(children) => root.children = children, - Result::Node(node) => { - if let hast::Node::Root(existing) = node { - root = existing; - } else { - root.children.push(node); - } - } - Result::None => {} - } - - if !state.footnote_calls.is_empty() { - let mut items = vec![]; - - let mut index = 0; - while index < state.footnote_calls.len() { - let (id, count) = &state.footnote_calls[index]; - let safe_id = sanitize(&id.to_lowercase()); - - // Find definition: we’ll always find it. - let mut definition_index = 0; - while definition_index < state.footnote_definitions.len() { - if &state.footnote_definitions[definition_index].0 == id { - break; - } - definition_index += 1; - } - debug_assert_ne!( - definition_index, - state.footnote_definitions.len(), - "expected definition" - ); - - // We’ll find each used definition once, so we can split off to take the content. - let mut content = state.footnote_definitions[definition_index].1.split_off(0); - - let mut reference_index = 0; - let mut backreferences = vec![]; - while reference_index < *count { - let mut backref_children = vec![hast::Node::Text(hast::Text { - value: "↩".into(), - position: None, - })]; - - if reference_index != 0 { - backreferences.push(hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - })); - - backref_children.push(hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: (reference_index + 1).to_string(), - position: None, - })], - position: None, - })); - } - - backreferences.push(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String(format!( - "#fnref-{}{}", - safe_id, - if reference_index == 0 { - "".into() - } else { - format!("-{}", &(reference_index + 1).to_string()) - } - )), - ), - ( - "dataFootnoteBackref".into(), - hast::PropertyValue::Boolean(true), - ), - ( - "ariaLabel".into(), - hast::PropertyValue::String("Back to content".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "data-footnote-backref".into() - ]), - ), - ], - children: backref_children, - position: None, - })); - - reference_index += 1; - } - - let mut backreference_opt = Some(backreferences); - - if let Some(hast::Node::Element(tail_element)) = content.last_mut() { - if tail_element.tag_name == "p" { - if let Some(hast::Node::Text(text)) = tail_element.children.last_mut() { - text.value.push(' '); - } else { - tail_element.children.push(hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - })); - } - - tail_element - .children - .append(&mut backreference_opt.take().unwrap()); - } - } - - // No paragraph, just push them. - if let Some(mut backreference) = backreference_opt { - content.append(&mut backreference); - } - - items.push(hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties: vec![( - "id".into(), - hast::PropertyValue::String(format!("#fn-{}", safe_id)), - )], - children: wrap(content, true), - position: None, - })); - index += 1; - } - - root.children.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - root.children.push(hast::Node::Element(hast::Element { - tag_name: "section".into(), - properties: vec![ - ("dataFootnotes".into(), hast::PropertyValue::Boolean(true)), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["footnotes".into()]), - ), - ], - children: vec![ - hast::Node::Element(hast::Element { - tag_name: "h2".into(), - properties: vec![ - ( - "id".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["sr-only".into()]), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: "Footnotes".into(), - position: None, - })], - position: None, - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - hast::Node::Element(hast::Element { - tag_name: "ol".into(), - properties: vec![], - children: wrap(items, true), - position: None, - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - ], - position: None, - })); - root.children.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - hast::Node::Root(root) -} - -fn one(state: &mut State, node: &mdast::Node, parent: Option<&mdast::Node>) -> Result { - match node { - mdast::Node::BlockQuote(d) => transform_block_quote(state, node, d), - mdast::Node::Break(d) => transform_break(state, node, d), - mdast::Node::Code(d) => transform_code(state, node, d), - mdast::Node::Delete(d) => transform_delete(state, node, d), - mdast::Node::Emphasis(d) => transform_emphasis(state, node, d), - mdast::Node::FootnoteDefinition(d) => transform_footnote_definition(state, node, d), - mdast::Node::FootnoteReference(d) => transform_footnote_reference(state, node, d), - mdast::Node::Heading(d) => transform_heading(state, node, d), - mdast::Node::Image(d) => transform_image(state, node, d), - mdast::Node::ImageReference(d) => transform_image_reference(state, node, d), - mdast::Node::InlineCode(d) => transform_inline_code(state, node, d), - mdast::Node::InlineMath(d) => transform_inline_math(state, node, d), - mdast::Node::Link(d) => transform_link(state, node, d), - mdast::Node::LinkReference(d) => transform_link_reference(state, node, d), - mdast::Node::ListItem(d) => transform_list_item(state, node, parent, d), - mdast::Node::List(d) => transform_list(state, node, d), - mdast::Node::Math(d) => transform_math(state, node, d), - mdast::Node::MdxFlowExpression(_) | mdast::Node::MdxTextExpression(_) => { - transform_mdx_expression(state, node) - } - mdast::Node::MdxJsxFlowElement(_) | mdast::Node::MdxJsxTextElement(_) => { - transform_mdx_jsx_element(state, node) - } - mdast::Node::MdxjsEsm(d) => transform_mdxjs_esm(state, node, d), - mdast::Node::Paragraph(d) => transform_paragraph(state, node, d), - mdast::Node::Root(d) => transform_root(state, node, d), - mdast::Node::Strong(d) => transform_strong(state, node, d), - // Note: this is only called here if there is a single cell passed, not when one is found in a table. - mdast::Node::TableCell(d) => { - transform_table_cell(state, node, false, mdast::AlignKind::None, d) - } - // Note: this is only called here if there is a single row passed, not when one is found in a table. - mdast::Node::TableRow(d) => transform_table_row(state, node, false, None, d), - mdast::Node::Table(d) => transform_table(state, node, d), - mdast::Node::Text(d) => transform_text(state, node, d), - mdast::Node::ThematicBreak(d) => transform_thematic_break(state, node, d), - // Ignore. - mdast::Node::Definition(_) - | mdast::Node::Html(_) - | mdast::Node::Yaml(_) - | mdast::Node::Toml(_) => Result::None, - } -} - -/// [`BlockQuote`][mdast::BlockQuote]. -fn transform_block_quote( - state: &mut State, - node: &mdast::Node, - block_quote: &mdast::BlockQuote, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "blockquote".into(), - properties: vec![], - children: wrap(all(state, node), true), - position: block_quote.position.clone(), - })) -} - -/// [`Break`][mdast::Break]. -fn transform_break(_state: &mut State, _node: &mdast::Node, break_: &mdast::Break) -> Result { - Result::Fragment(vec![ - hast::Node::Element(hast::Element { - tag_name: "br".into(), - properties: vec![], - children: vec![], - position: break_.position.clone(), - }), - hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - }), - ]) -} - -/// [`Code`][mdast::Code]. -fn transform_code(_state: &mut State, _node: &mdast::Node, code: &mdast::Code) -> Result { - let mut value = code.value.clone(); - value.push('\n'); - let mut properties = vec![]; - - if let Some(lang) = code.lang.as_ref() { - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![format!("language-{}", lang)]), - )); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties, - children: vec![hast::Node::Text(hast::Text { - value, - position: None, - })], - position: code.position.clone(), - })], - position: code.position.clone(), - })) -} - -/// [`Delete`][mdast::Delete]. -fn transform_delete(state: &mut State, node: &mdast::Node, delete: &mdast::Delete) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "del".into(), - properties: vec![], - children: all(state, node), - position: delete.position.clone(), - })) -} - -/// [`Emphasis`][mdast::Emphasis]. -fn transform_emphasis(state: &mut State, node: &mdast::Node, emphasis: &mdast::Emphasis) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "em".into(), - properties: vec![], - children: all(state, node), - position: emphasis.position.clone(), - })) -} - -/// [`FootnoteDefinition`][mdast::FootnoteDefinition]. -fn transform_footnote_definition( - state: &mut State, - node: &mdast::Node, - footnote_definition: &mdast::FootnoteDefinition, -) -> Result { - let children = all(state, node); - // Set aside. - state - .footnote_definitions - .push((footnote_definition.identifier.clone(), children)); - Result::None -} - -/// [`FootnoteReference`][mdast::FootnoteReference]. -fn transform_footnote_reference( - state: &mut State, - _node: &mdast::Node, - footnote_reference: &mdast::FootnoteReference, -) -> Result { - let safe_id = sanitize(&footnote_reference.identifier.to_lowercase()); - let mut call_index = 0; - - // See if this has been called before. - while call_index < state.footnote_calls.len() { - if state.footnote_calls[call_index].0 == footnote_reference.identifier { - break; - } - call_index += 1; - } - - // New. - if call_index == state.footnote_calls.len() { - state - .footnote_calls - .push((footnote_reference.identifier.clone(), 0)); - } - - // Increment. - state.footnote_calls[call_index].1 += 1; - - let reuse_counter = state.footnote_calls[call_index].1; - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "sup".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties: vec![ - ( - "href".into(), - hast::PropertyValue::String(format!("#fn-{}", safe_id)), - ), - ( - "id".into(), - hast::PropertyValue::String(format!( - "fnref-{}{}", - safe_id, - if reuse_counter > 1 { - format!("-{}", reuse_counter) - } else { - "".into() - } - )), - ), - ("dataFootnoteRef".into(), hast::PropertyValue::Boolean(true)), - ( - "ariaDescribedBy".into(), - hast::PropertyValue::String("footnote-label".into()), - ), - ], - children: vec![hast::Node::Text(hast::Text { - value: (call_index + 1).to_string(), - position: None, - })], - position: None, - })], - position: footnote_reference.position.clone(), - })) -} - -/// [`Heading`][mdast::Heading]. -fn transform_heading(state: &mut State, node: &mdast::Node, heading: &mdast::Heading) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: format!("h{}", heading.depth), - properties: vec![], - children: all(state, node), - position: heading.position.clone(), - })) -} - -/// [`Image`][mdast::Image]. -fn transform_image(_state: &mut State, _node: &mdast::Node, image: &mdast::Image) -> Result { - let mut properties = vec![]; - - properties.push(( - "src".into(), - hast::PropertyValue::String(sanitize(&image.url)), - )); - - properties.push(("alt".into(), hast::PropertyValue::String(image.alt.clone()))); - - if let Some(value) = image.title.as_ref() { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties, - children: vec![], - position: image.position.clone(), - })) -} - -/// [`ImageReference`][mdast::ImageReference]. -fn transform_image_reference( - state: &mut State, - _node: &mdast::Node, - image_reference: &mdast::ImageReference, -) -> Result { - let mut properties = vec![]; - - let definition = state - .definitions - .iter() - .find(|d| d.0 == image_reference.identifier); - - let (_, url, title) = - definition.expect("expected reference to have a corresponding definition"); - - properties.push(("src".into(), hast::PropertyValue::String(sanitize(url)))); - - properties.push(( - "alt".into(), - hast::PropertyValue::String(image_reference.alt.clone()), - )); - - if let Some(value) = title { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "img".into(), - properties, - children: vec![], - position: image_reference.position.clone(), - })) -} - -/// [`InlineCode`][mdast::InlineCode]. -fn transform_inline_code( - _state: &mut State, - _node: &mdast::Node, - inline_code: &mdast::InlineCode, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![], - children: vec![hast::Node::Text(hast::Text { - value: replace_eols_with_spaces(&inline_code.value), - position: None, - })], - position: inline_code.position.clone(), - })) -} - -/// [`InlineMath`][mdast::InlineMath]. -fn transform_inline_math( - _state: &mut State, - _node: &mdast::Node, - inline_math: &mdast::InlineMath, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["language-math".into(), "math-inline".into()]), - )], - children: vec![hast::Node::Text(hast::Text { - value: replace_eols_with_spaces(&inline_math.value), - position: None, - })], - position: inline_math.position.clone(), - })) -} - -/// [`Link`][mdast::Link]. -fn transform_link(state: &mut State, node: &mdast::Node, link: &mdast::Link) -> Result { - let mut properties = vec![]; - - properties.push(( - "href".into(), - hast::PropertyValue::String(sanitize(&link.url)), - )); - - if let Some(value) = link.title.as_ref() { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties, - children: all(state, node), - position: link.position.clone(), - })) -} - -/// [`LinkReference`][mdast::LinkReference]. -fn transform_link_reference( - state: &mut State, - node: &mdast::Node, - link_reference: &mdast::LinkReference, -) -> Result { - let mut properties = vec![]; - - let definition = state - .definitions - .iter() - .find(|d| d.0 == link_reference.identifier); - - let (_, url, title) = - definition.expect("expected reference to have a corresponding definition"); - - properties.push(("href".into(), hast::PropertyValue::String(sanitize(url)))); - - if let Some(value) = title { - properties.push(("title".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "a".into(), - properties, - children: all(state, node), - position: link_reference.position.clone(), - })) -} - -/// [`ListItem`][mdast::ListItem]. -fn transform_list_item( - state: &mut State, - node: &mdast::Node, - parent: Option<&mdast::Node>, - list_item: &mdast::ListItem, -) -> Result { - let mut children = all(state, node); - let mut loose = list_item_loose(node); - - if let Some(parent) = parent { - if matches!(parent, mdast::Node::List(_)) { - loose = list_loose(parent); - } - }; - - let mut properties = vec![]; - - // Inject a checkbox. - if let Some(checked) = list_item.checked { - // According to github-markdown-css, this class hides bullet. - // See: . - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["task-list-item".into()]), - )); - - let mut input = Some(hast::Node::Element(hast::Element { - tag_name: "input".into(), - properties: vec![ - ( - "type".into(), - hast::PropertyValue::String("checkbox".into()), - ), - ("checked".into(), hast::PropertyValue::Boolean(checked)), - ("disabled".into(), hast::PropertyValue::Boolean(true)), - ], - children: vec![], - position: None, - })); - - if let Some(hast::Node::Element(x)) = children.first_mut() { - if x.tag_name == "p" { - if !x.children.is_empty() { - x.children.insert( - 0, - hast::Node::Text(hast::Text { - value: " ".into(), - position: None, - }), - ); - } - - x.children.insert(0, input.take().unwrap()); - } - } - - // If the input wasn‘t injected yet, inject a paragraph. - if let Some(input) = input { - children.insert( - 0, - hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: vec![input], - position: None, - }), - ); - } - } - - children.reverse(); - let mut result = vec![]; - let mut head = true; - let empty = children.is_empty(); - let mut tail_p = false; - - while let Some(child) = children.pop() { - let mut is_p = false; - if let hast::Node::Element(el) = &child { - if el.tag_name == "p" { - is_p = true; - } - } - - // Add eols before nodes, except if this is a tight, first paragraph. - if loose || !head || !is_p { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - if is_p && !loose { - // Unwrap the paragraph. - if let hast::Node::Element(mut el) = child { - result.append(&mut el.children); - } - } else { - result.push(child); - } - - head = false; - tail_p = is_p; - } - - // Add eol after last node, except if it is tight or a paragraph. - if !empty && (loose || !tail_p) { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "li".into(), - properties, - children: result, - position: list_item.position.clone(), - })) -} - -/// [`List`][mdast::List]. -fn transform_list(state: &mut State, node: &mdast::Node, list: &mdast::List) -> Result { - let mut contains_task_list = false; - let mut index = 0; - - while index < list.children.len() { - if let mdast::Node::ListItem(item) = &list.children[index] { - if item.checked.is_some() { - contains_task_list = true; - } - } - - index += 1; - } - - let mut properties = vec![]; - - // Add start. - if let Some(start) = list.start { - if list.ordered && start != 1 { - properties.push(( - "start".into(), - hast::PropertyValue::String(start.to_string()), - )); - } - } - - // Like GitHub, add a class for custom styling. - if contains_task_list { - properties.push(( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec!["contains-task-list".into()]), - )); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: if list.ordered { - "ol".into() - } else { - "ul".into() - }, - properties, - children: wrap(all(state, node), true), - position: list.position.clone(), - })) -} - -/// [`Math`][mdast::Math]. -fn transform_math(_state: &mut State, _node: &mdast::Node, math: &mdast::Math) -> Result { - let mut value = math.value.clone(); - value.push('\n'); - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "pre".into(), - properties: vec![], - children: vec![hast::Node::Element(hast::Element { - tag_name: "code".into(), - properties: vec![( - "className".into(), - hast::PropertyValue::SpaceSeparated(vec![ - "language-math".into(), - "math-display".into(), - ]), - )], - children: vec![hast::Node::Text(hast::Text { - value, - position: None, - })], - position: math.position.clone(), - })], - position: math.position.clone(), - })) -} - -/// [`MdxFlowExpression`][mdast::MdxFlowExpression],[`MdxTextExpression`][mdast::MdxTextExpression]. -fn transform_mdx_expression(_state: &mut State, node: &mdast::Node) -> Result { - match node { - mdast::Node::MdxFlowExpression(node) => { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.value.clone(), - position: node.position.clone(), - stops: node.stops.clone(), - })) - } - mdast::Node::MdxTextExpression(node) => { - Result::Node(hast::Node::MdxExpression(hast::MdxExpression { - value: node.value.clone(), - position: node.position.clone(), - stops: node.stops.clone(), - })) - } - _ => unreachable!("expected expression"), - } -} - -/// [`MdxJsxFlowElement`][mdast::MdxJsxFlowElement],[`MdxJsxTextElement`][mdast::MdxJsxTextElement]. -fn transform_mdx_jsx_element(state: &mut State, node: &mdast::Node) -> Result { - let (name, attributes) = match node { - mdast::Node::MdxJsxFlowElement(n) => (&n.name, &n.attributes), - mdast::Node::MdxJsxTextElement(n) => (&n.name, &n.attributes), - _ => unreachable!("expected mdx jsx element"), - }; - - Result::Node(hast::Node::MdxJsxElement(hast::MdxJsxElement { - name: name.clone(), - attributes: attributes.clone(), - children: all(state, node), - position: node.position().cloned(), - })) -} - -/// [`MdxjsEsm`][mdast::MdxjsEsm]. -fn transform_mdxjs_esm( - _state: &mut State, - _node: &mdast::Node, - mdxjs_esm: &mdast::MdxjsEsm, -) -> Result { - Result::Node(hast::Node::MdxjsEsm(hast::MdxjsEsm { - value: mdxjs_esm.value.clone(), - position: mdxjs_esm.position.clone(), - stops: mdxjs_esm.stops.clone(), - })) -} - -/// [`Paragraph`][mdast::Paragraph]. -fn transform_paragraph( - state: &mut State, - node: &mdast::Node, - paragraph: &mdast::Paragraph, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "p".into(), - properties: vec![], - children: all(state, node), - position: paragraph.position.clone(), - })) -} - -/// [`Root`][mdast::Root]. -fn transform_root(state: &mut State, node: &mdast::Node, root: &mdast::Root) -> Result { - Result::Node(hast::Node::Root(hast::Root { - children: wrap(all(state, node), false), - position: root.position.clone(), - })) -} - -/// [`Strong`][mdast::Strong]. -fn transform_strong(state: &mut State, node: &mdast::Node, strong: &mdast::Strong) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "strong".into(), - properties: vec![], - children: all(state, node), - position: strong.position.clone(), - })) -} - -/// [`TableCell`][mdast::TableCell]. -fn transform_table_cell( - state: &mut State, - node: &mdast::Node, - head: bool, - align: mdast::AlignKind, - table_cell: &mdast::TableCell, -) -> Result { - let align_value = match align { - mdast::AlignKind::None => None, - mdast::AlignKind::Left => Some("left"), - mdast::AlignKind::Right => Some("right"), - mdast::AlignKind::Center => Some("center"), - }; - - let mut properties = vec![]; - - if let Some(value) = align_value { - properties.push(("align".into(), hast::PropertyValue::String(value.into()))); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: if head { "th".into() } else { "td".into() }, - properties, - children: all(state, node), - position: table_cell.position.clone(), - })) -} - -/// [`TableRow`][mdast::TableRow]. -fn transform_table_row( - state: &mut State, - _node: &mdast::Node, - head: bool, - align: Option<&[mdast::AlignKind]>, - table_row: &mdast::TableRow, -) -> Result { - let mut children = vec![]; - let mut index = 0; - #[allow(clippy::redundant_closure_for_method_calls)] - let len = align.map_or(table_row.children.len(), |d| d.len()); - let empty_cell = mdast::Node::TableCell(mdast::TableCell { - children: vec![], - position: None, - }); - - while index < len { - let align_value = align - .and_then(|d| d.get(index)) - .unwrap_or(&mdast::AlignKind::None); - - let child = table_row.children.get(index).unwrap_or(&empty_cell); - - let result = if let mdast::Node::TableCell(table_cell) = child { - transform_table_cell(state, child, head, *align_value, table_cell) - } else { - unreachable!("expected tale cell in table row") - }; - - append_result(&mut children, result); - index += 1; - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "tr".into(), - properties: vec![], - children: wrap(children, true), - position: table_row.position.clone(), - })) -} - -/// [`Table`][mdast::Table]. -fn transform_table(state: &mut State, _node: &mdast::Node, table: &mdast::Table) -> Result { - let mut rows = vec![]; - let mut index = 0; - - while index < table.children.len() { - let child = &table.children[index]; - let result = if let mdast::Node::TableRow(table_row) = child { - transform_table_row( - state, - &table.children[index], - index == 0, - Some(&table.align), - table_row, - ) - } else { - unreachable!("expected table row as child of table") - }; - - append_result(&mut rows, result); - index += 1; - } - - let body_rows = rows.split_off(1); - let head_row = rows.pop(); - let mut children = vec![]; - - if let Some(row) = head_row { - let position = row.position().cloned(); - children.push(hast::Node::Element(hast::Element { - tag_name: "thead".into(), - properties: vec![], - children: wrap(vec![row], true), - position, - })); - } - - if !body_rows.is_empty() { - let mut position = None; - - if let Some(position_start) = body_rows.first().and_then(hast::Node::position) { - if let Some(position_end) = body_rows.last().and_then(hast::Node::position) { - position = Some(Position { - start: position_start.start.clone(), - end: position_end.end.clone(), - }); - } - } - - children.push(hast::Node::Element(hast::Element { - tag_name: "tbody".into(), - properties: vec![], - children: wrap(body_rows, true), - position, - })); - } - - Result::Node(hast::Node::Element(hast::Element { - tag_name: "table".into(), - properties: vec![], - children: wrap(children, true), - position: table.position.clone(), - })) -} - -/// [`Text`][mdast::Text]. -fn transform_text(_state: &mut State, _node: &mdast::Node, text: &mdast::Text) -> Result { - Result::Node(hast::Node::Text(hast::Text { - value: text.value.clone(), - position: text.position.clone(), - })) -} - -/// [`ThematicBreak`][mdast::ThematicBreak]. -fn transform_thematic_break( - _state: &mut State, - _node: &mdast::Node, - thematic_break: &mdast::ThematicBreak, -) -> Result { - Result::Node(hast::Node::Element(hast::Element { - tag_name: "hr".into(), - properties: vec![], - children: vec![], - position: thematic_break.position.clone(), - })) -} - -// Transform children of `parent`. -fn all(state: &mut State, parent: &mdast::Node) -> Vec { - let mut nodes = vec![]; - if let Some(children) = parent.children() { - let mut index = 0; - while index < children.len() { - let child = &children[index]; - let result = one(state, child, Some(parent)); - append_result(&mut nodes, result); - index += 1; - } - } - - nodes -} - -/// Wrap `nodes` with line feeds between each entry. -/// Optionally adds line feeds at the start and end. -fn wrap(mut nodes: Vec, loose: bool) -> Vec { - let mut result = vec![]; - let was_empty = nodes.is_empty(); - let mut head = true; - - nodes.reverse(); - - if loose { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - while let Some(item) = nodes.pop() { - // Inject when there’s more: - if !head { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - head = false; - result.push(item); - } - - if loose && !was_empty { - result.push(hast::Node::Text(hast::Text { - value: "\n".into(), - position: None, - })); - } - - result -} - -/// Visit. -fn visit(node: &mdast::Node, visitor: Visitor) -where - Visitor: FnMut(&mdast::Node), -{ - visit_impl(node, visitor); -} - -/// Visit, mutably. -// Probably useful later: -#[allow(dead_code)] -fn visit_mut(node: &mut mdast::Node, visitor: Visitor) -where - Visitor: FnMut(&mut mdast::Node), -{ - visit_mut_impl(node, visitor); -} - -/// Internal implementation to visit. -fn visit_impl(node: &mdast::Node, mut visitor: Visitor) -> Visitor -where - Visitor: FnMut(&mdast::Node), -{ - visitor(node); - - if let Some(children) = node.children() { - let mut index = 0; - while index < children.len() { - let child = &children[index]; - visitor = visit_impl(child, visitor); - index += 1; - } - } - - visitor -} - -/// Internal implementation to visit, mutably. -fn visit_mut_impl(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor -where - Visitor: FnMut(&mut mdast::Node), -{ - visitor(node); - - if let Some(children) = node.children_mut() { - let mut index = 0; - while let Some(child) = children.get_mut(index) { - visitor = visit_mut_impl(child, visitor); - index += 1; - } - } - - visitor -} - -// To do: trim arounds breaks: . -/// Append an (optional, variadic) result. -fn append_result(list: &mut Vec, result: Result) { - match result { - Result::Fragment(mut fragment) => list.append(&mut fragment), - Result::Node(node) => list.push(node), - Result::None => {} - }; -} - -/// Replace line endings (CR, LF, CRLF) with spaces. -/// -/// Used for inline code and inline math. -fn replace_eols_with_spaces(value: &str) -> String { - // It’ll grow a bit small for each CR+LF. - let mut result = String::with_capacity(value.len()); - let bytes = value.as_bytes(); - let mut index = 0; - let mut start = 0; - - while index < bytes.len() { - let byte = bytes[index]; - - if byte == b'\r' || byte == b'\n' { - result.push_str(&value[start..index]); - result.push(' '); - - if index + 1 < bytes.len() && byte == b'\r' && bytes[index + 1] == b'\n' { - index += 1; - } - - start = index + 1; - } - - index += 1; - } - - result.push_str(&value[start..]); - - result -} - -/// Check if a list is loose. -fn list_loose(node: &mdast::Node) -> bool { - if let mdast::Node::List(list) = node { - if list.spread { - return true; - } - - if let Some(children) = node.children() { - let mut index = 0; - while index < children.len() { - if list_item_loose(&children[index]) { - return true; - } - index += 1; - } - } - } - - false -} - -/// Check if a list item is loose. -fn list_item_loose(node: &mdast::Node) -> bool { - if let mdast::Node::ListItem(item) = node { - item.spread - } else { - false - } -} diff --git a/tests/test_utils/to_swc.rs b/tests/test_utils/to_swc.rs deleted file mode 100644 index 02de514..0000000 --- a/tests/test_utils/to_swc.rs +++ /dev/null @@ -1,756 +0,0 @@ -extern crate swc_common; -extern crate swc_ecma_ast; -use crate::test_utils::{ - hast, - micromark_swc_utils::position_to_span, - swc::{parse_esm_to_tree, parse_expression_to_tree}, - swc_utils::create_ident, -}; -use core::str; -use micromark::{Location, MdxExpressionKind}; - -/// Result. -#[derive(Debug, PartialEq, Eq)] -pub struct Program { - pub path: Option, - /// 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<'a> { - /// 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, - /// Optional way to turn relative positions into points. - location: Option<&'a Location>, -} - -#[allow(dead_code)] -pub fn to_swc( - tree: &hast::Node, - path: Option, - location: Option<&Location>, -) -> Result { - let mut context = Context { - space: Space::Html, - comments: vec![], - esm: vec![], - location, - }; - let expr = match one(&mut context, tree)? { - Some(swc_ecma_ast::JSXElementChild::JSXFragment(x)) => { - Some(swc_ecma_ast::Expr::JSXFragment(x)) - } - Some(swc_ecma_ast::JSXElementChild::JSXElement(x)) => { - Some(swc_ecma_ast::Expr::JSXElement(x)) - } - Some(child) => Some(swc_ecma_ast::Expr::JSXFragment(create_fragment( - vec![child], - tree, - ))), - None => None, - }; - - // Add the ESM. - let mut module = swc_ecma_ast::Module { - shebang: None, - body: context.esm, - span: position_to_span(tree.position()), - }; - - // We have some content, wrap it. - if let Some(expr) = expr { - module - .body - .push(swc_ecma_ast::ModuleItem::Stmt(swc_ecma_ast::Stmt::Expr( - swc_ecma_ast::ExprStmt { - expr: Box::new(expr), - span: swc_common::DUMMY_SP, - }, - ))); - } - - Ok(Program { - path, - module, - comments: context.comments, - }) -} - -/// Transform one node. -fn one( - context: &mut Context, - node: &hast::Node, -) -> Result, 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, stops)) => { - Some(swc_ecma_ast::JSXAttrValue::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( - value, - &MdxExpressionKind::AttributeValueExpression, - stops, - context.location, - )?), - span: swc_common::DUMMY_SP, - }, - )) - } - None => None, - }; - - swc_ecma_ast::JSXAttrOrSpread::JSXAttr(swc_ecma_ast::JSXAttr { - span: swc_common::DUMMY_SP, - name: create_jsx_attr_name(&prop.name), - value, - }) - } - hast::AttributeContent::Expression(value, stops) => { - let expr = parse_expression_to_tree( - value, - &MdxExpressionKind::AttributeExpression, - stops, - context.location, - )?; - swc_ecma_ast::JSXAttrOrSpread::SpreadElement(swc_ecma_ast::SpreadElement { - dot3_token: swc_common::DUMMY_SP, - expr, - }) - } - }; - - attrs.push(attr); - index += 1; - } - - Ok(Some(if let Some(name) = &element.name { - swc_ecma_ast::JSXElementChild::JSXElement(create_element(name, attrs, children, node)) - } else { - swc_ecma_ast::JSXElementChild::JSXFragment(create_fragment(children, node)) - })) -} - -/// [`MdxExpression`][hast::MdxExpression]. -fn transform_mdx_expression( - context: &mut Context, - node: &hast::Node, - expression: &hast::MdxExpression, -) -> Result, String> { - Ok(Some(swc_ecma_ast::JSXElementChild::JSXExprContainer( - swc_ecma_ast::JSXExprContainer { - expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree( - &expression.value, - &MdxExpressionKind::Expression, - &expression.stops, - context.location, - )?), - span: position_to_span(node.position()), - }, - ))) -} - -/// [`MdxjsEsm`][hast::MdxjsEsm]. -fn transform_mdxjs_esm( - context: &mut Context, - _node: &hast::Node, - esm: &hast::MdxjsEsm, -) -> Result, String> { - let mut module = parse_esm_to_tree(&esm.value, &esm.stops, context.location)?; - let mut index = 0; - - // To do: check that identifiers are not duplicated across esm blocks. - while index < module.body.len() { - if !matches!(module.body[index], swc_ecma_ast::ModuleItem::ModuleDecl(_)) { - return Err("Unexpected `statement` in code: only import/exports are supported".into()); - } - index += 1; - } - - context.esm.append(&mut module.body); - Ok(None) -} - -/// [`Root`][hast::Root]. -fn transform_root( - context: &mut Context, - node: &hast::Node, - _root: &hast::Root, -) -> Result, String> { - let mut children = all(context, node)?; - let mut queue = vec![]; - let mut nodes = vec![]; - let mut seen = false; - - children.reverse(); - - // Remove initial/final whitespace. - while let Some(child) = children.pop() { - let mut stash = false; - - if let swc_ecma_ast::JSXElementChild::JSXExprContainer(container) = &child { - if let swc_ecma_ast::JSXExpr::Expr(expr) = &container.expr { - if let swc_ecma_ast::Expr::Lit(swc_ecma_ast::Lit::Str(str)) = (*expr).as_ref() { - if inter_element_whitespace(str.value.as_ref()) { - stash = true; - } - } - } - } - - if stash { - if seen { - queue.push(child); - } - } else { - if !queue.is_empty() { - nodes.append(&mut queue); - } - nodes.push(child); - seen = true; - } - } - - Ok(Some(swc_ecma_ast::JSXElementChild::JSXFragment( - create_fragment(nodes, node), - ))) -} - -/// [`Text`][hast::Text]. -fn transform_text( - _context: &mut Context, - node: &hast::Node, - text: &hast::Text, -) -> Option { - 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 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)), - } -} - -fn inter_element_whitespace(value: &str) -> bool { - let bytes = value.as_bytes(); - let mut index = 0; - - while index < bytes.len() { - match bytes[index] { - b'\t' | 0x0C | b'\r' | b'\n' | b' ' => {} - _ => return false, - } - index += 1; - } - - true -} - -/// Different kinds of JSX names. -enum JsxName<'a> { - // `a.b.c` - Member(Vec<&'a str>), - // `a:b` - Namespace(&'a str, &'a str), - // `a` - Normal(&'a str), -} - -/// Parse a JSX name from a string. -fn parse_jsx_name(name: &str) -> JsxName { - let bytes = name.as_bytes(); - let mut index = 0; - let mut start = 0; - let mut parts = vec![]; - - while index < bytes.len() { - if bytes[index] == b'.' { - parts.push(&name[start..index]); - start = index + 1; - } - - index += 1; - } - - // `` - 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