diff --git a/examples/plugins.rs b/examples/plugins.rs new file mode 100644 index 0000000..7bae341 --- /dev/null +++ b/examples/plugins.rs @@ -0,0 +1,108 @@ +extern crate mdxjs; + +use markdown::mdast; +use mdxjs::hast; +use mdxjs::{HastNode, MdastNode, Options, PluginOptions, RecmaProgram}; +use std::rc::Rc; +use swc_core::common::{Span, SyntaxContext}; +use swc_core::ecma::ast as estree; +use swc_core::ecma::atoms::JsWord; + +/// Example that compiles the example MDX document from +/// to JavaScript. +fn main() -> Result<(), String> { + println!( + "{}", + mdxjs::compile_with_plugins( + "# test", + &Options { + ..Default::default() + }, + &PluginOptions { + experimental_mdast_transforms: Some(vec![Rc::new(|root: &mut MdastNode| { + mdast_visit_mut(root, |n| { + if let mdast::Node::Text(text) = n { + text.value = "Hello World!".into(); + } + }); + Ok(()) + })]), + experimental_hast_transforms: Some(vec![Rc::new(|root: &mut HastNode| { + hast_visit_mut(root, |n| { + if let hast::Node::Element(e) = n { + if e.tag_name == "h1" { + e.tag_name = "h2".into(); + } + }; + }); + Ok(()) + })]), + experimental_recma_transforms: Some(vec![Rc::new(|program: &mut RecmaProgram| { + let body = &mut program.module.body; + body.push(estree::ModuleItem::Stmt(estree::Stmt::Expr( + estree::ExprStmt { + expr: Box::new(estree::Expr::Ident(estree::Ident::from(( + JsWord::from("hello"), + SyntaxContext::empty(), + )))), + span: Span::default(), + }, + ))); + Ok(()) + })]) + } + )? + ); + + Ok(()) +} + +fn mdast_visit_mut(node: &mut mdast::Node, visitor: Visitor) +where + Visitor: FnMut(&mut mdast::Node), +{ + mdast_visit_mut_impl(node, visitor); +} + +fn mdast_visit_mut_impl(node: &mut mdast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mut mdast::Node), +{ + visitor(node); + + if let Some(children) = node.children_mut() { + let mut index = 0; + while index < children.len() { + let child = &mut children[index]; + visitor = mdast_visit_mut_impl(child, visitor); + index += 1; + } + } + + visitor +} + +fn hast_visit_mut(node: &mut hast::Node, visitor: Visitor) +where + Visitor: FnMut(&mut hast::Node), +{ + hast_visit_mut_impl(node, visitor); +} + +fn hast_visit_mut_impl(node: &mut hast::Node, mut visitor: Visitor) -> Visitor +where + Visitor: FnMut(&mut hast::Node), +{ + visitor(node); + + if let Some(children) = node.children_mut() { + let mut index = 0; + while index < children.len() { + let child = &mut children[index]; + visitor = hast_visit_mut_impl(child, visitor); + index += 1; + } + } + + visitor +} diff --git a/src/configuration.rs b/src/configuration.rs index 862713e..777fbf7 100644 --- a/src/configuration.rs +++ b/src/configuration.rs @@ -1,6 +1,11 @@ //! Configuration. use crate::mdx_plugin_recma_document::JsxRuntime; +use std::rc::Rc; + +pub use crate::hast::Node as HastNode; +pub use crate::hast_util_to_swc::Program as RecmaProgram; +pub use markdown::mdast::Node as MdastNode; /// Like `Constructs` from `markdown-rs`. /// @@ -156,6 +161,28 @@ impl MdxParseOptions { } } +type MdastPlugin = Rc Result<(), String> + 'static>; + +type HastPlugin = Rc Result<(), String> + 'static>; + +type RecmaPlugin = Rc Result<(), String> + 'static>; + +pub struct PluginOptions { + pub experimental_mdast_transforms: Option>, + pub experimental_hast_transforms: Option>, + pub experimental_recma_transforms: Option>, +} + +impl Default for PluginOptions { + fn default() -> Self { + Self { + experimental_mdast_transforms: None, + experimental_hast_transforms: None, + experimental_recma_transforms: None, + } + } +} + /// Configuration (optional). #[derive(Clone, Debug)] #[cfg_attr(feature = "serializable", derive(serde::Serialize, serde::Deserialize))] diff --git a/src/hast_util_to_swc.rs b/src/hast_util_to_swc.rs index 4bebd66..af899e6 100644 --- a/src/hast_util_to_swc.rs +++ b/src/hast_util_to_swc.rs @@ -43,7 +43,7 @@ use swc_core::ecma::ast::{ pub const MAGIC_EXPLICIT_MARKER: u32 = 1337; /// Result. -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Program { /// File path. pub path: Option, diff --git a/src/lib.rs b/src/lib.rs index 7160a58..fe34624 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,8 +12,9 @@ #![allow(clippy::cast_precision_loss)] extern crate markdown; + mod configuration; -mod hast; +pub mod hast; mod hast_util_to_swc; mod mdast_util_to_hast; mod mdx_plugin_recma_document; @@ -32,7 +33,9 @@ use crate::{ }; use markdown::{to_mdast, Constructs, Location, ParseOptions}; -pub use crate::configuration::{MdxConstructs, MdxParseOptions, Options}; +pub use crate::configuration::{ + HastNode, MdastNode, MdxConstructs, MdxParseOptions, Options, PluginOptions, RecmaProgram, +}; pub use crate::mdx_plugin_recma_document::JsxRuntime; /// Turn MDX into JavaScript. @@ -53,6 +56,20 @@ pub use crate::mdx_plugin_recma_document::JsxRuntime; /// This project errors for many different reasons, such as syntax errors in /// the MDX format or misconfiguration. pub fn compile(value: &str, options: &Options) -> Result { + compile_with_plugins(value, options, &PluginOptions::default()) +} + +/// Turn MDX into JavaScript using the specified Plugins +/// +/// ## Errors +/// +/// This project errors for many different reasons, such as syntax errors in +/// the MDX format or misconfiguration. +pub fn compile_with_plugins( + value: &str, + options: &Options, + plugins: &PluginOptions, +) -> Result { let parse_options = ParseOptions { constructs: Constructs { attention: options.parse.constructs.attention, @@ -111,12 +128,33 @@ pub fn compile(value: &str, options: &Options) -> Result { }; let location = Location::new(value.as_bytes()); - let mdast = to_mdast(value, &parse_options)?; - let hast = mdast_util_to_hast(&mdast); + let mut mdast = to_mdast(value, &parse_options)?; + + if let Some(mdast_plugins) = &plugins.experimental_mdast_transforms { + for plugin in mdast_plugins { + plugin(&mut mdast)?; + } + } + + let mut hast = mdast_util_to_hast(&mdast); + + if let Some(hast_plugins) = &plugins.experimental_hast_transforms { + for plugin in hast_plugins { + plugin(&mut hast)?; + } + } + let mut program = hast_util_to_swc(&hast, options.filepath.clone(), Some(&location))?; + mdx_plugin_recma_document(&mut program, &document_options, Some(&location))?; mdx_plugin_recma_jsx_rewrite(&mut program, &rewrite_options, Some(&location)); + if let Some(recma_plugins) = &plugins.experimental_recma_transforms { + for plugin in recma_plugins { + plugin(&mut program)?; + } + } + if !options.jsx { swc_util_build_jsx(&mut program, &build_options, Some(&location))?; }