aboutsummaryrefslogtreecommitdiffstats
path: root/tests/test_utils
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_utils')
-rw-r--r--tests/test_utils/hast.rs508
-rw-r--r--tests/test_utils/hast_util_to_swc.rs781
-rw-r--r--tests/test_utils/mdast_util_to_hast.rs1275
-rw-r--r--tests/test_utils/mdx.rs221
-rw-r--r--tests/test_utils/mdx_plugin_recma_document.rs662
-rw-r--r--tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs1165
-rw-r--r--tests/test_utils/mod.rs6
-rw-r--r--tests/test_utils/swc.rs383
-rw-r--r--tests/test_utils/swc_utils.rs226
9 files changed, 150 insertions, 5077 deletions
diff --git a/tests/test_utils/hast.rs b/tests/test_utils/hast.rs
deleted file mode 100644
index 857df4d..0000000
--- a/tests/test_utils/hast.rs
+++ /dev/null
@@ -1,508 +0,0 @@
-//! HTML syntax tree: [hast][].
-//!
-//! [hast]: https://github.com/syntax-tree/hast
-
-#![allow(dead_code)]
-// ^-- To do: externalize.
-
-extern crate alloc;
-use alloc::{
- fmt,
- string::{String, ToString},
- vec::Vec,
-};
-pub use markdown::mdast::{AttributeContent, AttributeValue, MdxJsxAttribute, Stop};
-use markdown::unist::Position;
-
-/// Nodes.
-#[derive(Clone, PartialEq, Eq)]
-pub enum Node {
- /// Root.
- Root(Root),
- /// Element.
- Element(Element),
- /// Document type.
- Doctype(Doctype),
- /// Comment.
- Comment(Comment),
- /// Text.
- Text(Text),
-
- // MDX being passed through.
- /// MDX: JSX element.
- MdxJsxElement(MdxJsxElement),
- /// MDX.js ESM.
- MdxjsEsm(MdxjsEsm),
- // MDX: expression.
- MdxExpression(MdxExpression),
-}
-
-impl fmt::Debug for Node {
- // Debug the wrapped struct.
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Node::Root(x) => write!(f, "{:?}", x),
- Node::Element(x) => write!(f, "{:?}", x),
- Node::Doctype(x) => write!(f, "{:?}", x),
- Node::Comment(x) => write!(f, "{:?}", x),
- Node::Text(x) => write!(f, "{:?}", x),
- Node::MdxJsxElement(x) => write!(f, "{:?}", x),
- Node::MdxExpression(x) => write!(f, "{:?}", x),
- Node::MdxjsEsm(x) => write!(f, "{:?}", x),
- }
- }
-}
-
-fn children_to_string(children: &[Node]) -> String {
- children.iter().map(ToString::to_string).collect()
-}
-
-impl ToString for Node {
- fn to_string(&self) -> String {
- match self {
- // Parents.
- Node::Root(x) => children_to_string(&x.children),
- Node::Element(x) => children_to_string(&x.children),
- Node::MdxJsxElement(x) => children_to_string(&x.children),
- // Literals.
- Node::Comment(x) => x.value.clone(),
- Node::Text(x) => x.value.clone(),
- Node::MdxExpression(x) => x.value.clone(),
- Node::MdxjsEsm(x) => x.value.clone(),
- // Voids.
- Node::Doctype(_) => "".into(),
- }
- }
-}
-
-impl Node {
- #[must_use]
- pub fn children(&self) -> Option<&Vec<Node>> {
- match self {
- // Parent.
- Node::Root(x) => Some(&x.children),
- Node::Element(x) => Some(&x.children),
- Node::MdxJsxElement(x) => Some(&x.children),
- // Non-parent.
- _ => None,
- }
- }
-
- pub fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
- match self {
- // Parent.
- Node::Root(x) => Some(&mut x.children),
- Node::Element(x) => Some(&mut x.children),
- Node::MdxJsxElement(x) => Some(&mut x.children),
- // Non-parent.
- _ => None,
- }
- }
-
- pub fn position(&self) -> Option<&Position> {
- match self {
- Node::Root(x) => x.position.as_ref(),
- Node::Element(x) => x.position.as_ref(),
- Node::Doctype(x) => x.position.as_ref(),
- Node::Comment(x) => x.position.as_ref(),
- Node::Text(x) => x.position.as_ref(),
- Node::MdxJsxElement(x) => x.position.as_ref(),
- Node::MdxExpression(x) => x.position.as_ref(),
- Node::MdxjsEsm(x) => x.position.as_ref(),
- }
- }
-
- pub fn position_mut(&mut self) -> Option<&mut Position> {
- match self {
- Node::Root(x) => x.position.as_mut(),
- Node::Element(x) => x.position.as_mut(),
- Node::Doctype(x) => x.position.as_mut(),
- Node::Comment(x) => x.position.as_mut(),
- Node::Text(x) => x.position.as_mut(),
- Node::MdxJsxElement(x) => x.position.as_mut(),
- Node::MdxExpression(x) => x.position.as_mut(),
- Node::MdxjsEsm(x) => x.position.as_mut(),
- }
- }
-
- pub fn position_set(&mut self, position: Option<Position>) {
- match self {
- Node::Root(x) => x.position = position,
- Node::Element(x) => x.position = position,
- Node::Doctype(x) => x.position = position,
- Node::Comment(x) => x.position = position,
- Node::Text(x) => x.position = position,
- Node::MdxJsxElement(x) => x.position = position,
- Node::MdxExpression(x) => x.position = position,
- Node::MdxjsEsm(x) => x.position = position,
- }
- }
-}
-
-/// Document.
-///
-/// ```html
-/// > | a
-/// ^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Root {
- // Parent.
- /// Content model.
- pub children: Vec<Node>,
- /// Positional info.
- pub position: Option<Position>,
-}
-
-/// Document type.
-///
-/// ```html
-/// > | <!doctype html>
-/// ^^^^^^^^^^^^^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Element {
- pub tag_name: String,
- pub properties: Vec<(String, PropertyValue)>,
- // Parent.
- pub children: Vec<Node>,
- /// Positional info.
- pub position: Option<Position>,
-}
-
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub enum PropertyValue {
- Boolean(bool),
- String(String),
- CommaSeparated(Vec<String>),
- SpaceSeparated(Vec<String>),
-}
-
-/// Document type.
-///
-/// ```html
-/// > | <!doctype html>
-/// ^^^^^^^^^^^^^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Doctype {
- // Void.
- /// Positional info.
- pub position: Option<Position>,
-}
-
-/// Comment.
-///
-/// ```html
-/// > | <!-- a -->
-/// ^^^^^^^^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Comment {
- // Text.
- /// Content model.
- pub value: String,
- /// Positional info.
- pub position: Option<Position>,
-}
-
-/// Text.
-///
-/// ```html
-/// > | a
-/// ^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct Text {
- // Text.
- /// Content model.
- pub value: String,
- /// Positional info.
- pub position: Option<Position>,
-}
-
-/// MDX: JSX element.
-///
-/// ```markdown
-/// > | <a />
-/// ^^^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct MdxJsxElement {
- // JSX element.
- /// Name.
- ///
- /// Fragments have no name.
- pub name: Option<String>,
- /// Attributes.
- pub attributes: Vec<AttributeContent>,
- // Parent.
- /// Content model.
- pub children: Vec<Node>,
- /// Positional info.
- pub position: Option<Position>,
-}
-
-/// MDX: expression.
-///
-/// ```markdown
-/// > | {a}
-/// ^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct MdxExpression {
- // Literal.
- /// Content model.
- pub value: String,
- /// Positional info.
- pub position: Option<Position>,
-
- // Custom data on where each slice of `value` came from.
- pub stops: Vec<Stop>,
-}
-
-/// MDX: ESM.
-///
-/// ```markdown
-/// > | import a from 'b'
-/// ^^^^^^^^^^^^^^^^^
-/// ```
-#[derive(Clone, Debug, PartialEq, Eq)]
-pub struct MdxjsEsm {
- // Literal.
- /// Content model.
- pub value: String,
- /// Positional info.
- pub position: Option<Position>,
-
- // Custom data on where each slice of `value` came from.
- pub stops: Vec<Stop>,
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use alloc::{format, string::ToString, vec};
- use markdown::unist::Position;
-
- // Literals.
-
- #[test]
- fn text() {
- let mut node = Node::Text(Text {
- value: "a".into(),
- position: None,
- });
-
- assert_eq!(
- format!("{:?}", node),
- "Text { value: \"a\", position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "a", "should support `ToString`");
- assert_eq!(node.children_mut(), None, "should support `children_mut`");
- assert_eq!(node.children(), None, "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "Text { value: \"a\", position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-
- #[test]
- fn comment() {
- let mut node = Node::Comment(Comment {
- value: "a".into(),
- position: None,
- });
-
- assert_eq!(
- format!("{:?}", node),
- "Comment { value: \"a\", position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "a", "should support `ToString`");
- assert_eq!(node.children_mut(), None, "should support `children_mut`");
- assert_eq!(node.children(), None, "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "Comment { value: \"a\", position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-
- #[test]
- fn mdx_expression() {
- let mut node = Node::MdxExpression(MdxExpression {
- value: "a".into(),
- stops: vec![],
- position: None,
- });
-
- assert_eq!(
- format!("{:?}", node),
- "MdxExpression { value: \"a\", position: None, stops: [] }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "a", "should support `ToString`");
- assert_eq!(node.children_mut(), None, "should support `children_mut`");
- assert_eq!(node.children(), None, "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "MdxExpression { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }",
- "should support `position_set`"
- );
- }
-
- #[test]
- fn mdxjs_esm() {
- let mut node = Node::MdxjsEsm(MdxjsEsm {
- value: "a".into(),
- stops: vec![],
- position: None,
- });
-
- assert_eq!(
- format!("{:?}", node),
- "MdxjsEsm { value: \"a\", position: None, stops: [] }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "a", "should support `ToString`");
- assert_eq!(node.children_mut(), None, "should support `children_mut`");
- assert_eq!(node.children(), None, "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "MdxjsEsm { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }",
- "should support `position_set`"
- );
- }
-
- // Voids.
-
- #[test]
- fn doctype() {
- let mut node = Node::Doctype(Doctype { position: None });
-
- assert_eq!(
- format!("{:?}", node),
- "Doctype { position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "", "should support `ToString`");
- assert_eq!(node.children_mut(), None, "should support `children_mut`");
- assert_eq!(node.children(), None, "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "Doctype { position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-
- // Parents.
-
- #[test]
- fn root() {
- let mut node = Node::Root(Root {
- position: None,
- children: vec![],
- });
-
- assert_eq!(
- format!("{:?}", node),
- "Root { children: [], position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "", "should support `ToString`");
- assert_eq!(
- node.children_mut(),
- Some(&mut vec![]),
- "should support `children_mut`"
- );
- assert_eq!(node.children(), Some(&vec![]), "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "Root { children: [], position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-
- #[test]
- fn element() {
- let mut node = Node::Element(Element {
- tag_name: "a".into(),
- properties: vec![],
- position: None,
- children: vec![],
- });
-
- assert_eq!(
- format!("{:?}", node),
- "Element { tag_name: \"a\", properties: [], children: [], position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "", "should support `ToString`");
- assert_eq!(
- node.children_mut(),
- Some(&mut vec![]),
- "should support `children_mut`"
- );
- assert_eq!(node.children(), Some(&vec![]), "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "Element { tag_name: \"a\", properties: [], children: [], position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-
- #[test]
- fn mdx_jsx_element() {
- let mut node = Node::MdxJsxElement(MdxJsxElement {
- name: None,
- attributes: vec![],
- position: None,
- children: vec![],
- });
-
- assert_eq!(
- format!("{:?}", node),
- "MdxJsxElement { name: None, attributes: [], children: [], position: None }",
- "should support `Debug`"
- );
- assert_eq!(node.to_string(), "", "should support `ToString`");
- assert_eq!(
- node.children_mut(),
- Some(&mut vec![]),
- "should support `children_mut`"
- );
- assert_eq!(node.children(), Some(&vec![]), "should support `children`");
- assert_eq!(node.position(), None, "should support `position`");
- assert_eq!(node.position_mut(), None, "should support `position`");
- node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1)));
- assert_eq!(
- format!("{:?}", node),
- "MdxJsxElement { name: None, attributes: [], children: [], position: Some(1:1-1:2 (0-1)) }",
- "should support `position_set`"
- );
- }
-}
diff --git a/tests/test_utils/hast_util_to_swc.rs b/tests/test_utils/hast_util_to_swc.rs
deleted file mode 100644
index ea7ffd1..0000000
--- a/tests/test_utils/hast_util_to_swc.rs
+++ /dev/null
@@ -1,781 +0,0 @@
-//! Turn an HTML AST into a JavaScript AST.
-//!
-//! Port of <https://github.com/syntax-tree/hast-util-to-estree>, by the same
-//! author:
-//!
-//! (The MIT License)
-//!
-//! Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
-//!
-//! 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,
- swc::{parse_esm_to_tree, parse_expression_to_tree},
- swc_utils::{create_ident, position_to_span},
-};
-use core::str;
-use markdown::{Location, MdxExpressionKind};
-
-/// Result.
-#[derive(Debug, PartialEq, Eq)]
-pub struct Program {
- pub path: Option<String>,
- /// JS AST.
- pub module: swc_ecma_ast::Module,
- /// Comments relating to AST.
- pub comments: Vec<swc_common::comments::Comment>,
-}
-
-/// Whether we’re in HTML or SVG.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-enum Space {
- Html,
- Svg,
-}
-
-#[derive(Debug)]
-struct Context<'a> {
- /// Whether we’re in HTML or SVG.
- ///
- /// Not used yet, likely useful in the future.
- space: Space,
- /// Comments we gather.
- comments: Vec<swc_common::comments::Comment>,
- /// Declarations and stuff.
- esm: Vec<swc_ecma_ast::ModuleItem>,
- /// 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<String>,
- location: Option<&Location>,
-) -> Result<Program, String> {
- let mut context = Context {
- space: Space::Html,
- comments: vec![],
- esm: vec![],
- location,
- };
- let expr = match one(&mut context, tree)? {
- Some(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<Option<swc_ecma_ast::JSXElementChild>, String> {
- let value = match node {
- hast::Node::Comment(x) => Some(transform_comment(context, node, x)),
- hast::Node::Element(x) => transform_element(context, node, x)?,
- hast::Node::MdxJsxElement(x) => transform_mdx_jsx_element(context, node, x)?,
- hast::Node::MdxExpression(x) => transform_mdx_expression(context, node, x)?,
- hast::Node::MdxjsEsm(x) => transform_mdxjs_esm(context, node, x)?,
- hast::Node::Root(x) => transform_root(context, node, x)?,
- hast::Node::Text(x) => transform_text(context, node, x),
- // Ignore:
- hast::Node::Doctype(_) => None,
- };
- Ok(value)
-}
-
-/// Transform children of `parent`.
-fn all(
- context: &mut Context,
- parent: &hast::Node,
-) -> Result<Vec<swc_ecma_ast::JSXElementChild>, String> {
- let mut result = vec![];
- if let Some(children) = parent.children() {
- let mut index = 0;
- while index < children.len() {
- let child = &children[index];
- // To do: remove line endings between table elements?
- // <https://github.com/syntax-tree/hast-util-to-estree/blob/6c45f166d106ea3a165c14ec50c35ed190055e65/lib/index.js>
- if let Some(child) = one(context, child)? {
- result.push(child);
- }
- index += 1;
- }
- }
-
- Ok(result)
-}
-
-/// [`Comment`][hast::Comment].
-fn transform_comment(
- context: &mut Context,
- node: &hast::Node,
- comment: &hast::Comment,
-) -> swc_ecma_ast::JSXElementChild {
- context.comments.push(swc_common::comments::Comment {
- kind: swc_common::comments::CommentKind::Block,
- text: comment.value.clone().into(),
- span: position_to_span(node.position()),
- });
-
- // Might be useless.
- // Might be useful when transforming to acorn/babel later.
- // This is done in the JS version too:
- // <https://github.com/syntax-tree/hast-util-to-estree/blob/6c45f166d106ea3a165c14ec50c35ed190055e65/lib/index.js#L168>
- swc_ecma_ast::JSXElementChild::JSXExprContainer(swc_ecma_ast::JSXExprContainer {
- expr: swc_ecma_ast::JSXExpr::JSXEmptyExpr(swc_ecma_ast::JSXEmptyExpr {
- span: position_to_span(node.position()),
- }),
- span: position_to_span(node.position()),
- })
-}
-
-/// [`Element`][hast::Element].
-fn transform_element(
- context: &mut Context,
- node: &hast::Node,
- element: &hast::Element,
-) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> {
- let space = context.space;
-
- if space == Space::Html && element.tag_name == "svg" {
- context.space = Space::Svg;
- }
-
- let children = all(context, node)?;
-
- context.space = space;
-
- let mut attrs = vec![];
-
- let mut index = 0;
- while index < element.properties.len() {
- let prop = &element.properties[index];
-
- // To do: turn style props into objects.
- let value = match &prop.1 {
- hast::PropertyValue::Boolean(x) => {
- // No value is same as `{true}` / Ignore `false`.
- if *x {
- None
- } else {
- index += 1;
- continue;
- }
- }
- hast::PropertyValue::String(x) => Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
- value: x.clone().into(),
- span: swc_common::DUMMY_SP,
- raw: None,
- })),
- hast::PropertyValue::CommaSeparated(x) => {
- Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
- value: x.join(", ").into(),
- span: swc_common::DUMMY_SP,
- raw: None,
- }))
- }
- hast::PropertyValue::SpaceSeparated(x) => {
- Some(swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
- value: x.join(" ").into(),
- span: swc_common::DUMMY_SP,
- raw: None,
- }))
- }
- };
-
- // Turn property case into either React-specific case, or HTML
- // attribute case.
- // To do: create a spread if this is an invalid attr name.
- let attr_name = prop_to_attr_name(&prop.0);
-
- attrs.push(swc_ecma_ast::JSXAttrOrSpread::JSXAttr(
- swc_ecma_ast::JSXAttr {
- name: create_jsx_attr_name(&attr_name),
- value: value.map(swc_ecma_ast::JSXAttrValue::Lit),
- span: swc_common::DUMMY_SP,
- },
- ));
-
- index += 1;
- }
-
- Ok(Some(swc_ecma_ast::JSXElementChild::JSXElement(
- create_element(&element.tag_name, attrs, children, node),
- )))
-}
-
-/// [`MdxJsxElement`][hast::MdxJsxElement].
-fn transform_mdx_jsx_element(
- context: &mut Context,
- node: &hast::Node,
- element: &hast::MdxJsxElement,
-) -> Result<Option<swc_ecma_ast::JSXElementChild>, String> {
- let space = context.space;
-
- if let Some(name) = &element.name {
- if space == Space::Html && name == "svg" {
- context.space = Space::Svg;
- }
- }
-
- let children = all(context, node)?;
-
- context.space = space;
-
- let mut attrs = vec![];
- let mut index = 0;
-
- while index < element.attributes.len() {
- let attr = match &element.attributes[index] {
- hast::AttributeContent::Property(prop) => {
- let value = match prop.value.as_ref() {
- Some(hast::AttributeValue::Literal(x)) => {
- Some(swc_ecma_ast::JSXAttrValue::Lit(swc_ecma_ast::Lit::Str(
- swc_ecma_ast::Str {
- value: x.clone().into(),
- span: swc_common::DUMMY_SP,
- raw: None,
- },
- )))
- }
- Some(hast::AttributeValue::Expression(value, 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<Option<swc_ecma_ast::JSXElementChild>, String> {
- Ok(Some(swc_ecma_ast::JSXElementChild::JSXExprContainer(
- swc_ecma_ast::JSXExprContainer {
- expr: swc_ecma_ast::JSXExpr::Expr(parse_expression_to_tree(
- &expression.value,
- &MdxExpressionKind::Expression,
- &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<Option<swc_ecma_ast::JSXElementChild>, 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<Option<swc_ecma_ast::JSXElementChild>, 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<swc_ecma_ast::JSXElementChild> {
- if text.value.is_empty() {
- None
- } else {
- Some(swc_ecma_ast::JSXElementChild::JSXExprContainer(
- swc_ecma_ast::JSXExprContainer {
- expr: swc_ecma_ast::JSXExpr::Expr(Box::new(swc_ecma_ast::Expr::Lit(
- swc_ecma_ast::Lit::Str(swc_ecma_ast::Str {
- value: text.value.clone().into(),
- span: position_to_span(node.position()),
- raw: None,
- }),
- ))),
- span: position_to_span(node.position()),
- },
- ))
- }
-}
-
-/// Create an element.
-///
-/// Creates a void one if there are no children.
-fn create_element(
- name: &str,
- attrs: Vec<swc_ecma_ast::JSXAttrOrSpread>,
- children: Vec<swc_ecma_ast::JSXElementChild>,
- node: &hast::Node,
-) -> Box<swc_ecma_ast::JSXElement> {
- Box::new(swc_ecma_ast::JSXElement {
- opening: swc_ecma_ast::JSXOpeningElement {
- name: create_jsx_name(name),
- attrs,
- self_closing: children.is_empty(),
- type_args: None,
- span: swc_common::DUMMY_SP,
- },
- closing: if children.is_empty() {
- None
- } else {
- Some(swc_ecma_ast::JSXClosingElement {
- name: create_jsx_name(name),
- span: swc_common::DUMMY_SP,
- })
- },
- children,
- span: position_to_span(node.position()),
- })
-}
-
-/// Create a fragment.
-fn create_fragment(
- children: Vec<swc_ecma_ast::JSXElementChild>,
- node: &hast::Node,
-) -> swc_ecma_ast::JSXFragment {
- swc_ecma_ast::JSXFragment {
- opening: swc_ecma_ast::JSXOpeningFragment {
- span: swc_common::DUMMY_SP,
- },
- closing: swc_ecma_ast::JSXClosingFragment {
- span: swc_common::DUMMY_SP,
- },
- children,
- span: position_to_span(node.position()),
- }
-}
-
-/// Create a JSX element name.
-fn create_jsx_name(name: &str) -> swc_ecma_ast::JSXElementName {
- match parse_jsx_name(name) {
- // `<a.b.c />`
- // `<a.b />`
- JsxName::Member(parts) => {
- // Always two or more items.
- let mut member = swc_ecma_ast::JSXMemberExpr {
- obj: swc_ecma_ast::JSXObject::Ident(create_ident(parts[0])),
- prop: create_ident(parts[1]),
- };
- let mut index = 2;
- while index < parts.len() {
- member = swc_ecma_ast::JSXMemberExpr {
- obj: swc_ecma_ast::JSXObject::JSXMemberExpr(Box::new(member)),
- prop: create_ident(parts[index]),
- };
- index += 1;
- }
- swc_ecma_ast::JSXElementName::JSXMemberExpr(member)
- }
- // `<a:b />`
- JsxName::Namespace(ns, name) => {
- swc_ecma_ast::JSXElementName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName {
- ns: create_ident(ns),
- name: create_ident(name),
- })
- }
- // `<a />`
- JsxName::Normal(name) => swc_ecma_ast::JSXElementName::Ident(create_ident(name)),
- }
-}
-
-/// Create a JSX attribute name.
-fn create_jsx_attr_name(name: &str) -> swc_ecma_ast::JSXAttrName {
- match parse_jsx_name(name) {
- JsxName::Member(_) => {
- unreachable!("member expressions in attribute names are not supported")
- }
- // `<a b:c />`
- JsxName::Namespace(ns, name) => {
- swc_ecma_ast::JSXAttrName::JSXNamespacedName(swc_ecma_ast::JSXNamespacedName {
- ns: create_ident(ns),
- name: create_ident(name),
- })
- }
- // `<a b />`
- JsxName::Normal(name) => swc_ecma_ast::JSXAttrName::Ident(create_ident(name)),
- }
-}
-
-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;
- }
-
- // `<a.b.c />`
- if !parts.is_empty() {
- parts.push(&name[start..]);
- JsxName::Member(parts)
- }
- // `<a:b />`
- else if let Some(colon) = bytes.iter().position(|d| matches!(d, b':')) {
- JsxName::Namespace(&name[0..colon], &name[(colon + 1)..])
- }
- // `<a />`
- else {
- JsxName::Normal(name)
- }
-}
-
-/// Turn a hast property into something that particularly React understands.
-fn prop_to_attr_name(prop: &str) -> String {
- // Arbitrary data props, kebab case them.
- if prop.len() > 4 && prop.starts_with("data") {
- // Assume like two dashes maybe?
- let mut result = String::with_capacity(prop.len() + 2);
- let bytes = prop.as_bytes();
- let mut index = 4;
- let mut start = index;
- let mut valid = true;
-
- result.push_str("data");
-
- while index < bytes.len() {
- let byte = bytes[index];
- let mut dash = index == 4;
-
- match byte {
- b'A'..=b'Z' => dash = true,
- b'-' | b'.' | b':' | b'0'..=b'9' | b'a'..=b'z' => {}
- _ => {
- valid = false;
- break;
- }
- }
-
- if dash {
- if start != index {
- result.push_str(&prop[start..index]);
- }
- result.push('-');
- result.push(byte.to_ascii_lowercase().into());
- start = index + 1;
- }
-
- index += 1;
- }
-
- if valid {
- result.push_str(&prop[start..]);
- return result;
- }
- }
-
- // Look up if prop differs from attribute case.
- // Unknown things are passed through.
- PROP_TO_REACT_PROP
- .iter()
- .find(|d| d.0 == prop)
- .or_else(|| PROP_TO_ATTR_EXCEPTIONS_SHARED.iter().find(|d| d.0 == prop))
- .map(|d| d.1.into())
- .unwrap_or_else(|| prop.into())
-}
-
-// Below data is generated with:
-//
-// Note: there are currently no HTML and SVG specific exceptions.
-// If those would start appearing, the logic that uses these lists needs
-// To support spaces.
-//
-// ```js
-// import * as x from "property-information";
-//
-// /** @type {Record<string, string>} */
-// let shared = {};
-// /** @type {Record<string, string>} */
-// let html = {};
-// /** @type {Record<string, string>} */
-// let svg = {};
-//
-// Object.keys(x.html.property).forEach((prop) => {
-// let attr = x.html.property[prop].attribute;
-// if (!x.html.property[prop].space && prop !== attr) {
-// html[prop] = attr;
-// }
-// });
-//
-// Object.keys(x.svg.property).forEach((prop) => {
-// let attr = x.svg.property[prop].attribute;
-// if (!x.svg.property[prop].space && prop !== attr) {
-// // Shared.
-// if (prop in html && html[prop] === attr) {
-// shared[prop] = attr;
-// delete html[prop];
-// } else {
-// svg[prop] = attr;
-// }
-// }
-// });
-//
-// /** @type {Array<[string, Array<[string, string]>]>} */
-// const all = [
-// ["PROP_TO_REACT_PROP", Object.entries(x.hastToReact)],
-// ["PROP_TO_ATTR_EXCEPTIONS", Object.entries(shared)],
-// ["PROP_TO_ATTR_EXCEPTIONS_HTML", Object.entries(html)],
-// ["PROP_TO_ATTR_EXCEPTIONS_SVG", Object.entries(svg)],
-// ];
-//
-// console.log(
-// all
-// .map((d) => {
-// return `const ${d[0]}: [(&str, &str); ${d[1].length}] = [
-// ${d[1].map((d) => ` ("${d[0]}", "${d[1]}")`).join(",\n")}
-// ];`;
-// })
-// .join("\n\n")
-// );
-// ```
-const PROP_TO_REACT_PROP: [(&str, &str); 17] = [
- ("classId", "classID"),
- ("dataType", "datatype"),
- ("itemId", "itemID"),
- ("strokeDashArray", "strokeDasharray"),
- ("strokeDashOffset", "strokeDashoffset"),
- ("strokeLineCap", "strokeLinecap"),
- ("strokeLineJoin", "strokeLinejoin"),
- ("strokeMiterLimit", "strokeMiterlimit"),
- ("typeOf", "typeof"),
- ("xLinkActuate", "xlinkActuate"),
- ("xLinkArcRole", "xlinkArcrole"),
- ("xLinkHref", "xlinkHref"),
- ("xLinkRole", "xlinkRole"),
- ("xLinkShow", "xlinkShow"),
- ("xLinkTitle", "xlinkTitle"),
- ("xLinkType", "xlinkType"),
- ("xmlnsXLink", "xmlnsXlink"),
-];
-
-const PROP_TO_ATTR_EXCEPTIONS_SHARED: [(&str, &str); 48] = [
- ("ariaActiveDescendant", "aria-activedescendant"),
- ("ariaAtomic", "aria-atomic"),
- ("ariaAutoComplete", "aria-autocomplete"),
- ("ariaBusy", "aria-busy"),
- ("ariaChecked", "aria-checked"),
- ("ariaColCount", "aria-colcount"),
- ("ariaColIndex", "aria-colindex"),
- ("ariaColSpan", "aria-colspan"),
- ("ariaControls", "aria-controls"),
- ("ariaCurrent", "aria-current"),
- ("ariaDescribedBy", "aria-describedby"),
- ("ariaDetails", "aria-details"),
- ("ariaDisabled", "aria-disabled"),
- ("ariaDropEffect", "aria-dropeffect"),
- ("ariaErrorMessage", "aria-errormessage"),
- ("ariaExpanded", "aria-expanded"),
- ("ariaFlowTo", "aria-flowto"),
- ("ariaGrabbed", "aria-grabbed"),
- ("ariaHasPopup", "aria-haspopup"),
- ("ariaHidden", "aria-hidden"),
- ("ariaInvalid", "aria-invalid"),
- ("ariaKeyShortcuts", "aria-keyshortcuts"),
- ("ariaLabel", "aria-label"),
- ("ariaLabelledBy", "aria-labelledby"),
- ("ariaLevel", "aria-level"),
- ("ariaLive", "aria-live"),
- ("ariaModal", "aria-modal"),
- ("ariaMultiLine", "aria-multiline"),
- ("ariaMultiSelectable", "aria-multiselectable"),
- ("ariaOrientation", "aria-orientation"),
- ("ariaOwns", "aria-owns"),
- ("ariaPlaceholder", "aria-placeholder"),
- ("ariaPosInSet", "aria-posinset"),
- ("ariaPressed", "aria-pressed"),
- ("ariaReadOnly", "aria-readonly"),
- ("ariaRelevant", "aria-relevant"),
- ("ariaRequired", "aria-required"),
- ("ariaRoleDescription", "aria-roledescription"),
- ("ariaRowCount", "aria-rowcount"),
- ("ariaRowIndex", "aria-rowindex"),
- ("ariaRowSpan", "aria-rowspan"),
- ("ariaSelected", "aria-selected"),
- ("ariaSetSize", "aria-setsize"),
- ("ariaSort", "aria-sort"),
- ("ariaValueMax", "aria-valuemax"),
- ("ariaValueMin", "aria-valuemin"),
- ("ariaValueNow", "aria-valuenow"),
- ("ariaValueText", "aria-valuetext"),
-];
diff --git a/tests/test_utils/mdast_util_to_hast.rs b/tests/test_utils/mdast_util_to_hast.rs
deleted file mode 100644
index 29d9489..0000000
--- a/tests/test_utils/mdast_util_to_hast.rs
+++ /dev/null
@@ -1,1275 +0,0 @@
-//! Turn a markdown AST into an HTML AST.
-//!
-//! Port of <https://github.com/syntax-tree/mdast-util-to-hast>, by the same
-//! author:
-//!
-//! (The MIT License)
-//!
-//! Copyright (c) 2016 Titus Wormer <tituswormer@gmail.com>
-//!
-//! 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 markdown::{mdast, sanitize, unist::Position};
-
-// To do: support these compile options:
-// ```
-// pub gfm_footnote_label: Option<String>,
-// pub gfm_footnote_label_tag_name: Option<String>,
-// pub gfm_footnote_label_attributes: Option<String>,
-// pub gfm_footnote_back_label: Option<String>,
-// pub gfm_footnote_clobber_prefix: Option<String>,
-// ```
-//
-// 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?
-// <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a/lib/revert.js>
-
-#[derive(Debug)]
-struct State {
- definitions: Vec<(String, String, Option<String>)>,
- footnote_definitions: Vec<(String, Vec<hast::Node>)>,
- footnote_calls: Vec<(String, usize)>,
-}
-
-#[derive(Debug)]
-enum Result {
- Fragment(Vec<hast::Node>),
- 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: <https://github.com/sindresorhus/github-markdown-css>.
- 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<hast::Node> {
- 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<hast::Node>, loose: bool) -> Vec<hast::Node> {
- 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<Visitor>(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<Visitor>(node: &mut mdast::Node, visitor: Visitor)
-where
- Visitor: FnMut(&mut mdast::Node),
-{
- visit_mut_impl(node, visitor);
-}
-
-/// Internal implementation to visit.
-fn visit_impl<Visitor>(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<Visitor>(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: <https://github.com/syntax-tree/mdast-util-to-hast/blob/c393d0a/lib/traverse.js>.
-/// Append an (optional, variadic) result.
-fn append_result(list: &mut Vec<hast::Node>, 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.rs b/tests/test_utils/mdx.rs
deleted file mode 100644
index 4481b78..0000000
--- a/tests/test_utils/mdx.rs
+++ /dev/null
@@ -1,221 +0,0 @@
-use crate::test_utils::{
- hast_util_to_swc::hast_util_to_swc,
- mdast_util_to_hast::mdast_util_to_hast,
- mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions},
- mdx_plugin_recma_jsx_rewrite::{mdx_plugin_recma_jsx_rewrite, Options as RewriteOptions},
- swc::{parse_esm, parse_expression, serialize},
-};
-use markdown::{to_mdast, Constructs, Location, ParseOptions};
-
-pub use super::mdx_plugin_recma_document::JsxRuntime;
-
-use swc_common::comments::{Comments, SingleThreadedComments};
-// use swc_ecma_transforms_react::{jsx, Options as JsxBuildOptions};
-// use swc_ecma_visit::VisitMutWith;
-
-// To do: use `Format`.
-/// Format the file is in (default: `Format::Detect`).
-#[allow(dead_code)]
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub enum Format {
- /// Use `Format::Markdown` for files with an extension in `md_extensions`
- /// and `Format::Mdx` otherwise.
- #[default]
- Detect,
- /// Treat file as MDX.
- Mdx,
- /// Treat file as plain vanilla markdown.
- Markdown,
-}
-
-// To do: use `OutputFormat`.
-/// Output format to generate (default: `OutputFormat::Program`).
-#[allow(dead_code)]
-#[derive(Clone, Debug, Default, Eq, PartialEq)]
-pub enum OutputFormat {
- /// The `Program` format will use import statements to import the JSX
- /// runtime (and optionally provider) and use an export statement to yield
- /// the `MDXContent` component.
- #[default]
- Program,
- /// The `FunctionBody` format will get the JSX runtime (and optionally
- /// provider) from `arguments[0]`, rewrite export statements, and use a
- /// return statement to yield what was exported.
- /// Normally, this output format will throw on `import` (and
- /// `export … from`) statements, but you can support them by setting
- /// `options.useDynamicImport`.
- FunctionBody,
-}
-
-/// Configuration (optional).
-#[derive(Clone, Debug, Default)]
-pub struct Options {
- // /// List of markdown extensions, with dot.
- // ///
- // /// Default: `vec![".md".into(), ".markdown".into(), ".mdown".into(), ".mkdn".into(), ".mkd".into(), ".mdwn".into(), ".mkdown".into(), ".ron".into()]`.
- // pub md_extensions: Option<Vec<String>>,
- // /// List of MDX extensions, with dot.
- // ///
- // /// Default: `vec![".mdx".into()]`.
- // pub mdx_extensions: Option<Vec<String>>,
- // /// Format the file is in (default: `Format::Detect`).
- // pub format: Format,
- // /// To do: support `output_format: FunctionBody
- // /// Output format to generate (default: `OutputFormat::Program`).
- // ///
- // /// In most cases `OutputFormat::Program` should be used, as it results in a
- // /// whole program.
- // /// `OutputFormat::FunctionBody` can be used to compile to code that can be
- // /// `eval`ed.
- // /// In some cases, you might want to do that, such as when compiling on the
- // /// server and running on the client.
- // pub output_format: OutputFormat,
- // /// Whether to compile to dynamic import expressions (default:
- // /// `false`).
- // ///
- // /// This option applies when `options.output_format` is
- // /// `OutputFormat::FunctionBody`.
- // ///
- // /// This project can turn import statements (`import x from 'y'`) into
- // /// dynamic imports (`const {x} = await import('y')`).
- // /// This is useful because import statements only work at the top level of
- // /// JavaScript modules, whereas `import()` is available inside function
- // /// bodies.
- // ///
- // /// When you turn `use_dynamic_import` on, you should probably set
- // /// `options.base_url` too.
- // pub use_dynamic_import: bool,
- // /// Resolve `import`s (and `export … from`, `import.meta.url`) from this
- // /// URL (default: `None`, example: `Some("https://example.com/".into())`).
- // ///
- // /// Relative specifiers are non-absolute URLs that start with `/`, `./`, or
- // /// `../`.
- // /// For example: `/index.js`, `./folder/file.js`, or `../main.js`.
- // ///
- // /// This option is useful when code will run in a different place.
- // /// One example is when `.mdx` files are in path *a* but compiled to path
- // /// *b* and imports should run relative the path *b*.
- // /// Another example is when evaluating code, whether in Node or a browser.
- // pub base_url: Option<String>,
- /// Whether to add extra information to error messages in generated code
- /// (default: `false`).
- pub development: bool,
-
- // To do: some alternative to generate source maps.
- // SourceMapGenerator
- /// Place to import a provider from (default: `None`, example:
- /// `Some("@mdx-js/react").into()`).
- ///
- /// Useful for runtimes that support context (React, Preact).
- /// The provider must export a `useMDXComponents`, which is called to
- /// access an object of components.
- pub provider_import_source: Option<String>,
-
- /// Whether to keep JSX (default: `false`).
- ///
- /// The default is to compile JSX away so that the resulting file is
- /// immediately runnable.
- pub jsx: bool,
-
- /// JSX runtime to use (default: `Some(JsxRuntime::Automatic)`).
- ///
- /// The classic runtime compiles to calls such as `h('p')`, the automatic
- /// runtime compiles to
- /// `import _jsx from '$importSource/jsx-runtime'\n_jsx('p')`.
- pub jsx_runtime: Option<JsxRuntime>,
-
- /// Place to import automatic JSX runtimes from (`Option<String>`, default:
- /// `Some("react".into())`).
- ///
- /// When in the automatic runtime, this is used to define an import for
- /// `_Fragment`, `_jsx`, and `_jsxs`.
- pub jsx_import_source: Option<String>,
-
- /// Pragma for JSX (default: `Some("React.createElement".into())`).
- ///
- /// When in the classic runtime, this is used as an identifier for function
- /// calls: `<x />` to `React.createElement('x')`.
- ///
- /// You should most probably define `pragma_frag` and `pragma_import_source`
- /// too when changing this.
- pub pragma: Option<String>,
-
- /// Pragma for JSX fragments (default: `Some("React.Fragment".into())`).
- ///
- /// When in the classic runtime, this is used as an identifier for
- /// fragments: `<>` to `React.createElement(React.Fragment)`.
- ///
- /// You should most probably define `pragma` and `pragma_import_source`
- /// too when changing this.
- pub pragma_frag: Option<String>,
-
- /// Where to import the identifier of `pragma` from (default:
- /// `Some("react".into())`).
- ///
- /// When in the classic runtime, this is used to import the `pragma`
- /// function.
- /// To illustrate with an example: when `pragma` is `"a.b"` and
- /// `pragma_import_source` is `"c"`, the following will be generated:
- /// `import a from 'c'`.
- pub pragma_import_source: Option<String>,
-}
-
-#[allow(dead_code)]
-pub fn mdx(value: &str, filepath: Option<String>, options: &Options) -> Result<String, String> {
- let parse_options = ParseOptions {
- constructs: Constructs::mdx(),
- mdx_esm_parse: Some(Box::new(parse_esm)),
- mdx_expression_parse: Some(Box::new(parse_expression)),
- ..ParseOptions::default()
- };
- let document_options = DocumentOptions {
- pragma: options.pragma.clone(),
- pragma_frag: options.pragma_frag.clone(),
- pragma_import_source: options.pragma_import_source.clone(),
- jsx_import_source: options.jsx_import_source.clone(),
- jsx_runtime: options.jsx_runtime,
- };
- let rewrite_options = RewriteOptions {
- development: options.development,
- provider_import_source: options.provider_import_source.clone(),
- };
-
- let location = Location::new(value.as_bytes());
- let mdast = to_mdast(value, &parse_options)?;
- let hast = mdast_util_to_hast(&mdast);
- let mut program = hast_util_to_swc(&hast, filepath, Some(&location))?;
- mdx_plugin_recma_document(&mut program, &document_options, Some(&location))?;
- mdx_plugin_recma_jsx_rewrite(&mut program, &rewrite_options, Some(&location));
-
- // Add our comments.
- let comments = SingleThreadedComments::default();
-
- for c in program.comments {
- comments.add_leading(c.span.lo, c);
- }
-
- println!("comments for swc: {:?}", comments);
-
- if !options.jsx {
- // let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
- // let build_options = JsxBuildOptions {
- // next: Some(true),
- // throw_if_namespace: Some(false),
- // development: Some(options.development),
- // use_spread: Some(true),
- // refresh: None,
- // runtime: None,
- // import_source: None,
- // pragma: None,
- // pragma_frag: None,
- // use_builtins: None,
- // };
- // let mark = Mark::fresh(Mark::default());
- // program
- // .module
- // .visit_mut_with(&mut jsx(cm, Some(comments), build_options, mark));
- }
-
- // To do: include comments.
- Ok(serialize(&program.module))
-}
diff --git a/tests/test_utils/mdx_plugin_recma_document.rs b/tests/test_utils/mdx_plugin_recma_document.rs
deleted file mode 100644
index 48648b9..0000000
--- a/tests/test_utils/mdx_plugin_recma_document.rs
+++ /dev/null
@@ -1,662 +0,0 @@
-//! Turn a JavaScript AST, coming from MD(X), into a component.
-//!
-//! Port of <https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/recma-document.js>,
-//! by the same author.
-
-use crate::test_utils::{
- hast_util_to_swc::Program,
- swc_utils::{bytepos_to_point, prefix_error_with_point, span_to_position},
-};
-use markdown::{
- unist::{Point, Position},
- Location,
-};
-
-/// JSX runtimes (default: `JsxRuntime: Automatic`).
-#[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<String>,
- /// Pragma for JSX fragments (used in classic runtime).
- ///
- /// Default: `React.Fragment`.
- pub pragma_frag: Option<String>,
- /// Where to import the identifier of `pragma` from (used in classic runtime).
- ///
- /// Default: `react`.
- pub pragma_import_source: Option<String>,
- /// Place to import automatic JSX runtimes from (used in automatic runtime).
- ///
- /// Default: `react`.
- pub jsx_import_source: Option<String>,
- /// JSX runtime to use.
- ///
- /// Default: `automatic`.
- pub jsx_runtime: Option<JsxRuntime>,
-}
-
-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(
- program: &mut Program,
- options: &Options,
- location: Option<&Location>,
-) -> Result<(), String> {
- // 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(())
-}
-
-/// Create a content component.
-fn create_mdx_content(
- expr: Option<swc_ecma_ast::Expr>,
- has_internal_layout: bool,
-) -> Vec<swc_ecma_ast::ModuleItem> {
- // ```jsx
- // <MDXLayout {...props}>xxx</MDXLayout>
- // ```
- 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 ? <MDXLayout>xxx</MDXLayout> : _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 <MDXLayout>xxx</MDXLayout>
- // }
- // ```
- 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
deleted file mode 100644
index 6b058fd..0000000
--- a/tests/test_utils/mdx_plugin_recma_jsx_rewrite.rs
+++ /dev/null
@@ -1,1165 +0,0 @@
-//! Rewrite JSX tags to accept them from props and an optional provider.
-//!
-//! Port of <https://github.com/mdx-js/mdx/blob/main/packages/mdx/lib/plugin/recma-jsx-rewrite.js>,
-//! by the same author.
-
-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 markdown::{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<String>,
- /// 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(
- program: &mut Program,
- options: &Options,
- location: Option<&Location>,
-) {
- 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));
- }
-}
-
-/// 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<String>,
- /// Used objects (`a` in `<a.b />`).
- objects: Vec<String>,
- /// Used components (`<A />`).
- components: Vec<String>,
- /// Used literals (`<a />`).
- tags: Vec<String>,
- /// 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<Position>)>,
-}
-
-/// Scope (block or function/global).
-#[derive(Debug, Clone)]
-struct Scope {
- /// If this is a function (or global) scope, we track info.
- info: Option<Info>,
- /// Things that are defined in this scope.
- defined: Vec<String>,
-}
-
-/// Context.
-#[derive(Debug, Default, Clone)]
-struct State<'a> {
- location: Option<&'a Location>,
- /// Path to file.
- path: Option<String>,
- /// List of current scopes.
- scopes: Vec<Scope>,
- /// 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<Info>) {
- 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 (`<X>`) but it’s also
- // used as an object (`<X.Y>`).
- 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 {
- // `<x.y>`, `<Foo.Bar>`, `<x.y.z>`.
- 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);
- }
- }
- }
- // `<foo>`, `<Foo>`, `<$>`, `<_bar>`, `<a_b>`.
- 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);
- }
- }
- }
- }
- // `<xml:thing>`.
- 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<String>) -> 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<String>) -> 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/mod.rs b/tests/test_utils/mod.rs
index de203c1..00b4e4e 100644
--- a/tests/test_utils/mod.rs
+++ b/tests/test_utils/mod.rs
@@ -1,8 +1,2 @@
-pub mod hast;
-pub mod hast_util_to_swc;
-pub mod mdast_util_to_hast;
-pub mod mdx;
-pub mod mdx_plugin_recma_document;
-pub mod mdx_plugin_recma_jsx_rewrite;
pub mod swc;
pub mod swc_utils;
diff --git a/tests/test_utils/swc.rs b/tests/test_utils/swc.rs
index f30561d..e85a239 100644
--- a/tests/test_utils/swc.rs
+++ b/tests/test_utils/swc.rs
@@ -1,116 +1,85 @@
//! Bridge between `markdown-rs` and SWC.
-use crate::test_utils::swc_utils::{bytepos_to_point, prefix_error_with_point, RewriteContext};
-use markdown::{mdast::Stop, unist::Point, Location, MdxExpressionKind, MdxSignal};
-use swc_common::{
- source_map::Pos, sync::Lrc, BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Spanned,
+extern crate markdown;
+
+use crate::test_utils::swc_utils::{create_span, RewritePrefixContext};
+use markdown::{MdxExpressionKind, MdxSignal};
+use std::rc::Rc;
+use swc_core::common::{
+ comments::{Comment, SingleThreadedComments, SingleThreadedCommentsMap},
+ source_map::Pos,
+ BytePos, FileName, SourceFile, Span, Spanned,
};
-use swc_ecma_ast::{EsVersion, Expr, Module};
-use swc_ecma_codegen::{text_writer::JsWriter, Emitter};
-use swc_ecma_parser::{
+use swc_core::ecma::ast::{EsVersion, Expr, Module, PropOrSpread};
+use swc_core::ecma::parser::{
error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsConfig, Syntax,
};
-use swc_ecma_visit::VisitMutWith;
+use swc_core::ecma::visit::VisitMutWith;
/// Lex ESM in MDX with SWC.
-#[allow(dead_code)]
pub fn parse_esm(value: &str) -> MdxSignal {
- let (file, syntax, version) = create_config(value.into());
- let mut errors = vec![];
- let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
+ let result = parse_esm_core(value);
match result {
- Err(error) => swc_error_to_signal(&error, "esm", value.len(), 0),
- Ok(tree) => {
- if errors.is_empty() {
- check_esm_ast(&tree)
- } else {
- swc_error_to_signal(&errors[0], "esm", value.len(), 0)
- }
- }
+ Err((span, message)) => swc_error_to_signal(span, &message, value.len()),
+ Ok(_) => MdxSignal::Ok,
}
}
-/// Parse ESM in MDX with SWC.
-/// See `drop_span` in `swc_ecma_utils` for inspiration?
-#[allow(dead_code)]
-pub fn parse_esm_to_tree(
- value: &str,
- stops: &[Stop],
- location: Option<&Location>,
-) -> Result<swc_ecma_ast::Module, String> {
+/// Core to parse ESM.
+fn parse_esm_core(value: &str) -> Result<Module, (Span, String)> {
let (file, syntax, version) = create_config(value.into());
let mut errors = vec![];
let result = parse_file_as_module(&file, syntax, version, None, &mut errors);
- let mut rewrite_context = RewriteContext {
- stops,
- location,
- prefix_len: 0,
- };
match result {
- Err(error) => Err(swc_error_to_error(&error, "esm", &rewrite_context)),
- Ok(mut module) => {
+ Err(error) => Err((
+ fix_span(error.span(), 1),
+ format!(
+ "Could not parse esm with swc: {}",
+ swc_error_to_string(&error)
+ ),
+ )),
+ Ok(module) => {
if errors.is_empty() {
- module.visit_mut_with(&mut rewrite_context);
- Ok(module)
- } else {
- Err(swc_error_to_error(&errors[0], "esm", &rewrite_context))
- }
- }
- }
-}
-
-/// Lex expressions in MDX with SWC.
-#[allow(dead_code)]
-pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal {
- // Empty expressions are OK.
- if matches!(kind, MdxExpressionKind::Expression)
- && matches!(whitespace_and_comments(0, value), MdxSignal::Ok)
- {
- return MdxSignal::Ok;
- }
-
- // For attribute expression, a spread is needed, for which we have to prefix
- // and suffix the input.
- // See `check_expression_ast` for how the AST is verified.
- let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) {
- ("({", "})")
- } else {
- ("", "")
- };
-
- let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix));
- let mut errors = vec![];
- let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
+ let mut index = 0;
+ while index < module.body.len() {
+ let node = &module.body[index];
+
+ if !node.is_module_decl() {
+ return Err((
+ fix_span(node.span(), 1),
+ "Unexpected statement in code: only import/exports are supported"
+ .into(),
+ ));
+ }
- match result {
- Err(error) => swc_error_to_signal(&error, "expression", value.len(), prefix.len()),
- Ok(tree) => {
- if errors.is_empty() {
- let expression_end = fix_swc_position(tree.span().hi.to_usize(), prefix.len());
- let result = check_expression_ast(&tree, kind);
- if matches!(result, MdxSignal::Ok) {
- whitespace_and_comments(expression_end, value)
- } else {
- result
+ index += 1;
}
+
+ Ok(module)
} else {
- swc_error_to_signal(&errors[0], "expression", value.len(), prefix.len())
+ Err((
+ fix_span(errors[0].span(), 1),
+ format!(
+ "Could not parse esm with swc: {}",
+ swc_error_to_string(&errors[0])
+ ),
+ ))
}
}
}
}
-/// Parse ESM in MDX with SWC.
-/// See `drop_span` in `swc_ecma_utils` for inspiration?
-#[allow(dead_code)]
-pub fn parse_expression_to_tree(
+fn parse_expression_core(
value: &str,
kind: &MdxExpressionKind,
- stops: &[Stop],
- location: Option<&Location>,
-) -> Result<Box<swc_ecma_ast::Expr>, String> {
+) -> Result<Option<Box<Expr>>, (Span, String)> {
+ // Empty expressions are OK.
+ if matches!(kind, MdxExpressionKind::Expression) && whitespace_and_comments(0, value).is_ok() {
+ return Ok(None);
+ }
+
// For attribute expression, a spread is needed, for which we have to prefix
// and suffix the input.
// See `check_expression_ast` for how the AST is verified.
@@ -123,185 +92,103 @@ pub fn parse_expression_to_tree(
let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix));
let mut errors = vec![];
let result = parse_file_as_expr(&file, syntax, version, None, &mut errors);
- let mut rewrite_context = RewriteContext {
- stops,
- location,
- prefix_len: prefix.len(),
- };
match result {
- Err(error) => Err(swc_error_to_error(&error, "expression", &rewrite_context)),
+ Err(error) => Err((
+ fix_span(error.span(), prefix.len() + 1),
+ format!(
+ "Could not parse expression with swc: {}",
+ swc_error_to_string(&error)
+ ),
+ )),
Ok(mut expr) => {
if errors.is_empty() {
- // Fix positions.
- expr.visit_mut_with(&mut rewrite_context);
+ let expression_end = expr.span().hi.to_usize() - 1;
+ if let Err((span, reason)) = whitespace_and_comments(expression_end, value) {
+ return Err((span, reason));
+ }
- let expr_bytepos = expr.span().lo;
+ expr.visit_mut_with(&mut RewritePrefixContext {
+ prefix_len: prefix.len() as u32,
+ });
if matches!(kind, MdxExpressionKind::AttributeExpression) {
- let mut obj = None;
+ let expr_span = expr.span();
- if let swc_ecma_ast::Expr::Paren(d) = *expr {
- if let swc_ecma_ast::Expr::Object(d) = *d.expr {
- obj = Some(d)
+ if let Expr::Paren(d) = *expr {
+ if let Expr::Object(mut obj) = *d.expr {
+ if obj.props.len() > 1 {
+ return Err((obj.span, "Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)".into()));
+ }
+
+ if let Some(PropOrSpread::Spread(d)) = obj.props.pop() {
+ return Ok(Some(d.expr));
+ }
}
};
- if let Some(mut obj) = obj {
- if obj.props.len() > 1 {
- Err(create_error_message(
- "Unexpected extra content in spread: only a single spread is supported",
- "expression",
- bytepos_to_point(&obj.span.lo, location).as_ref()
- ))
- } else if let Some(swc_ecma_ast::PropOrSpread::Spread(d)) = obj.props.pop()
- {
- Ok(d.expr)
- } else {
- Err(create_error_message(
- "Unexpected prop in spread: only a spread is supported",
- "expression",
- bytepos_to_point(&obj.span.lo, location).as_ref(),
- ))
- }
- } else {
- Err(create_error_message(
- "Expected an object spread (`{...spread}`)",
- "expression",
- bytepos_to_point(&expr_bytepos, location).as_ref(),
- ))
- }
- } else {
- Ok(expr)
+ return Err((
+ expr_span,
+ "Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)".into(),
+ ));
}
+
+ Ok(Some(expr))
} else {
- Err(swc_error_to_error(
- &errors[0],
- "expression",
- &rewrite_context,
+ Err((
+ fix_span(errors[0].span(), prefix.len() + 1),
+ format!(
+ "Could not parse expression with swc: {}",
+ swc_error_to_string(&errors[0])
+ ),
))
}
}
}
}
-/// Serialize an SWC module.
-/// To do: support comments.
-#[allow(dead_code)]
-pub fn serialize(module: &Module) -> String {
- let mut buf = vec![];
- let cm = Lrc::new(SourceMap::new(FilePathMapping::empty()));
- // let comm = &program.comments as &dyn swc_common::comments::Comments;
- {
- let mut emitter = Emitter {
- cfg: swc_ecma_codegen::Config {
- ..Default::default()
- },
- cm: cm.clone(),
- // To do: figure out how to pass them.
- comments: None,
- wr: JsWriter::new(cm, "\n", &mut buf, None),
- };
-
- emitter.emit_module(module).unwrap();
- }
-
- String::from_utf8_lossy(&buf).into()
-}
-
-/// Check that the resulting AST of ESM is OK.
-///
-/// This checks that only module declarations (import/exports) are used, not
-/// statements.
-fn check_esm_ast(tree: &Module) -> MdxSignal {
- let mut index = 0;
- while index < tree.body.len() {
- let node = &tree.body[index];
-
- if !node.is_module_decl() {
- let relative = fix_swc_position(node.span().lo.to_usize(), 0);
- return MdxSignal::Error(
- "Unexpected statement in code: only import/exports are supported".into(),
- relative,
- );
- }
+/// Lex expressions in MDX with SWC.
+pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal {
+ let result = parse_expression_core(value, kind);
- index += 1;
+ match result {
+ Err((span, message)) => swc_error_to_signal(span, &message, value.len()),
+ Ok(_) => MdxSignal::Ok,
}
-
- MdxSignal::Ok
}
-/// Check that the resulting AST of an expressions is OK.
-///
-/// This checks that attribute expressions are the expected spread.
-fn check_expression_ast(tree: &Expr, kind: &MdxExpressionKind) -> MdxSignal {
- if matches!(kind, MdxExpressionKind::AttributeExpression)
- && tree
- .unwrap_parens()
- .as_object()
- .and_then(|object| {
- if object.props.len() == 1 {
- object.props[0].as_spread()
- } else {
- None
- }
- })
- .is_none()
- {
- MdxSignal::Error("Expected a single spread value, such as `...x`".into(), 0)
- } else {
- MdxSignal::Ok
- }
+// To do: remove this attribute, use it somewhere.
+#[allow(dead_code)]
+/// Turn SWC comments into a flat vec.
+pub fn flat_comments(single_threaded_comments: SingleThreadedComments) -> Vec<Comment> {
+ let raw_comments = single_threaded_comments.take_all();
+ let take = |list: SingleThreadedCommentsMap| {
+ Rc::try_unwrap(list)
+ .unwrap()
+ .into_inner()
+ .into_values()
+ .flatten()
+ .collect::<Vec<_>>()
+ };
+ let mut list = take(raw_comments.0);
+ list.append(&mut take(raw_comments.1));
+ list
}
/// Turn an SWC error into an `MdxSignal`.
///
/// * If the error happens at `value_len`, yields `MdxSignal::Eof`
/// * Else, yields `MdxSignal::Error`.
-fn swc_error_to_signal(
- error: &SwcError,
- name: &str,
- value_len: usize,
- prefix_len: usize,
-) -> MdxSignal {
- let reason = create_error_reason(&swc_error_to_string(error), name);
- let error_end = fix_swc_position(error.span().hi.to_usize(), prefix_len);
+fn swc_error_to_signal(span: Span, reason: &str, value_len: usize) -> MdxSignal {
+ let error_end = span.hi.to_usize();
if error_end >= value_len {
- MdxSignal::Eof(reason)
+ MdxSignal::Eof(reason.into())
} else {
- MdxSignal::Error(
- reason,
- fix_swc_position(error.span().lo.to_usize(), prefix_len),
- )
+ MdxSignal::Error(reason.into(), span.lo.to_usize())
}
}
-fn swc_error_to_error(error: &SwcError, name: &str, context: &RewriteContext) -> String {
- create_error_message(
- &swc_error_to_string(error),
- name,
- context
- .location
- .and_then(|location| {
- location.relative_to_point(
- context.stops,
- fix_swc_position(error.span().lo.to_usize(), context.prefix_len),
- )
- })
- .as_ref(),
- )
-}
-
-fn create_error_message(reason: &str, name: &str, point: Option<&Point>) -> String {
- prefix_error_with_point(create_error_reason(name, reason), point)
-}
-
-fn create_error_reason(reason: &str, name: &str) -> String {
- format!("Could not parse {} with swc: {}", name, reason)
-}
-
/// Turn an SWC error into a string.
fn swc_error_to_string(error: &SwcError) -> String {
error.kind().msg().into()
@@ -313,7 +200,7 @@ fn swc_error_to_string(error: &SwcError) -> String {
/// This is needed because for expressions, we use an API that parses up to
/// a valid expression, but there may be more expressions after it, which we
/// don’t alow.
-fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal {
+fn whitespace_and_comments(mut index: usize, value: &str) -> Result<(), (Span, String)> {
let bytes = value.as_bytes();
let len = bytes.len();
let mut in_multiline = false;
@@ -329,10 +216,7 @@ fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal {
}
// In a line comment: `// a`.
else if in_line {
- if index + 1 < len && bytes[index] == b'\r' && bytes[index + 1] == b'\n' {
- index += 1;
- in_line = false;
- } else if bytes[index] == b'\r' || bytes[index] == b'\n' {
+ if bytes[index] == b'\r' || bytes[index] == b'\n' {
in_line = false;
}
}
@@ -352,30 +236,27 @@ fn whitespace_and_comments(mut index: usize, value: &str) -> MdxSignal {
}
// Outside comment, not whitespace.
else {
- return MdxSignal::Error(
+ return Err((
+ create_span(index as u32, value.len() as u32),
"Could not parse expression with swc: Unexpected content after expression".into(),
- index,
- );
+ ));
}
index += 1;
}
if in_multiline {
- MdxSignal::Error(
- "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into(),
- index,
- )
- } else if in_line {
+ return Err((
+ create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into()));
+ }
+
+ if in_line {
// EOF instead of EOL is specifically not allowed, because that would
// mean the closing brace is on the commented-out line
- MdxSignal::Error(
- "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".into(),
- index,
- )
- } else {
- MdxSignal::Ok
+ return Err((create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n`".into()));
}
+
+ Ok(())
}
/// Create configuration for SWC, shared between ESM and expressions.
@@ -401,8 +282,8 @@ fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) {
)
}
-/// Turn an SWC byte position from a resulting AST to an offset in the original
-/// input string.
-fn fix_swc_position(index: usize, prefix_len: usize) -> usize {
- index - 1 - prefix_len
+fn fix_span(mut span: Span, offset: usize) -> Span {
+ span.lo = BytePos::from_usize(span.lo.to_usize() - offset);
+ span.hi = BytePos::from_usize(span.hi.to_usize() - offset);
+ span
}
diff --git a/tests/test_utils/swc_utils.rs b/tests/test_utils/swc_utils.rs
index ac4c4db..326550b 100644
--- a/tests/test_utils/swc_utils.rs
+++ b/tests/test_utils/swc_utils.rs
@@ -1,228 +1,38 @@
-//! Lots of helpers for dealing with SWC, particularly from unist.
+//! Lots of helpers for dealing with SWC, particularly from unist, and for
+//! building its ES AST.
-use markdown::{
- mdast::Stop,
- unist::{Point, Position},
- Location,
-};
+use swc_core::common::{BytePos, Span, SyntaxContext, DUMMY_SP};
+use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut};
-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<Position> {
- 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<Point> {
- 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.
+/// Visitor to fix SWC byte positions by removing a prefix.
///
/// > 👉 **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>,
+pub struct RewritePrefixContext {
+ /// Size of prefix considered outside this tree.
+ pub prefix_len: u32,
}
-impl<'a> VisitMut for RewriteContext<'a> {
+impl VisitMut for RewritePrefixContext {
noop_visit_mut_type!();
- // Rewrite spans.
+ /// 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(),
- };
- }
- }
+ if span.lo.0 > self.prefix_len && span.hi.0 > self.prefix_len {
+ result = create_span(span.lo.0 - self.prefix_len, span.hi.0 - self.prefix_len);
}
*span = result;
}
}
-/// Generate an ident.
-///
-/// ```js
-/// a
-/// ```
-pub fn create_ident(sym: &str) -> Ident {
- Ident {
- sym: sym.into(),
- optional: false,
- span: DUMMY_SP,
- }
-}
-
-/// Generate an ident expression.
-///
-/// ```js
-/// a
-/// ```
-pub fn create_ident_expression(sym: &str) -> Expr {
- Expr::Ident(create_ident(sym))
-}
-
-/// Generate a binary expression.
-///
-/// ```js
-/// a + b + c
-/// a || b
-/// ```
-pub fn create_binary_expression(mut exprs: Vec<Expr>, op: BinaryOp) -> Expr {
- exprs.reverse();
-
- let mut left = None;
-
- while let Some(right_expr) = exprs.pop() {
- left = Some(if let Some(left_expr) = left {
- Expr::Bin(BinExpr {
- left: Box::new(left_expr),
- right: Box::new(right_expr),
- op,
- span: swc_common::DUMMY_SP,
- })
- } else {
- right_expr
- });
- }
-
- left.expect("expected one or more expressions")
-}
-
-/// Generate a member expression.
-///
-/// ```js
-/// a.b
-/// a
-/// ```
-pub fn create_member_expression(name: &str) -> Expr {
- 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.len() > 1 {
- let mut member = MemberExpr {
- obj: Box::new(create_ident_expression(parts[0])),
- prop: MemberProp::Ident(create_ident(parts[1])),
- span: swc_common::DUMMY_SP,
- };
- let mut index = 2;
- while index < parts.len() {
- member = MemberExpr {
- obj: Box::new(Expr::Member(member)),
- prop: MemberProp::Ident(create_ident(parts[1])),
- span: swc_common::DUMMY_SP,
- };
- index += 1;
- }
- Expr::Member(member)
- } else {
- create_ident_expression(name)
+/// Generate a span.
+pub fn create_span(lo: u32, hi: u32) -> Span {
+ Span {
+ lo: BytePos(lo),
+ hi: BytePos(hi),
+ ctxt: SyntaxContext::default(),
}
}