aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Titus Wormer <tituswormer@gmail.com>2022-10-12 17:40:01 +0200
committerLibravatar Titus Wormer <tituswormer@gmail.com>2022-10-12 17:40:01 +0200
commit54d84a4fd326dcfae871e203d673d55cbf40a279 (patch)
tree977bc7e7acc017e86d06aa6ee07bda93ba58631f
parentae9382b7f9466c8df421f0f64850a69a4c658581 (diff)
downloadmarkdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.tar.gz
markdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.tar.bz2
markdown-rs-54d84a4fd326dcfae871e203d673d55cbf40a279.zip
Add internals to compile JSX away (wip)
-rw-r--r--Cargo.toml1
-rw-r--r--tests/test_utils/mdx.rs221
-rw-r--r--tests/test_utils/mdx_plugin_recma_document.rs2
-rw-r--r--tests/test_utils/mod.rs1
-rw-r--r--tests/xxx_mdx.rs154
5 files changed, 378 insertions, 1 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 0cc6fd0..ef0d46e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -30,6 +30,7 @@ swc_ecma_codegen = "0.127"
swc_common = "0.29"
swc_ecma_ast = "0.94"
swc_ecma_parser = "0.122"
+swc_ecma_transforms_react = "0.155"
swc_ecma_visit = "0.80"
[build-dependencies]
diff --git a/tests/test_utils/mdx.rs b/tests/test_utils/mdx.rs
new file mode 100644
index 0000000..28238d0
--- /dev/null
+++ b/tests/test_utils/mdx.rs
@@ -0,0 +1,221 @@
+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 micromark::{micromark_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 = micromark_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
index 5c0d423..4c35b7a 100644
--- a/tests/test_utils/mdx_plugin_recma_document.rs
+++ b/tests/test_utils/mdx_plugin_recma_document.rs
@@ -13,7 +13,7 @@ use micromark::{
Location,
};
-/// JSX runtimes.
+/// JSX runtimes (default: `JsxRuntime: Automatic`).
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum JsxRuntime {
/// Automatic runtime.
diff --git a/tests/test_utils/mod.rs b/tests/test_utils/mod.rs
index 8d1f144..de203c1 100644
--- a/tests/test_utils/mod.rs
+++ b/tests/test_utils/mod.rs
@@ -1,6 +1,7 @@
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;
diff --git a/tests/xxx_mdx.rs b/tests/xxx_mdx.rs
new file mode 100644
index 0000000..59125b3
--- /dev/null
+++ b/tests/xxx_mdx.rs
@@ -0,0 +1,154 @@
+extern crate micromark;
+extern crate swc_common;
+extern crate swc_ecma_ast;
+extern crate swc_ecma_codegen;
+mod test_utils;
+use pretty_assertions::assert_eq;
+use test_utils::mdx::{mdx, JsxRuntime, Options};
+
+#[test]
+fn mdx_test() -> Result<(), String> {
+ // To do: JSX should be compiled away.
+ assert_eq!(
+ mdx("", Some("example.mdx".into()), &Options::default())?,
+ "function _createMdxContent(props) {
+ return <></>;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+",
+ "should work",
+ );
+
+ // To do: JSX should be compiled away.
+ assert_eq!(
+ mdx("<A />", Some("example.mdx".into()), &Options {
+ development: true,
+ ..Default::default()
+ })?,
+ "function _createMdxContent(props) {
+ const { A } = props.components || {};
+ if (!A) _missingMdxReference(\"A\", true, \"1:1-1:6\");
+ return <A />;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+function _missingMdxReference(id, component, place) {
+ throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\" + (place ? \"\\nIt’s referenced in your code at `\" + place + \"` in `example.mdx`\" : \"\"));
+}
+",
+ "should support `options.development: true`",
+ );
+
+ // To do: JSX should be compiled away.
+ assert_eq!(
+ mdx("<A />", Some("example.mdx".into()), &Options {
+ provider_import_source: Some("@mdx-js/react".into()),
+ ..Default::default()
+ })?,
+ "import { useMDXComponents as _provideComponents } from \"@mdx-js/react\";
+function _createMdxContent(props) {
+ const { A } = Object.assign({}, _provideComponents(), props.components);
+ if (!A) _missingMdxReference(\"A\", true);
+ return <A />;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components);
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+function _missingMdxReference(id, component) {
+ throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\");
+}
+",
+ "should support `options.provider_import_source`",
+ );
+
+ assert_eq!(
+ mdx("", Some("example.mdx".into()), &Options {
+ jsx: true,
+ ..Default::default()
+ })?,
+ "function _createMdxContent(props) {
+ return <></>;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+",
+ "should support `options.jsx: true`",
+ );
+
+ // To do: JSX should be compiled away.
+ // To do: should use calls of `React.createElement` / `React.Fragment`.
+ assert_eq!(
+ mdx("", Some("example.mdx".into()), &Options {
+ jsx_runtime: Some(JsxRuntime::Classic),
+ ..Default::default()
+ })?,
+ "import { React } from \"react\";
+function _createMdxContent(props) {
+ return <></>;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+",
+ "should support `options.jsx_runtime: JsxRuntime::Classic`",
+ );
+
+ // To do: JSX should be compiled away.
+ // To do: should import `_jsx` and such.
+ // To do: should use calls of `_jsx`, etc.
+ assert_eq!(
+ mdx("", Some("example.mdx".into()), &Options {
+ jsx_import_source: Some("preact".into()),
+ ..Default::default()
+ })?,
+ "function _createMdxContent(props) {
+ return <></>;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+",
+ "should support `options.jsx_import_source: Some(\"preact\".into())`",
+ );
+
+ // To do: JSX should be compiled away.
+ // To do: should use calls of `a.b`, symbol of `a.c`.
+ assert_eq!(
+ mdx("", Some("example.mdx".into()), &Options {
+ jsx_runtime: Some(JsxRuntime::Classic),
+ pragma: Some("a.b".into()),
+ pragma_frag: Some("a.c".into()),
+ pragma_import_source: Some("d".into()),
+ ..Default::default()
+ })?,
+ "import { a } from \"d\";
+function _createMdxContent(props) {
+ return <></>;
+}
+function MDXContent(props = {}) {
+ const { wrapper: MDXLayout } = props.components || {};
+ return MDXLayout ? <MDXLayout {...props}><_createMdxContent {...props}/></MDXLayout> : _createMdxContent(props);
+}
+export default MDXContent;
+",
+ "should support `options.pragma`, `options.pragma_frag`, `options.pragma_import_source`",
+ );
+
+ Ok(())
+}