├── .editorconfig ├── .github ├── code-of-conduct.md ├── contribute.md ├── support.md └── workflows │ └── main.yml ├── .gitignore ├── Cargo.toml ├── codecov.yml ├── examples └── lib.rs ├── funding.yml ├── license ├── readme.md ├── renovate.json ├── src ├── configuration.rs ├── hast.rs ├── hast_util_to_swc.rs ├── lib.rs ├── mdast_util_to_hast.rs ├── mdx_plugin_recma_document.rs ├── mdx_plugin_recma_jsx_rewrite.rs ├── swc.rs ├── swc_util_build_jsx.rs └── swc_utils.rs └── tests └── test.rs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.rs] 12 | indent_size = 4 13 | 14 | [tests/commonmark.rs] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Code of conduct 2 | 3 | ## Our pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances 31 | of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political 33 | attacks 34 | * Public or private harassment 35 | * Publishing others’ private information, such as a physical or email address, 36 | without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this code of conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This code of conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official email address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | `tituswormer@gmail.com`. 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement guidelines 71 | 72 | Community leaders will follow these guidelines in determining 73 | the consequences for any action they deem in violation of this code of conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. 83 | A public apology may be requested. 84 | 85 | ### 2. Warning 86 | 87 | **Community impact**: A violation through a single incident or series of 88 | actions. 89 | 90 | **Consequence**: A warning with consequences for continued behavior. 91 | No interaction with the people involved, including unsolicited interaction with 92 | those enforcing the code of conduct, for a specified period of time. 93 | This includes avoiding interactions in community spaces as well as external 94 | channels like social media. 95 | Violating these terms may lead to a temporary or permanent ban. 96 | 97 | ### 3. Temporary ban 98 | 99 | **Community impact**: A serious violation of community standards, including 100 | sustained inappropriate behavior. 101 | 102 | **Consequence**: A temporary ban from any sort of interaction or public 103 | communication with the community for a specified period of time. 104 | No public or private interaction with the people involved, including 105 | unsolicited interaction with those enforcing the code of conduct, is allowed 106 | during this period. 107 | Violating these terms may lead to a permanent ban. 108 | 109 | ### 4. Permanent ban 110 | 111 | **Community impact**: Demonstrating a pattern of violation of community 112 | standards, including sustained inappropriate behavior, harassment of an 113 | individual, or aggression toward or disparagement of classes of individuals. 114 | 115 | **Consequence**: A permanent ban from any sort of public interaction within the 116 | community. 117 | 118 | ## Attribution 119 | 120 | This code of conduct is adapted from the [contributor covenant][homepage], 121 | version 2.1, available at 122 | [`contributor-covenant.org/version/2/1/code_of_conduct.html`][v2.1]. 123 | 124 | Community impact guidelines were inspired by 125 | [Mozilla’s code of conduct enforcement ladder][mozilla-coc]. 126 | 127 | For answers to common questions about this code of conduct, see the FAQ at 128 | [`contributor-covenant.org/faq`][faq]. 129 | 130 | [homepage]: https://www.contributor-covenant.org 131 | 132 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 133 | 134 | [mozilla-coc]: https://github.com/mozilla/inclusion 135 | 136 | [faq]: https://www.contributor-covenant.org/faq 137 | -------------------------------------------------------------------------------- /.github/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | > 👉 **Important**: this project has a [code of conduct][coc]. 4 | > By interacting with this repository and community you agree to abide by its 5 | > terms. 6 | 7 | This article explains how to contribute. 8 | Please read through the following guidelines. 9 | 10 | ## Contributions 11 | 12 | There are several ways to contribute, not just by writing code. 13 | See [Support][] if you have questions. 14 | 15 | ### Financial support 16 | 17 | You can help financially. 18 | See [Sponsor][] for more info. 19 | 20 | ### Improve docs 21 | 22 | As a user you’re perfect to help improve the docs. 23 | Typo corrections, error fixes, better explanations, new examples, etcetera. 24 | 25 | ### Improve issues 26 | 27 | Some issues lack information, aren’t reproducible, or are just incorrect. 28 | You can help by trying to make them easier to resolve. 29 | Existing issues might benefit from your unique experience or opinions. 30 | 31 | ### Write code 32 | 33 | Code contributions are very welcome too. 34 | It’s probably a good idea to first post a question or open an issue to report a 35 | bug or suggest a new feature before creating a pull request. 36 | See [Project][] for more info. 37 | 38 | ## Submitting an issue 39 | 40 | - the issue tracker is for issues, discussions are for questions 41 | - search the issue tracker (including closed issues) before opening a new 42 | issue 43 | - ensure you’re using the latest versions of packages and other tools 44 | - use a clear and descriptive title 45 | - include as much information as possible: steps to reproduce the issue, 46 | error message, version, operating system, etcetera 47 | - the more time you put into an issue, the better help you can get 48 | - the best issue report is a failing test proving it 49 | 50 | ## Submitting a pull request 51 | 52 | - run `cargo fmt` and `cargo test` locally to format and test your changes 53 | - non-trivial changes are often best discussed in an issue first, to prevent 54 | you from doing unnecessary work 55 | - for ambitious tasks, you should try to get your work in front of the 56 | community for feedback as soon as possible 57 | - new features should be accompanied by tests and documentation 58 | - don’t include unrelated changes 59 | - write a convincing description of why your pull request should land: 60 | it’s your job to be convincing 61 | 62 | ## Project (for maintainers) 63 | 64 | See [Project][project] in the readme for info on how the project is structured 65 | and how to run useful scripts. 66 | 67 | ### Commit 68 | 69 | Look at the commits in the project for the style being used. 70 | 71 | For example: 72 | 73 | ```git-commit 74 | Update `swc_core` 75 | 76 | Some long description here 77 | 78 | Closes GH-24. 79 | ``` 80 | 81 | Some points: 82 | 83 | - short descriptive message as title 84 | - no issue/PR references in title 85 | - reference the issues/PRs that are closed in the commit body 86 | - optionally you can include who reviewed or co-authored: 87 | 88 | ``` 89 | Reviewed-by: Titus Wormer 90 | 91 | Co-authored-by: Titus Wormer 92 | ``` 93 | 94 | ### Release 95 | 96 | Perform the following steps locally, no PR needed: 97 | 98 | - update the `version` field in `Cargo.toml` 99 | - `git commit --all --message 1.2.3 && git tag 1.2.3 && git push && git push --tags` 100 | - `cargo publish` 101 | 102 | For the release notes, here’s what I do. 103 | You can also look at the existing release notes for how to do it. 104 | 105 | - go to releases: 106 | - click “Draft a new release” 107 | - click “Choose a release”, choose the one you just released 108 | - click “Generate release notes”, it might generate for example: 109 | 110 | ```markdown 111 | ## What's Changed 112 | 113 | - Update `swc_core` by @kdy1 in https://github.com/wooorm/mdxjs-rs/pull/25 114 | 115 | **Full Changelog**: https://github.com/wooorm/mdxjs-rs/compare/0.1.10...0.1.11 116 | ``` 117 | 118 | - locally I run `git l` (git alias for 119 | `l = log --pretty=oneline --graph --abbrev-commit`) to produce a markdown 120 | list of the commits, such as: 121 | ```markdown 122 | - 4513866 (HEAD -> main, tag: 0.1.11, origin/main) 0.1.11 123 | - 833eacf Update `swc_core` 124 | ``` 125 | - finally I manually merge the two results to get: 126 | 127 | ``` 128 | * 833eacf Update `swc_core` 129 | by @kdy1 in https://github.com/wooorm/mdxjs-rs/pull/25 130 | 131 | **Full Changelog**: https://github.com/wooorm/mdxjs-rs/compare/0.1.10...0.1.11 132 | ``` 133 | 134 | - for long release notes with important info, I think about what a reader 135 | wants and needs. 136 | What is breaking? 137 | What is actually important? 138 | Sometimes I reorder and amend stuff to highlight what’s important and how 139 | users need to migrate! 140 | 141 | ## Resources 142 | 143 | - [how to contribute to open source](https://opensource.guide/how-to-contribute/) 144 | - [making your first contribution](https://medium.com/@vadimdemedes/making-your-first-contribution-de6576ddb190) 145 | - [using pull requests](https://help.github.com/articles/about-pull-requests/) 146 | - [GitHub help](https://help.github.com) 147 | 148 | ## License 149 | 150 | [CC-BY-4.0][license] © [Titus Wormer][author] 151 | 152 | 153 | 154 | [license]: https://creativecommons.org/licenses/by/4.0/ 155 | [author]: https://wooorm.com 156 | [support]: support.md 157 | [coc]: code-of-conduct.md 158 | [sponsor]: https://github.com/wooorm/mdxjs-rs/#sponsor 159 | [project]: https://github.com/wooorm/mdxjs-rs/#project 160 | -------------------------------------------------------------------------------- /.github/support.md: -------------------------------------------------------------------------------- 1 | # Support 2 | 3 | > 👉 **Important**: this project has a [code of conduct][coc]. 4 | > By interacting with this repository and community you agree to abide by its 5 | > terms. 6 | 7 | This article explains how and where to get help. 8 | Please read through the following guidelines. 9 | 10 | ## Asking quality questions 11 | 12 | Questions can go to [GitHub discussions][chat]. 13 | 14 | Help us help you! 15 | Spend time framing questions and add links and resources. 16 | Spending the extra time up front helps save everyone time in the long run. 17 | Here are some tips: 18 | 19 | * see [*How do I ask a good question* by `StackOverflow`][how-to-ask] for a 20 | good guide 21 | * [talk to a duck][rubberduck]! 22 | * don’t fall for the [XY problem][xy] 23 | * search to find out if a similar question has been asked 24 | * try to define what you need help with: 25 | * is there something in particular you want to do? 26 | * what problem are you encountering and what steps have you taken to try 27 | and fix it? 28 | * is there a concept you don’t understand? 29 | * provide sample code, if possible 30 | * screenshots can help, but if there’s important text such as code or error 31 | messages in them, please also provide those as text 32 | * the more time you put into asking your question, the better we can help you 33 | 34 | ## Contributions 35 | 36 | See [`contribute.md`][contribute] on how to contribute. 37 | 38 | ## License 39 | 40 | [CC-BY-4.0][license] © [Titus Wormer][author] 41 | 42 | 43 | 44 | [license]: https://creativecommons.org/licenses/by/4.0/ 45 | 46 | [author]: https://wooorm.com 47 | 48 | [rubberduck]: https://rubberduckdebugging.com 49 | 50 | [xy]: https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem/66378#66378 51 | 52 | [chat]: https://github.com/wooorm/mdxjs-rs/discussions 53 | 54 | [contribute]: contribute.md 55 | 56 | [coc]: code-of-conduct.md 57 | 58 | [how-to-ask]: https://stackoverflow.com/help/how-to-ask 59 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | coverage: 3 | runs-on: ubuntu-latest 4 | steps: 5 | - uses: actions/checkout@v4 6 | - uses: dtolnay/rust-toolchain@v1 7 | with: 8 | toolchain: stable 9 | - run: cargo install cargo-tarpaulin && cargo tarpaulin --out xml 10 | - uses: codecov/codecov-action@v5 11 | main: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: dtolnay/rust-toolchain@v1 16 | with: 17 | toolchain: stable 18 | components: clippy, rustfmt 19 | - run: cargo fmt --check && cargo clippy --all-targets --all-features 20 | - run: cargo check --all-features 21 | - run: cargo test 22 | name: main 23 | on: 24 | - pull_request 25 | - push 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | *.lock 4 | target 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [dependencies] 2 | markdown = "1" 3 | rustc-hash = "2" 4 | serde = { optional = true, version = "1" } 5 | swc_core = { features = [ 6 | "common", 7 | "ecma_ast", 8 | "ecma_codegen", 9 | "ecma_parser", 10 | "ecma_visit", 11 | ], version = "26" } 12 | 13 | [dev-dependencies] 14 | pretty_assertions = "1" 15 | 16 | [features] 17 | serializable = ["serde"] 18 | 19 | [package] 20 | authors = ["Titus Wormer "] 21 | categories = ["compilers", "text-processing"] 22 | description = "Compile MDX to JavaScript in Rust." 23 | edition = "2018" 24 | homepage = "https://github.com/wooorm/mdxjs-rs" 25 | include = ["license", "src/"] 26 | keywords = ["compile", "markdown", "mdx"] 27 | license = "MIT" 28 | name = "mdxjs" 29 | repository = "https://github.com/wooorm/mdxjs-rs" 30 | rust-version = "1.56" 31 | version = "1.0.3" 32 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | patch: false 4 | project: 5 | default: 6 | informational: true 7 | -------------------------------------------------------------------------------- /examples/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate mdxjs; 2 | 3 | /// Example that compiles the example MDX document from 4 | /// to JavaScript. 5 | fn main() -> Result<(), markdown::message::Message> { 6 | println!( 7 | "{}", 8 | mdxjs::compile( 9 | r##" 10 | import {Chart} from './snowfall.js' 11 | export const year = 2018 12 | 13 | # Last year’s snowfall 14 | 15 | In {year}, the snowfall was above average. 16 | It was followed by a warm spring which caused 17 | flood conditions in many of the nearby rivers. 18 | 19 | 20 | "##, 21 | &Default::default() 22 | )? 23 | ); 24 | 25 | Ok(()) 26 | } 27 | -------------------------------------------------------------------------------- /funding.yml: -------------------------------------------------------------------------------- 1 | github: wooorm 2 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2022 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # mdxjs-rs 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![GitHub][repo-badge]][repo] 6 | [![docs.rs][docs-badge]][docs] 7 | [![crates.io][crate-badge]][crate] 8 | 9 | Compile MDX to JavaScript in Rust. 10 | 11 | ## When should I use this? 12 | 13 | You can use this crate when you’re dealing with the Rust language and want 14 | to compile MDX to JavaScript. 15 | To parse the MDX format to a syntax tree, use [`markdown-rs`][markdown-rs] instead. 16 | 17 | This project does not yet support plugins. 18 | To benefit from the unified (remark and rehype) ecosystem, use 19 | [`@mdx-js/mdx`][mdx-js]. 20 | 21 | ## What is this? 22 | 23 | This Rust crate works exactly like the npm package [`@mdx-js/mdx`][mdx-js]. 24 | It uses the Rust crates [`markdown-rs`][markdown-rs] and [SWC][] to deal with the 25 | markdown and JavaScript inside MDX. 26 | 27 | ## Questions 28 | 29 | * to learn MDX, see [`mdxjs.com`][mdx-site] 30 | * for the API, see the [crate docs][docs] 31 | * for questions, see [Discussions][chat] 32 | * to help, see [contribute][] or [sponsor][] below 33 | 34 | ## Contents 35 | 36 | * [Install](#install) 37 | * [Use](#use) 38 | * [API](#api) 39 | * [Project](#project) 40 | * [Test](#test) 41 | * [Version](#version) 42 | * [Security](#security) 43 | * [Contribute](#contribute) 44 | * [Sponsor](#sponsor) 45 | * [Thanks](#thanks) 46 | * [License](#license) 47 | 48 | ## Install 49 | 50 | With [Rust][] (rust edition 2018+, ±version 1.56+), install with `cargo`: 51 | 52 | ```sh 53 | cargo add mdxjs 54 | ``` 55 | 56 | ## Use 57 | 58 | ```rs 59 | extern crate mdxjs; 60 | 61 | fn main() -> Result<(), markdown::message::Message> { 62 | println!( 63 | "{}", 64 | mdxjs::compile( 65 | r###" 66 | import {Chart} from './snowfall.js' 67 | export const year = 2018 68 | 69 | # Last year’s snowfall 70 | 71 | In {year}, the snowfall was above average. 72 | It was followed by a warm spring which caused 73 | flood conditions in many of the nearby rivers. 74 | 75 | 76 | "###, 77 | &Default::default() 78 | )? 79 | ); 80 | 81 | Ok(()) 82 | } 83 | ``` 84 | 85 | Yields (prettified): 86 | 87 | ```javascript 88 | import {Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs} from 'react/jsx-runtime' 89 | import {Chart} from './snowfall.js' 90 | export const year = 2018 91 | 92 | function _createMdxContent(props) { 93 | const _components = Object.assign({h1: 'h1', p: 'p'}, props.components) 94 | return _jsxs(_Fragment, { 95 | children: [ 96 | _jsx(_components.h1, {children: 'Last year’s snowfall'}), 97 | '\n', 98 | _jsxs(_components.p, { 99 | children: [ 100 | 'In ', 101 | year, 102 | ', the snowfall was above average.\nIt was followed by a warm spring which caused\nflood conditions in many of the nearby rivers.' 103 | ] 104 | }), 105 | '\n', 106 | _jsx(Chart, {year: year, color: '#fcb32c'}) 107 | ] 108 | }) 109 | } 110 | 111 | function MDXContent(props = {}) { 112 | const {wrapper: MDXLayout} = props.components || {} 113 | return MDXLayout 114 | ? _jsx(MDXLayout, Object.assign({}, props, {children: _jsx(_createMdxContent, props)})) 115 | : _createMdxContent(props) 116 | } 117 | 118 | export default MDXContent 119 | ``` 120 | 121 | ## API 122 | 123 | `mdxjs-rs` exposes 124 | [`compile`](https://docs.rs/mdxjs/latest/mdxjs/fn.compile.html), 125 | [`JsxRuntime`](https://docs.rs/mdxjs/latest/mdxjs/enum.JsxRuntime.html), 126 | [`Options`](https://docs.rs/mdxjs/latest/mdxjs/struct.Options.html), 127 | and a few other structs and enums. 128 | 129 | See the [crate docs][docs] for more info. 130 | 131 | ## Project 132 | 133 | ### Test 134 | 135 | `mdxjs-rs` is tested with a lot of tests. 136 | These tests reach all branches in the code, which means that this project has 137 | 100% code coverage. 138 | 139 | The following bash scripts are useful when working on this project: 140 | 141 | * run examples: 142 | ```sh 143 | RUST_BACKTRACE=1 cargo run --example lib 144 | ``` 145 | * format: 146 | ```sh 147 | cargo fmt && cargo fix 148 | ``` 149 | * lint: 150 | ```sh 151 | cargo fmt --check && cargo clippy --all-targets 152 | ``` 153 | * test: 154 | ```sh 155 | RUST_BACKTRACE=1 cargo test 156 | ``` 157 | * docs: 158 | ```sh 159 | cargo doc --document-private-items 160 | ``` 161 | 162 | ### Version 163 | 164 | `mdxjs-rs` follows [SemVer](https://semver.org). 165 | 166 | ### Security 167 | 168 | MDX is a programming language. 169 | It is JavaScript. 170 | It is not safe to let people you don’t trust write MDX. 171 | 172 | ### Contribute 173 | 174 | See [`contributing.md`][contributing] for ways to help. 175 | See [`support.md`][support] for ways to get help. 176 | See [`code-of-conduct.md`][coc] for how to communicate in and around this 177 | project. 178 | 179 | ### Sponsor 180 | 181 | Support this effort and give back by sponsoring: 182 | 183 | * [GitHub Sponsors](https://github.com/sponsors/wooorm) 184 | (personal; monthly or one-time) 185 | * [OpenCollective](https://opencollective.com/unified) or 186 | [GitHub Sponsors](https://github.com/sponsors/unifiedjs) 187 | (unified; monthly or one-time) 188 | 189 | ### Thanks 190 | 191 | Special thanks go out to: 192 | 193 | * [Vercel][] for funding the initial development 194 | 195 | ## License 196 | 197 | [MIT][license] © [Titus Wormer][author] 198 | 199 | [build-badge]: https://github.com/wooorm/mdxjs-rs/workflows/main/badge.svg 200 | 201 | [build]: https://github.com/wooorm/mdxjs-rs/actions 202 | 203 | [coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/mdxjs-rs.svg 204 | 205 | [coverage]: https://codecov.io/github/wooorm/mdxjs-rs 206 | 207 | [repo-badge]: https://img.shields.io/badge/GitHub-wooorm%2Fmdxjs--rs-brightgreen 208 | 209 | [repo]: https://github.com/wooorm/mdxjs-rs 210 | 211 | [docs-badge]: https://img.shields.io/docsrs/mdxjs 212 | 213 | [docs]: https://docs.rs/mdxjs/ 214 | 215 | [crate-badge]: https://img.shields.io/crates/v/mdxjs 216 | 217 | [crate]: https://crates.io/crates/mdxjs/ 218 | 219 | [chat]: https://github.com/wooorm/mdxjs-rs/discussions 220 | 221 | [license]: license 222 | 223 | [author]: https://wooorm.com 224 | 225 | [mdx-js]: https://mdxjs.com/packages/mdx/ 226 | 227 | [mdx-site]: https://mdxjs.com 228 | 229 | [markdown-rs]: https://github.com/wooorm/markdown-rs 230 | 231 | [swc]: https://swc.rs 232 | 233 | [rust]: https://www.rust-lang.org 234 | 235 | [vercel]: https://vercel.com 236 | 237 | [contribute]: #contribute 238 | 239 | [sponsor]: #sponsor 240 | 241 | [contributing]: .github/contribute.md 242 | 243 | [support]: .github/support.md 244 | 245 | [coc]: .github/code-of-conduct.md 246 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["config:base", ":preserveSemverRanges"] 4 | } 5 | -------------------------------------------------------------------------------- /src/configuration.rs: -------------------------------------------------------------------------------- 1 | //! Configuration. 2 | 3 | use crate::mdx_plugin_recma_document::JsxRuntime; 4 | 5 | /// Like `Constructs` from `markdown-rs`. 6 | /// 7 | /// You can’t use: 8 | /// 9 | /// * `autolink` 10 | /// * `code_indented` 11 | /// * `html_flow` 12 | /// * `html_text` 13 | /// * `mdx_esm` 14 | /// * `mdx_expression_flow` 15 | /// * `mdx_expression_text` 16 | /// * `mdx_jsx_flow` 17 | /// * `mdx_jsx_text` 18 | /// 19 | // To do: link all docs when `markdown-rs` is stable. 20 | #[derive(Clone, Debug, Eq, PartialEq)] 21 | #[cfg_attr(feature = "serializable", derive(serde::Serialize, serde::Deserialize))] 22 | #[cfg_attr(feature = "serializable", serde(rename_all = "camelCase", default))] 23 | pub struct MdxConstructs { 24 | pub attention: bool, 25 | pub block_quote: bool, 26 | pub character_escape: bool, 27 | pub character_reference: bool, 28 | pub code_fenced: bool, 29 | pub code_text: bool, 30 | pub definition: bool, 31 | pub frontmatter: bool, 32 | pub gfm_autolink_literal: bool, 33 | pub gfm_footnote_definition: bool, 34 | pub gfm_label_start_footnote: bool, 35 | pub gfm_strikethrough: bool, 36 | pub gfm_table: bool, 37 | pub gfm_task_list_item: bool, 38 | pub hard_break_escape: bool, 39 | pub hard_break_trailing: bool, 40 | pub heading_atx: bool, 41 | pub heading_setext: bool, 42 | pub label_start_image: bool, 43 | pub label_start_link: bool, 44 | pub label_end: bool, 45 | pub list_item: bool, 46 | pub math_flow: bool, 47 | pub math_text: bool, 48 | pub thematic_break: bool, 49 | } 50 | 51 | impl Default for MdxConstructs { 52 | /// MDX with `CommonMark`. 53 | /// 54 | /// `CommonMark` is a relatively strong specification of how markdown 55 | /// works. 56 | /// Most markdown parsers try to follow it. 57 | /// 58 | /// For more information, see the `CommonMark` specification: 59 | /// . 60 | fn default() -> Self { 61 | Self { 62 | attention: true, 63 | block_quote: true, 64 | character_escape: true, 65 | character_reference: true, 66 | code_fenced: true, 67 | code_text: true, 68 | definition: true, 69 | frontmatter: false, 70 | gfm_autolink_literal: false, 71 | gfm_label_start_footnote: false, 72 | gfm_footnote_definition: false, 73 | gfm_strikethrough: false, 74 | gfm_table: false, 75 | gfm_task_list_item: false, 76 | hard_break_escape: true, 77 | hard_break_trailing: true, 78 | heading_atx: true, 79 | heading_setext: true, 80 | label_start_image: true, 81 | label_start_link: true, 82 | label_end: true, 83 | list_item: true, 84 | math_flow: false, 85 | math_text: false, 86 | thematic_break: true, 87 | } 88 | } 89 | } 90 | 91 | impl MdxConstructs { 92 | /// MDX with GFM. 93 | /// 94 | /// GFM stands for **GitHub flavored markdown**. 95 | /// GFM extends `CommonMark` and adds support for autolink literals, 96 | /// footnotes, strikethrough, tables, and tasklists. 97 | /// 98 | /// For more information, see the GFM specification: 99 | /// . 100 | pub fn gfm() -> Self { 101 | Self { 102 | gfm_autolink_literal: true, 103 | gfm_footnote_definition: true, 104 | gfm_label_start_footnote: true, 105 | gfm_strikethrough: true, 106 | gfm_table: true, 107 | gfm_task_list_item: true, 108 | ..Self::default() 109 | } 110 | } 111 | } 112 | 113 | // To do: link all docs when `markdown-rs` is stable. 114 | /// Like `ParseOptions` from `markdown-rs`. 115 | /// 116 | /// The constructs you can pass are limited. 117 | /// 118 | /// Additionally, you can’t use: 119 | /// 120 | /// * `mdx_expression_parse` 121 | /// * `mdx_esm_parse` 122 | #[derive(Clone, Debug, Eq, PartialEq)] 123 | #[cfg_attr(feature = "serializable", derive(serde::Serialize, serde::Deserialize))] 124 | #[cfg_attr(feature = "serializable", serde(rename_all = "camelCase", default))] 125 | pub struct MdxParseOptions { 126 | pub constructs: MdxConstructs, 127 | pub gfm_strikethrough_single_tilde: bool, 128 | pub math_text_single_dollar: bool, 129 | } 130 | 131 | impl Default for MdxParseOptions { 132 | /// MDX with `CommonMark` defaults. 133 | fn default() -> Self { 134 | Self { 135 | constructs: MdxConstructs::default(), 136 | gfm_strikethrough_single_tilde: true, 137 | math_text_single_dollar: true, 138 | } 139 | } 140 | } 141 | 142 | impl MdxParseOptions { 143 | /// MDX with GFM. 144 | /// 145 | /// GFM stands for GitHub flavored markdown. 146 | /// GFM extends `CommonMark` and adds support for autolink literals, 147 | /// footnotes, strikethrough, tables, and tasklists. 148 | /// 149 | /// For more information, see the GFM specification: 150 | /// 151 | pub fn gfm() -> Self { 152 | Self { 153 | constructs: MdxConstructs::gfm(), 154 | ..Self::default() 155 | } 156 | } 157 | } 158 | 159 | /// Configuration (optional). 160 | #[derive(Clone, Debug)] 161 | #[cfg_attr(feature = "serializable", derive(serde::Serialize, serde::Deserialize))] 162 | #[cfg_attr(feature = "serializable", serde(rename_all = "camelCase", default))] 163 | pub struct Options { 164 | /// Configuration that describes how to parse from markdown. 165 | pub parse: MdxParseOptions, 166 | 167 | /// Whether to add extra information to error messages in generated code 168 | /// (default: `false`). 169 | /// 170 | /// When in the automatic JSX runtime, this also enabled its development 171 | /// functionality. 172 | pub development: bool, 173 | 174 | // To do: some alternative to generate source maps. 175 | // SourceMapGenerator 176 | /// Place to import a provider from (default: `None`, example: 177 | /// `Some("@mdx-js/react").into()`). 178 | /// 179 | /// Useful for runtimes that support context (React, Preact). 180 | /// The provider must export a `useMDXComponents`, which is called to 181 | /// access an object of components. 182 | pub provider_import_source: Option, 183 | 184 | /// Whether to keep JSX (default: `false`). 185 | /// 186 | /// The default is to compile JSX away so that the resulting file is 187 | /// immediately runnable. 188 | pub jsx: bool, 189 | 190 | /// JSX runtime to use (default: `Some(JsxRuntime::Automatic)`). 191 | /// 192 | /// The classic runtime compiles to calls such as `h('p')`, the automatic 193 | /// runtime compiles to 194 | /// `import _jsx from '$importSource/jsx-runtime'\n_jsx('p')`. 195 | pub jsx_runtime: Option, 196 | 197 | /// Place to import automatic JSX runtimes from (`Option`, default: 198 | /// `Some("react".into())`). 199 | /// 200 | /// When in the automatic runtime, this is used to define an import for 201 | /// `_Fragment`, `_jsx`, and `_jsxs`. 202 | pub jsx_import_source: Option, 203 | 204 | /// Pragma for JSX (default: `Some("React.createElement".into())`). 205 | /// 206 | /// When in the classic runtime, this is used as an identifier for function 207 | /// calls: `` to `React.createElement('x')`. 208 | /// 209 | /// You should most probably define `pragma_frag` and `pragma_import_source` 210 | /// too when changing this. 211 | pub pragma: Option, 212 | 213 | /// Pragma for JSX fragments (default: `Some("React.Fragment".into())`). 214 | /// 215 | /// When in the classic runtime, this is used as an identifier for 216 | /// fragments: `<>` to `React.createElement(React.Fragment)`. 217 | /// 218 | /// You should most probably define `pragma` and `pragma_import_source` 219 | /// too when changing this. 220 | pub pragma_frag: Option, 221 | 222 | /// Where to import the identifier of `pragma` from (default: 223 | /// `Some("react".into())`). 224 | /// 225 | /// When in the classic runtime, this is used to import the `pragma` 226 | /// function. 227 | /// To illustrate with an example: when `pragma` is `"a.b"` and 228 | /// `pragma_import_source` is `"c"`, the following will be generated: 229 | /// `import a from 'c'`. 230 | pub pragma_import_source: Option, 231 | 232 | // New: 233 | /// File path to the source file (example: 234 | /// `Some("path/to/example.mdx".into())`). 235 | /// 236 | /// Used when `development: true` to improve error messages. 237 | pub filepath: Option, 238 | } 239 | 240 | impl Default for Options { 241 | /// Default options to use the automatic JSX runtime with React 242 | /// and handle MDX according to `CommonMark`. 243 | fn default() -> Self { 244 | Self { 245 | parse: MdxParseOptions::default(), 246 | development: false, 247 | provider_import_source: None, 248 | jsx: false, 249 | jsx_runtime: Some(JsxRuntime::default()), 250 | jsx_import_source: None, 251 | pragma: None, 252 | pragma_frag: None, 253 | pragma_import_source: None, 254 | filepath: None, 255 | } 256 | } 257 | } 258 | 259 | impl Options { 260 | /// MDX with GFM. 261 | /// 262 | /// GFM stands for GitHub flavored markdown. 263 | /// GFM extends `CommonMark` and adds support for autolink literals, 264 | /// footnotes, strikethrough, tables, and tasklists. 265 | /// On the compilation side, GFM turns on the GFM tag filter. 266 | /// The tagfilter is useless, but it’s included here for consistency. 267 | /// 268 | /// For more information, see the GFM specification: 269 | /// 270 | pub fn gfm() -> Self { 271 | Self { 272 | parse: MdxParseOptions::gfm(), 273 | ..Self::default() 274 | } 275 | } 276 | } 277 | 278 | #[cfg(test)] 279 | mod tests { 280 | use super::*; 281 | 282 | #[test] 283 | fn test_constructs() { 284 | let constructs = MdxConstructs::default(); 285 | assert!(constructs.attention, "should default to `CommonMark` (1)"); 286 | assert!( 287 | !constructs.gfm_autolink_literal, 288 | "should default to `CommonMark` (2)" 289 | ); 290 | assert!( 291 | !constructs.frontmatter, 292 | "should default to `CommonMark` (3)" 293 | ); 294 | 295 | let constructs = MdxConstructs::gfm(); 296 | assert!(constructs.attention, "should support `gfm` shortcut (1)"); 297 | assert!( 298 | constructs.gfm_autolink_literal, 299 | "should support `gfm` shortcut (2)" 300 | ); 301 | assert!(!constructs.frontmatter, "should support `gfm` shortcut (3)"); 302 | } 303 | 304 | #[test] 305 | fn test_parse_options() { 306 | let options = MdxParseOptions::default(); 307 | assert!( 308 | options.constructs.attention, 309 | "should default to `CommonMark` (1)" 310 | ); 311 | assert!( 312 | !options.constructs.gfm_autolink_literal, 313 | "should default to `CommonMark` (2)" 314 | ); 315 | assert!( 316 | !options.constructs.frontmatter, 317 | "should default to `CommonMark` (3)" 318 | ); 319 | 320 | let options = MdxParseOptions::gfm(); 321 | assert!( 322 | options.constructs.attention, 323 | "should support `gfm` shortcut (1)" 324 | ); 325 | assert!( 326 | options.constructs.gfm_autolink_literal, 327 | "should support `gfm` shortcut (2)" 328 | ); 329 | assert!( 330 | !options.constructs.frontmatter, 331 | "should support `gfm` shortcut (3)" 332 | ); 333 | } 334 | 335 | #[test] 336 | fn test_options() { 337 | let options = Options::default(); 338 | assert!( 339 | options.parse.constructs.attention, 340 | "should default to `CommonMark` (1)" 341 | ); 342 | assert!( 343 | !options.parse.constructs.gfm_autolink_literal, 344 | "should default to `CommonMark` (2)" 345 | ); 346 | assert!( 347 | !options.parse.constructs.frontmatter, 348 | "should default to `CommonMark` (3)" 349 | ); 350 | 351 | let options = Options::gfm(); 352 | assert!( 353 | options.parse.constructs.attention, 354 | "should support `gfm` shortcut (1)" 355 | ); 356 | assert!( 357 | options.parse.constructs.gfm_autolink_literal, 358 | "should support `gfm` shortcut (2)" 359 | ); 360 | assert!( 361 | !options.parse.constructs.frontmatter, 362 | "should support `gfm` shortcut (3)" 363 | ); 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /src/hast.rs: -------------------------------------------------------------------------------- 1 | //! HTML syntax tree: [hast][]. 2 | //! 3 | //! [hast]: https://github.com/syntax-tree/hast 4 | #![allow(dead_code)] 5 | #![allow(clippy::to_string_trait_impl)] 6 | 7 | extern crate alloc; 8 | extern crate markdown; 9 | #[allow(unused_imports)] 10 | pub use markdown::mdast::MdxJsxAttribute; 11 | pub use markdown::mdast::{AttributeContent, AttributeValue, Stop}; 12 | use markdown::unist::Position; 13 | 14 | /// Nodes. 15 | #[derive(Clone, PartialEq, Eq)] 16 | pub enum Node { 17 | /// Root. 18 | Root(Root), 19 | /// Element. 20 | Element(Element), 21 | /// Document type. 22 | Doctype(Doctype), 23 | /// Comment. 24 | Comment(Comment), 25 | /// Text. 26 | Text(Text), 27 | // MDX being passed through. 28 | /// MDX: JSX element. 29 | MdxJsxElement(MdxJsxElement), 30 | /// MDX.js ESM. 31 | MdxjsEsm(MdxjsEsm), 32 | // MDX: expression. 33 | MdxExpression(MdxExpression), 34 | } 35 | 36 | impl alloc::fmt::Debug for Node { 37 | /// Debug the wrapped struct. 38 | fn fmt(&self, f: &mut alloc::fmt::Formatter<'_>) -> alloc::fmt::Result { 39 | match self { 40 | Node::Root(x) => write!(f, "{:?}", x), 41 | Node::Element(x) => write!(f, "{:?}", x), 42 | Node::Doctype(x) => write!(f, "{:?}", x), 43 | Node::Comment(x) => write!(f, "{:?}", x), 44 | Node::Text(x) => write!(f, "{:?}", x), 45 | Node::MdxJsxElement(x) => write!(f, "{:?}", x), 46 | Node::MdxExpression(x) => write!(f, "{:?}", x), 47 | Node::MdxjsEsm(x) => write!(f, "{:?}", x), 48 | } 49 | } 50 | } 51 | 52 | /// Turn a slice of hast nodes into a string. 53 | fn children_to_string(children: &[Node]) -> String { 54 | children.iter().map(ToString::to_string).collect() 55 | } 56 | 57 | impl ToString for Node { 58 | /// Turn a hast node into a string. 59 | fn to_string(&self) -> String { 60 | match self { 61 | // Parents. 62 | Node::Root(x) => children_to_string(&x.children), 63 | Node::Element(x) => children_to_string(&x.children), 64 | Node::MdxJsxElement(x) => children_to_string(&x.children), 65 | // Literals. 66 | Node::Comment(x) => x.value.clone(), 67 | Node::Text(x) => x.value.clone(), 68 | Node::MdxExpression(x) => x.value.clone(), 69 | Node::MdxjsEsm(x) => x.value.clone(), 70 | // Voids. 71 | Node::Doctype(_) => String::new(), 72 | } 73 | } 74 | } 75 | 76 | impl Node { 77 | /// Get children of a hast node. 78 | #[must_use] 79 | pub fn children(&self) -> Option<&Vec> { 80 | match self { 81 | // Parent. 82 | Node::Root(x) => Some(&x.children), 83 | Node::Element(x) => Some(&x.children), 84 | Node::MdxJsxElement(x) => Some(&x.children), 85 | // Non-parent. 86 | _ => None, 87 | } 88 | } 89 | 90 | /// Get children of a hast node, mutably. 91 | pub fn children_mut(&mut self) -> Option<&mut Vec> { 92 | match self { 93 | // Parent. 94 | Node::Root(x) => Some(&mut x.children), 95 | Node::Element(x) => Some(&mut x.children), 96 | Node::MdxJsxElement(x) => Some(&mut x.children), 97 | // Non-parent. 98 | _ => None, 99 | } 100 | } 101 | 102 | /// Get the position of a hast node. 103 | pub fn position(&self) -> Option<&Position> { 104 | match self { 105 | Node::Root(x) => x.position.as_ref(), 106 | Node::Element(x) => x.position.as_ref(), 107 | Node::Doctype(x) => x.position.as_ref(), 108 | Node::Comment(x) => x.position.as_ref(), 109 | Node::Text(x) => x.position.as_ref(), 110 | Node::MdxJsxElement(x) => x.position.as_ref(), 111 | Node::MdxExpression(x) => x.position.as_ref(), 112 | Node::MdxjsEsm(x) => x.position.as_ref(), 113 | } 114 | } 115 | 116 | /// Get the position of a hast node, mutably. 117 | pub fn position_mut(&mut self) -> Option<&mut Position> { 118 | match self { 119 | Node::Root(x) => x.position.as_mut(), 120 | Node::Element(x) => x.position.as_mut(), 121 | Node::Doctype(x) => x.position.as_mut(), 122 | Node::Comment(x) => x.position.as_mut(), 123 | Node::Text(x) => x.position.as_mut(), 124 | Node::MdxJsxElement(x) => x.position.as_mut(), 125 | Node::MdxExpression(x) => x.position.as_mut(), 126 | Node::MdxjsEsm(x) => x.position.as_mut(), 127 | } 128 | } 129 | 130 | /// Set the position of a hast node. 131 | pub fn position_set(&mut self, position: Option) { 132 | match self { 133 | Node::Root(x) => x.position = position, 134 | Node::Element(x) => x.position = position, 135 | Node::Doctype(x) => x.position = position, 136 | Node::Comment(x) => x.position = position, 137 | Node::Text(x) => x.position = position, 138 | Node::MdxJsxElement(x) => x.position = position, 139 | Node::MdxExpression(x) => x.position = position, 140 | Node::MdxjsEsm(x) => x.position = position, 141 | } 142 | } 143 | } 144 | 145 | /// Document. 146 | /// 147 | /// ```html 148 | /// > | a 149 | /// ^ 150 | /// ``` 151 | #[derive(Clone, Debug, PartialEq, Eq)] 152 | pub struct Root { 153 | // Parent. 154 | /// Content model. 155 | pub children: Vec, 156 | /// Positional info. 157 | pub position: Option, 158 | } 159 | 160 | /// Document type. 161 | /// 162 | /// ```html 163 | /// > | 164 | /// ^^^^^^^^^^^^^^^ 165 | /// ``` 166 | #[derive(Clone, Debug, PartialEq, Eq)] 167 | pub struct Element { 168 | /// Tag name. 169 | pub tag_name: String, 170 | /// Properties. 171 | pub properties: Vec<(String, PropertyValue)>, 172 | // Parent. 173 | /// Children. 174 | pub children: Vec, 175 | /// Positional info. 176 | pub position: Option, 177 | } 178 | 179 | /// Property value. 180 | #[derive(Clone, Debug, PartialEq, Eq)] 181 | pub enum PropertyValue { 182 | /// A boolean. 183 | Boolean(bool), 184 | /// A string. 185 | String(String), 186 | /// A comma-separated list of strings. 187 | CommaSeparated(Vec), 188 | /// A space-separated list of strings. 189 | SpaceSeparated(Vec), 190 | } 191 | 192 | /// Document type. 193 | /// 194 | /// ```html 195 | /// > | 196 | /// ^^^^^^^^^^^^^^^ 197 | /// ``` 198 | #[derive(Clone, Debug, PartialEq, Eq)] 199 | pub struct Doctype { 200 | // Void. 201 | /// Positional info. 202 | pub position: Option, 203 | } 204 | 205 | /// Comment. 206 | /// 207 | /// ```html 208 | /// > | 209 | /// ^^^^^^^^^^ 210 | /// ``` 211 | #[derive(Clone, Debug, PartialEq, Eq)] 212 | pub struct Comment { 213 | // Text. 214 | /// Content model. 215 | pub value: String, 216 | /// Positional info. 217 | pub position: Option, 218 | } 219 | 220 | /// Text. 221 | /// 222 | /// ```html 223 | /// > | a 224 | /// ^ 225 | /// ``` 226 | #[derive(Clone, Debug, PartialEq, Eq)] 227 | pub struct Text { 228 | // Text. 229 | /// Content model. 230 | pub value: String, 231 | /// Positional info. 232 | pub position: Option, 233 | } 234 | 235 | /// MDX: JSX element. 236 | /// 237 | /// ```markdown 238 | /// > | 239 | /// ^^^^^ 240 | /// ``` 241 | #[derive(Clone, Debug, PartialEq, Eq)] 242 | pub struct MdxJsxElement { 243 | // JSX element. 244 | /// Name. 245 | /// 246 | /// Fragments have no name. 247 | pub name: Option, 248 | /// Attributes. 249 | pub attributes: Vec, 250 | // Parent. 251 | /// Content model. 252 | pub children: Vec, 253 | /// Positional info. 254 | pub position: Option, 255 | } 256 | 257 | /// MDX: expression. 258 | /// 259 | /// ```markdown 260 | /// > | {a} 261 | /// ^^^ 262 | /// ``` 263 | #[derive(Clone, Debug, PartialEq, Eq)] 264 | pub struct MdxExpression { 265 | // Literal. 266 | /// Content model. 267 | pub value: String, 268 | /// Positional info. 269 | pub position: Option, 270 | 271 | /// Custom data on where each slice of `value` came from. 272 | pub stops: Vec, 273 | } 274 | 275 | /// MDX: ESM. 276 | /// 277 | /// ```markdown 278 | /// > | import a from 'b' 279 | /// ^^^^^^^^^^^^^^^^^ 280 | /// ``` 281 | #[derive(Clone, Debug, PartialEq, Eq)] 282 | pub struct MdxjsEsm { 283 | // Literal. 284 | /// Content model. 285 | pub value: String, 286 | /// Positional info. 287 | pub position: Option, 288 | 289 | /// Custom data on where each slice of `value` came from. 290 | pub stops: Vec, 291 | } 292 | 293 | #[cfg(test)] 294 | mod tests { 295 | use super::*; 296 | use markdown::unist::Position; 297 | use pretty_assertions::assert_eq; 298 | 299 | // Literals. 300 | 301 | #[test] 302 | fn text() { 303 | let mut node = Node::Text(Text { 304 | value: "a".into(), 305 | position: None, 306 | }); 307 | 308 | assert_eq!( 309 | format!("{:?}", node), 310 | "Text { value: \"a\", position: None }", 311 | "should support `Debug`" 312 | ); 313 | assert_eq!(node.to_string(), "a", "should support `ToString`"); 314 | assert_eq!(node.children_mut(), None, "should support `children_mut`"); 315 | assert_eq!(node.children(), None, "should support `children`"); 316 | assert_eq!(node.position(), None, "should support `position`"); 317 | assert_eq!(node.position_mut(), None, "should support `position`"); 318 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 319 | assert_eq!( 320 | format!("{:?}", node), 321 | "Text { value: \"a\", position: Some(1:1-1:2 (0-1)) }", 322 | "should support `position_set`" 323 | ); 324 | } 325 | 326 | #[test] 327 | fn comment() { 328 | let mut node = Node::Comment(Comment { 329 | value: "a".into(), 330 | position: None, 331 | }); 332 | 333 | assert_eq!( 334 | format!("{:?}", node), 335 | "Comment { value: \"a\", position: None }", 336 | "should support `Debug`" 337 | ); 338 | assert_eq!(node.to_string(), "a", "should support `ToString`"); 339 | assert_eq!(node.children_mut(), None, "should support `children_mut`"); 340 | assert_eq!(node.children(), None, "should support `children`"); 341 | assert_eq!(node.position(), None, "should support `position`"); 342 | assert_eq!(node.position_mut(), None, "should support `position`"); 343 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 344 | assert_eq!( 345 | format!("{:?}", node), 346 | "Comment { value: \"a\", position: Some(1:1-1:2 (0-1)) }", 347 | "should support `position_set`" 348 | ); 349 | } 350 | 351 | #[test] 352 | fn mdx_expression() { 353 | let mut node = Node::MdxExpression(MdxExpression { 354 | value: "a".into(), 355 | stops: vec![], 356 | position: None, 357 | }); 358 | 359 | assert_eq!( 360 | format!("{:?}", node), 361 | "MdxExpression { value: \"a\", position: None, stops: [] }", 362 | "should support `Debug`" 363 | ); 364 | assert_eq!(node.to_string(), "a", "should support `ToString`"); 365 | assert_eq!(node.children_mut(), None, "should support `children_mut`"); 366 | assert_eq!(node.children(), None, "should support `children`"); 367 | assert_eq!(node.position(), None, "should support `position`"); 368 | assert_eq!(node.position_mut(), None, "should support `position`"); 369 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 370 | assert_eq!( 371 | format!("{:?}", node), 372 | "MdxExpression { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }", 373 | "should support `position_set`" 374 | ); 375 | } 376 | 377 | #[test] 378 | fn mdxjs_esm() { 379 | let mut node = Node::MdxjsEsm(MdxjsEsm { 380 | value: "a".into(), 381 | stops: vec![], 382 | position: None, 383 | }); 384 | 385 | assert_eq!( 386 | format!("{:?}", node), 387 | "MdxjsEsm { value: \"a\", position: None, stops: [] }", 388 | "should support `Debug`" 389 | ); 390 | assert_eq!(node.to_string(), "a", "should support `ToString`"); 391 | assert_eq!(node.children_mut(), None, "should support `children_mut`"); 392 | assert_eq!(node.children(), None, "should support `children`"); 393 | assert_eq!(node.position(), None, "should support `position`"); 394 | assert_eq!(node.position_mut(), None, "should support `position`"); 395 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 396 | assert_eq!( 397 | format!("{:?}", node), 398 | "MdxjsEsm { value: \"a\", position: Some(1:1-1:2 (0-1)), stops: [] }", 399 | "should support `position_set`" 400 | ); 401 | } 402 | 403 | // Voids. 404 | 405 | #[test] 406 | fn doctype() { 407 | let mut node = Node::Doctype(Doctype { position: None }); 408 | 409 | assert_eq!( 410 | format!("{:?}", node), 411 | "Doctype { position: None }", 412 | "should support `Debug`" 413 | ); 414 | assert_eq!(node.to_string(), "", "should support `ToString`"); 415 | assert_eq!(node.children_mut(), None, "should support `children_mut`"); 416 | assert_eq!(node.children(), None, "should support `children`"); 417 | assert_eq!(node.position(), None, "should support `position`"); 418 | assert_eq!(node.position_mut(), None, "should support `position`"); 419 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 420 | assert_eq!( 421 | format!("{:?}", node), 422 | "Doctype { position: Some(1:1-1:2 (0-1)) }", 423 | "should support `position_set`" 424 | ); 425 | } 426 | 427 | // Parents. 428 | 429 | #[test] 430 | fn root() { 431 | let mut node = Node::Root(Root { 432 | position: None, 433 | children: vec![], 434 | }); 435 | 436 | assert_eq!( 437 | format!("{:?}", node), 438 | "Root { children: [], position: None }", 439 | "should support `Debug`" 440 | ); 441 | assert_eq!(node.to_string(), "", "should support `ToString`"); 442 | assert_eq!( 443 | node.children_mut(), 444 | Some(&mut vec![]), 445 | "should support `children_mut`" 446 | ); 447 | assert_eq!(node.children(), Some(&vec![]), "should support `children`"); 448 | assert_eq!(node.position(), None, "should support `position`"); 449 | assert_eq!(node.position_mut(), None, "should support `position`"); 450 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 451 | assert_eq!( 452 | format!("{:?}", node), 453 | "Root { children: [], position: Some(1:1-1:2 (0-1)) }", 454 | "should support `position_set`" 455 | ); 456 | } 457 | 458 | #[test] 459 | fn element() { 460 | let mut node = Node::Element(Element { 461 | tag_name: "a".into(), 462 | properties: vec![], 463 | position: None, 464 | children: vec![], 465 | }); 466 | 467 | assert_eq!( 468 | format!("{:?}", node), 469 | "Element { tag_name: \"a\", properties: [], children: [], position: None }", 470 | "should support `Debug`" 471 | ); 472 | assert_eq!(node.to_string(), "", "should support `ToString`"); 473 | assert_eq!( 474 | node.children_mut(), 475 | Some(&mut vec![]), 476 | "should support `children_mut`" 477 | ); 478 | assert_eq!(node.children(), Some(&vec![]), "should support `children`"); 479 | assert_eq!(node.position(), None, "should support `position`"); 480 | assert_eq!(node.position_mut(), None, "should support `position`"); 481 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 482 | assert_eq!( 483 | format!("{:?}", node), 484 | "Element { tag_name: \"a\", properties: [], children: [], position: Some(1:1-1:2 (0-1)) }", 485 | "should support `position_set`" 486 | ); 487 | } 488 | 489 | #[test] 490 | fn mdx_jsx_element() { 491 | let mut node = Node::MdxJsxElement(MdxJsxElement { 492 | name: None, 493 | attributes: vec![], 494 | position: None, 495 | children: vec![], 496 | }); 497 | 498 | assert_eq!( 499 | format!("{:?}", node), 500 | "MdxJsxElement { name: None, attributes: [], children: [], position: None }", 501 | "should support `Debug`" 502 | ); 503 | assert_eq!(node.to_string(), "", "should support `ToString`"); 504 | assert_eq!( 505 | node.children_mut(), 506 | Some(&mut vec![]), 507 | "should support `children_mut`" 508 | ); 509 | assert_eq!(node.children(), Some(&vec![]), "should support `children`"); 510 | assert_eq!(node.position(), None, "should support `position`"); 511 | assert_eq!(node.position_mut(), None, "should support `position`"); 512 | node.position_set(Some(Position::new(1, 1, 0, 1, 2, 1))); 513 | assert_eq!( 514 | format!("{:?}", node), 515 | "MdxJsxElement { name: None, attributes: [], children: [], position: Some(1:1-1:2 (0-1)) }", 516 | "should support `position_set`" 517 | ); 518 | } 519 | } 520 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Public API of `mdxjs-rs`. 2 | //! 3 | //! This module exposes primarily [`compile()`][]. 4 | //! 5 | //! * [`compile()`][] 6 | //! — turn MDX into JavaScript 7 | #![deny(clippy::pedantic)] 8 | #![allow(clippy::implicit_hasher)] 9 | #![allow(clippy::must_use_candidate)] 10 | #![allow(clippy::too_many_lines)] 11 | #![allow(clippy::struct_excessive_bools)] 12 | #![allow(clippy::cast_possible_truncation)] 13 | #![allow(clippy::cast_precision_loss)] 14 | 15 | extern crate markdown; 16 | mod configuration; 17 | pub mod hast; 18 | mod hast_util_to_swc; 19 | mod mdast_util_to_hast; 20 | mod mdx_plugin_recma_document; 21 | mod mdx_plugin_recma_jsx_rewrite; 22 | mod swc; 23 | mod swc_util_build_jsx; 24 | mod swc_utils; 25 | 26 | use crate::{ 27 | hast_util_to_swc::hast_util_to_swc as to_swc, 28 | mdx_plugin_recma_document::{ 29 | mdx_plugin_recma_document as recma_document, Options as DocumentOptions, 30 | }, 31 | mdx_plugin_recma_jsx_rewrite::{ 32 | mdx_plugin_recma_jsx_rewrite as recma_jsx_rewrite, Options as RewriteOptions, 33 | }, 34 | swc::{parse_esm, parse_expression, serialize}, 35 | swc_util_build_jsx::{swc_util_build_jsx, Options as BuildOptions}, 36 | }; 37 | use hast_util_to_swc::Program; 38 | use markdown::{ 39 | message::{self, Message}, 40 | to_mdast, Constructs, Location, ParseOptions, 41 | }; 42 | use rustc_hash::FxHashSet; 43 | use swc_core::common::Span; 44 | 45 | pub use crate::configuration::{MdxConstructs, MdxParseOptions, Options}; 46 | pub use crate::mdast_util_to_hast::mdast_util_to_hast; 47 | pub use crate::mdx_plugin_recma_document::JsxRuntime; 48 | 49 | /// Turn MDX into JavaScript. 50 | /// 51 | /// ## Examples 52 | /// 53 | /// ``` 54 | /// use mdxjs::compile; 55 | /// # fn main() -> Result<(), markdown::message::Message> { 56 | /// 57 | /// assert_eq!(compile("# Hi!", &Default::default())?, "import { jsx as _jsx } from \"react/jsx-runtime\";\nfunction _createMdxContent(props) {\n const _components = Object.assign({\n h1: \"h1\"\n }, props.components);\n return _jsx(_components.h1, {\n children: \"Hi!\"\n });\n}\nfunction MDXContent(props = {}) {\n const { wrapper: MDXLayout } = props.components || {};\n return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, {\n children: _jsx(_createMdxContent, props)\n })) : _createMdxContent(props);\n}\nexport default MDXContent;\n"); 58 | /// # Ok(()) 59 | /// # } 60 | /// ``` 61 | /// 62 | /// ## Errors 63 | /// 64 | /// This project errors for many different reasons, such as syntax errors in 65 | /// the MDX format or misconfiguration. 66 | pub fn compile(value: &str, options: &Options) -> Result { 67 | let mdast = mdast_util_from_mdx(value, options)?; 68 | let hast = mdast_util_to_hast(&mdast); 69 | let location = Location::new(value.as_bytes()); 70 | let mut explicit_jsxs = FxHashSet::default(); 71 | let mut program = hast_util_to_swc(&hast, options, Some(&location), &mut explicit_jsxs)?; 72 | mdx_plugin_recma_document(&mut program, options, Some(&location))?; 73 | mdx_plugin_recma_jsx_rewrite(&mut program, options, Some(&location), &explicit_jsxs)?; 74 | Ok(serialize(&mut program.module, Some(&program.comments))) 75 | } 76 | 77 | /// Turn MDX into a syntax tree. 78 | /// 79 | /// ## Errors 80 | /// 81 | /// There are several errors that can occur with how 82 | /// JSX, expressions, or ESM are written. 83 | /// 84 | /// ## Examples 85 | /// 86 | /// ``` 87 | /// use mdxjs::{mdast_util_from_mdx, Options}; 88 | /// # fn main() -> Result<(), markdown::message::Message> { 89 | /// 90 | /// let tree = mdast_util_from_mdx("# Hey, *you*!", &Options::default())?; 91 | /// 92 | /// println!("{:?}", tree); 93 | /// // => Root { children: [Heading { children: [Text { value: "Hey, ", position: Some(1:3-1:8 (2-7)) }, Emphasis { children: [Text { value: "you", position: Some(1:9-1:12 (8-11)) }], position: Some(1:8-1:13 (7-12)) }, Text { value: "!", position: Some(1:13-1:14 (12-13)) }], position: Some(1:1-1:14 (0-13)), depth: 1 }], position: Some(1:1-1:14 (0-13)) } 94 | /// # Ok(()) 95 | /// # } 96 | /// ``` 97 | pub fn mdast_util_from_mdx( 98 | value: &str, 99 | options: &Options, 100 | ) -> Result { 101 | let parse_options = ParseOptions { 102 | constructs: Constructs { 103 | attention: options.parse.constructs.attention, 104 | autolink: false, 105 | block_quote: options.parse.constructs.block_quote, 106 | character_escape: options.parse.constructs.character_escape, 107 | character_reference: options.parse.constructs.character_reference, 108 | code_fenced: options.parse.constructs.code_fenced, 109 | code_indented: false, 110 | code_text: options.parse.constructs.code_text, 111 | definition: options.parse.constructs.definition, 112 | frontmatter: options.parse.constructs.frontmatter, 113 | gfm_autolink_literal: options.parse.constructs.gfm_autolink_literal, 114 | gfm_footnote_definition: options.parse.constructs.gfm_footnote_definition, 115 | gfm_label_start_footnote: options.parse.constructs.gfm_label_start_footnote, 116 | gfm_strikethrough: options.parse.constructs.gfm_strikethrough, 117 | gfm_table: options.parse.constructs.gfm_table, 118 | gfm_task_list_item: options.parse.constructs.gfm_task_list_item, 119 | hard_break_escape: options.parse.constructs.hard_break_escape, 120 | hard_break_trailing: options.parse.constructs.hard_break_trailing, 121 | html_flow: false, 122 | html_text: false, 123 | heading_atx: options.parse.constructs.heading_atx, 124 | heading_setext: options.parse.constructs.heading_setext, 125 | label_start_image: options.parse.constructs.label_start_image, 126 | label_start_link: options.parse.constructs.label_start_link, 127 | label_end: options.parse.constructs.label_end, 128 | list_item: options.parse.constructs.list_item, 129 | math_flow: options.parse.constructs.math_flow, 130 | math_text: options.parse.constructs.math_text, 131 | mdx_esm: true, 132 | mdx_expression_flow: true, 133 | mdx_expression_text: true, 134 | mdx_jsx_flow: true, 135 | mdx_jsx_text: true, 136 | thematic_break: options.parse.constructs.thematic_break, 137 | }, 138 | gfm_strikethrough_single_tilde: options.parse.gfm_strikethrough_single_tilde, 139 | math_text_single_dollar: options.parse.math_text_single_dollar, 140 | mdx_esm_parse: Some(Box::new(parse_esm)), 141 | mdx_expression_parse: Some(Box::new(parse_expression)), 142 | }; 143 | 144 | to_mdast(value, &parse_options) 145 | } 146 | 147 | /// Compile hast into SWC’s ES AST. 148 | /// 149 | /// ## Errors 150 | /// 151 | /// This function currently does not emit errors. 152 | pub fn hast_util_to_swc( 153 | hast: &hast::Node, 154 | options: &Options, 155 | location: Option<&Location>, 156 | explicit_jsxs: &mut FxHashSet, 157 | ) -> Result { 158 | to_swc(hast, options.filepath.clone(), location, explicit_jsxs) 159 | } 160 | 161 | /// Wrap the SWC ES AST nodes coming from hast into a whole document. 162 | /// 163 | /// ## Errors 164 | /// 165 | /// This functions errors for double layouts (default exports). 166 | pub fn mdx_plugin_recma_document( 167 | program: &mut Program, 168 | options: &Options, 169 | location: Option<&Location>, 170 | ) -> Result<(), markdown::message::Message> { 171 | let document_options = DocumentOptions { 172 | pragma: options.pragma.clone(), 173 | pragma_frag: options.pragma_frag.clone(), 174 | pragma_import_source: options.pragma_import_source.clone(), 175 | jsx_import_source: options.jsx_import_source.clone(), 176 | jsx_runtime: options.jsx_runtime, 177 | }; 178 | recma_document(program, &document_options, location) 179 | } 180 | 181 | /// Rewrite JSX in an MDX file so that components can be passed in and provided. 182 | /// Also compiles JSX to function calls unless `options.jsx` is true. 183 | /// 184 | /// ## Errors 185 | /// 186 | /// This functions errors for incorrect JSX runtime configuration *inside* 187 | /// MDX files and problems with SWC (broken JS syntax). 188 | pub fn mdx_plugin_recma_jsx_rewrite( 189 | program: &mut Program, 190 | options: &Options, 191 | location: Option<&Location>, 192 | explicit_jsxs: &FxHashSet, 193 | ) -> Result<(), markdown::message::Message> { 194 | let rewrite_options = RewriteOptions { 195 | development: options.development, 196 | provider_import_source: options.provider_import_source.clone(), 197 | }; 198 | 199 | recma_jsx_rewrite(program, &rewrite_options, location, explicit_jsxs); 200 | 201 | if !options.jsx { 202 | let build_options = BuildOptions { 203 | development: options.development, 204 | }; 205 | 206 | swc_util_build_jsx(program, &build_options, location)?; 207 | } 208 | 209 | Ok(()) 210 | } 211 | -------------------------------------------------------------------------------- /src/mdx_plugin_recma_document.rs: -------------------------------------------------------------------------------- 1 | //! Turn a JavaScript AST, coming from MD(X), into a component. 2 | //! 3 | //! Port of , 4 | //! by the same author. 5 | 6 | use crate::hast_util_to_swc::Program; 7 | use crate::swc_utils::{ 8 | bytepos_to_point, create_call_expression, create_ident, create_ident_expression, 9 | create_null_expression, create_object_expression, create_str, position_opt_to_string, 10 | span_to_position, 11 | }; 12 | use markdown::{ 13 | unist::{Point, Position}, 14 | Location, 15 | }; 16 | use swc_core::common::SyntaxContext; 17 | use swc_core::ecma::ast::{ 18 | AssignPat, BindingIdent, BlockStmt, Callee, CondExpr, Decl, DefaultDecl, ExportDefaultExpr, 19 | ExportSpecifier, Expr, ExprOrSpread, FnDecl, Function, ImportDecl, ImportDefaultSpecifier, 20 | ImportNamedSpecifier, ImportPhase, ImportSpecifier, JSXAttrOrSpread, JSXClosingElement, 21 | JSXElement, JSXElementChild, JSXElementName, JSXOpeningElement, ModuleDecl, ModuleExportName, 22 | ModuleItem, Param, Pat, ReturnStmt, SpreadElement, Stmt, VarDecl, VarDeclKind, VarDeclarator, 23 | }; 24 | /// JSX runtimes (default: `JsxRuntime::Automatic`). 25 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 26 | #[cfg_attr(feature = "serializable", derive(serde::Serialize, serde::Deserialize))] 27 | #[cfg_attr(feature = "serializable", serde(rename_all = "camelCase"))] 28 | pub enum JsxRuntime { 29 | /// Automatic runtime. 30 | /// 31 | /// With the automatic runtime, some module is expected to exist somewhere. 32 | /// That modules is expected to expose a certain API. 33 | /// The compiler adds an import of that module and compiles JSX away to 34 | /// function calls that use that API. 35 | #[default] 36 | Automatic, 37 | /// Classic runtime. 38 | /// 39 | /// With the classic runtime, you define two values yourself in each file, 40 | /// which are expected to work a certain way. 41 | /// The compiler compiles JSX away to function calls using those two values. 42 | Classic, 43 | } 44 | 45 | /// Configuration. 46 | #[derive(Debug, PartialEq, Eq)] 47 | pub struct Options { 48 | /// Pragma for JSX (used in classic runtime). 49 | /// 50 | /// Default: `React.createElement`. 51 | pub pragma: Option, 52 | /// Pragma for JSX fragments (used in classic runtime). 53 | /// 54 | /// Default: `React.Fragment`. 55 | pub pragma_frag: Option, 56 | /// Where to import the identifier of `pragma` from (used in classic runtime). 57 | /// 58 | /// Default: `react`. 59 | pub pragma_import_source: Option, 60 | /// Place to import automatic JSX runtimes from (used in automatic runtime). 61 | /// 62 | /// Default: `react`. 63 | pub jsx_import_source: Option, 64 | /// JSX runtime to use. 65 | /// 66 | /// Default: `automatic`. 67 | pub jsx_runtime: Option, 68 | } 69 | 70 | impl Default for Options { 71 | /// Use the automatic JSX runtime with React. 72 | fn default() -> Self { 73 | Self { 74 | pragma: None, 75 | pragma_frag: None, 76 | pragma_import_source: None, 77 | jsx_import_source: None, 78 | jsx_runtime: Some(JsxRuntime::default()), 79 | } 80 | } 81 | } 82 | 83 | /// Wrap the SWC ES AST nodes coming from hast into a whole document. 84 | pub fn mdx_plugin_recma_document( 85 | program: &mut Program, 86 | options: &Options, 87 | location: Option<&Location>, 88 | ) -> Result<(), markdown::message::Message> { 89 | // New body children. 90 | let mut replacements = vec![]; 91 | 92 | // Inject JSX configuration comment. 93 | if let Some(runtime) = &options.jsx_runtime { 94 | let mut pragmas = vec![]; 95 | let react = &"react".into(); 96 | let create_element = &"React.createElement".into(); 97 | let fragment = &"React.Fragment".into(); 98 | 99 | if *runtime == JsxRuntime::Automatic { 100 | pragmas.push("@jsxRuntime automatic".into()); 101 | pragmas.push(format!( 102 | "@jsxImportSource {}", 103 | if let Some(jsx_import_source) = &options.jsx_import_source { 104 | jsx_import_source 105 | } else { 106 | react 107 | } 108 | )); 109 | } else { 110 | pragmas.push("@jsxRuntime classic".into()); 111 | pragmas.push(format!( 112 | "@jsx {}", 113 | if let Some(pragma) = &options.pragma { 114 | pragma 115 | } else { 116 | create_element 117 | } 118 | )); 119 | pragmas.push(format!( 120 | "@jsxFrag {}", 121 | if let Some(pragma_frag) = &options.pragma_frag { 122 | pragma_frag 123 | } else { 124 | fragment 125 | } 126 | )); 127 | } 128 | 129 | if !pragmas.is_empty() { 130 | program.comments.insert( 131 | 0, 132 | swc_core::common::comments::Comment { 133 | kind: swc_core::common::comments::CommentKind::Block, 134 | text: pragmas.join(" ").into(), 135 | span: swc_core::common::DUMMY_SP, 136 | }, 137 | ); 138 | } 139 | } 140 | 141 | // Inject an import in the classic runtime for the pragma (and presumably, 142 | // fragment). 143 | if options.jsx_runtime == Some(JsxRuntime::Classic) { 144 | let pragma = if let Some(pragma) = &options.pragma { 145 | pragma 146 | } else { 147 | "React" 148 | }; 149 | let sym = pragma.split('.').next().expect("first item always exists"); 150 | 151 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 152 | specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier { 153 | local: create_ident(sym).into(), 154 | span: swc_core::common::DUMMY_SP, 155 | })], 156 | src: Box::new(create_str( 157 | if let Some(source) = &options.pragma_import_source { 158 | source 159 | } else { 160 | "react" 161 | }, 162 | )), 163 | type_only: false, 164 | with: None, 165 | phase: ImportPhase::default(), 166 | span: swc_core::common::DUMMY_SP, 167 | }))); 168 | } 169 | 170 | // Find the `export default`, the JSX expression, and leave the rest as it 171 | // is. 172 | let mut input = program.module.body.split_off(0); 173 | input.reverse(); 174 | let mut layout = false; 175 | let mut layout_position = None; 176 | let mut content = false; 177 | 178 | while let Some(module_item) = input.pop() { 179 | match module_item { 180 | // ```js 181 | // export default props => <>{props.children} 182 | // ``` 183 | // 184 | // Treat it as an inline layout declaration. 185 | // 186 | // In estree, the below two are the same node (`ExportDefault`). 187 | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultDecl(decl)) => { 188 | err_for_double_layout( 189 | layout, 190 | layout_position.as_ref(), 191 | bytepos_to_point(decl.span.lo, location).as_ref(), 192 | )?; 193 | layout = true; 194 | layout_position = span_to_position(decl.span, location); 195 | match decl.decl { 196 | DefaultDecl::Class(cls) => { 197 | replacements.push(create_layout_decl(Expr::Class(cls))); 198 | } 199 | DefaultDecl::Fn(func) => { 200 | replacements.push(create_layout_decl(Expr::Fn(func))); 201 | } 202 | DefaultDecl::TsInterfaceDecl(_) => { 203 | return Err( 204 | markdown::message::Message { 205 | reason: "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(), 206 | place: bytepos_to_point(decl.span.lo, location).map(|p| Box::new(markdown::message::Place::Point(p))), 207 | source: Box::new("mdxjs-rs".into()), 208 | rule_id: Box::new("ts-interface".into()), 209 | } 210 | ); 211 | } 212 | } 213 | } 214 | ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(expr)) => { 215 | err_for_double_layout( 216 | layout, 217 | layout_position.as_ref(), 218 | bytepos_to_point(expr.span.lo, location).as_ref(), 219 | )?; 220 | layout = true; 221 | layout_position = span_to_position(expr.span, location); 222 | replacements.push(create_layout_decl(*expr.expr)); 223 | } 224 | // ```js 225 | // export {a, b as c} from 'd' 226 | // export {a, b as c} 227 | // ``` 228 | ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(mut named_export)) => { 229 | let mut index = 0; 230 | let mut id = None; 231 | 232 | while index < named_export.specifiers.len() { 233 | let mut take = false; 234 | // Note: the `ExportSpecifier::Default` 235 | // branch of this looks interesting, but as far as I 236 | // understand it *is not* valid ES. 237 | // `export a from 'b'` is a syntax error, even in SWC. 238 | if let ExportSpecifier::Named(named) = &named_export.specifiers[index] { 239 | if let Some(ModuleExportName::Ident(ident)) = &named.exported { 240 | if &ident.sym == "default" { 241 | // For some reason the AST supports strings 242 | // instead of identifiers. 243 | // Looks like some TC39 proposal. Ignore for now 244 | // and only do things if this is an ID. 245 | if let ModuleExportName::Ident(ident) = &named.orig { 246 | err_for_double_layout( 247 | layout, 248 | layout_position.as_ref(), 249 | bytepos_to_point(ident.span.lo, location).as_ref(), 250 | )?; 251 | layout = true; 252 | layout_position = span_to_position(ident.span, location); 253 | take = true; 254 | id = Some(ident.clone()); 255 | } 256 | } 257 | } 258 | } 259 | 260 | if take { 261 | named_export.specifiers.remove(index); 262 | } else { 263 | index += 1; 264 | } 265 | } 266 | 267 | if let Some(id) = id { 268 | let source = named_export.src.clone(); 269 | 270 | // If there was just a default export, we can drop the original node. 271 | if !named_export.specifiers.is_empty() { 272 | // Pass through. 273 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( 274 | named_export, 275 | ))); 276 | } 277 | 278 | // It’s an `export {x} from 'y'`, so generate an import. 279 | if let Some(source) = source { 280 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 281 | specifiers: vec![ImportSpecifier::Named(ImportNamedSpecifier { 282 | local: create_ident("MDXLayout").into(), 283 | imported: Some(ModuleExportName::Ident(id)), 284 | span: swc_core::common::DUMMY_SP, 285 | is_type_only: false, 286 | })], 287 | src: source, 288 | type_only: false, 289 | with: None, 290 | phase: ImportPhase::default(), 291 | span: swc_core::common::DUMMY_SP, 292 | }))); 293 | } 294 | // It’s an `export {x}`, so generate a variable declaration. 295 | else { 296 | replacements.push(create_layout_decl(create_ident_expression(&id.sym))); 297 | } 298 | } else { 299 | // Pass through. 300 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( 301 | named_export, 302 | ))); 303 | } 304 | } 305 | ModuleItem::ModuleDecl(ModuleDecl::Import(x)) => { 306 | // Pass through. 307 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::Import(x))); 308 | } 309 | ModuleItem::ModuleDecl( 310 | ModuleDecl::ExportDecl(_) 311 | | ModuleDecl::ExportAll(_) 312 | | ModuleDecl::TsImportEquals(_) 313 | | ModuleDecl::TsExportAssignment(_) 314 | | ModuleDecl::TsNamespaceExport(_), 315 | ) => { 316 | // Pass through. 317 | replacements.push(module_item); 318 | } 319 | ModuleItem::Stmt(Stmt::Expr(expr_stmt)) => { 320 | match *expr_stmt.expr { 321 | Expr::JSXElement(elem) => { 322 | content = true; 323 | replacements.append(&mut create_mdx_content( 324 | Some(Expr::JSXElement(elem)), 325 | layout, 326 | )); 327 | } 328 | Expr::JSXFragment(mut frag) => { 329 | // Unwrap if possible. 330 | if frag.children.len() == 1 { 331 | let item = frag.children.pop().unwrap(); 332 | 333 | if let JSXElementChild::JSXElement(elem) = item { 334 | content = true; 335 | replacements.append(&mut create_mdx_content( 336 | Some(Expr::JSXElement(elem)), 337 | layout, 338 | )); 339 | continue; 340 | } 341 | 342 | frag.children.push(item); 343 | } 344 | 345 | content = true; 346 | replacements.append(&mut create_mdx_content( 347 | Some(Expr::JSXFragment(frag)), 348 | layout, 349 | )); 350 | } 351 | _ => { 352 | // Pass through. 353 | replacements.push(ModuleItem::Stmt(Stmt::Expr(expr_stmt))); 354 | } 355 | } 356 | } 357 | ModuleItem::Stmt(stmt) => { 358 | replacements.push(ModuleItem::Stmt(stmt)); 359 | } 360 | } 361 | } 362 | 363 | // Generate an empty component. 364 | if !content { 365 | replacements.append(&mut create_mdx_content(None, layout)); 366 | } 367 | 368 | // ```jsx 369 | // export default MDXContent 370 | // ``` 371 | replacements.push(ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr( 372 | ExportDefaultExpr { 373 | expr: Box::new(create_ident_expression("MDXContent")), 374 | span: swc_core::common::DUMMY_SP, 375 | }, 376 | ))); 377 | 378 | program.module.body = replacements; 379 | 380 | Ok(()) 381 | } 382 | 383 | /// Create a content component. 384 | fn create_mdx_content(expr: Option, has_internal_layout: bool) -> Vec { 385 | // ```jsx 386 | // xxx 387 | // ``` 388 | let mut result = Expr::JSXElement(Box::new(JSXElement { 389 | opening: JSXOpeningElement { 390 | name: JSXElementName::Ident(create_ident("MDXLayout").into()), 391 | attrs: vec![JSXAttrOrSpread::SpreadElement(SpreadElement { 392 | dot3_token: swc_core::common::DUMMY_SP, 393 | expr: Box::new(create_ident_expression("props")), 394 | })], 395 | self_closing: false, 396 | type_args: None, 397 | span: swc_core::common::DUMMY_SP, 398 | }, 399 | closing: Some(JSXClosingElement { 400 | name: JSXElementName::Ident(create_ident("MDXLayout").into()), 401 | span: swc_core::common::DUMMY_SP, 402 | }), 403 | // ```jsx 404 | // <_createMdxContent {...props} /> 405 | // ``` 406 | children: vec![JSXElementChild::JSXElement(Box::new(JSXElement { 407 | opening: JSXOpeningElement { 408 | name: JSXElementName::Ident(create_ident("_createMdxContent").into()), 409 | attrs: vec![JSXAttrOrSpread::SpreadElement(SpreadElement { 410 | dot3_token: swc_core::common::DUMMY_SP, 411 | expr: Box::new(create_ident_expression("props")), 412 | })], 413 | self_closing: true, 414 | type_args: None, 415 | span: swc_core::common::DUMMY_SP, 416 | }, 417 | closing: None, 418 | children: vec![], 419 | span: swc_core::common::DUMMY_SP, 420 | }))], 421 | span: swc_core::common::DUMMY_SP, 422 | })); 423 | 424 | if !has_internal_layout { 425 | // ```jsx 426 | // MDXLayout ? xxx : _createMdxContent(props) 427 | // ``` 428 | result = Expr::Cond(CondExpr { 429 | test: Box::new(create_ident_expression("MDXLayout")), 430 | cons: Box::new(result), 431 | alt: Box::new(create_call_expression( 432 | Callee::Expr(Box::new(create_ident_expression("_createMdxContent"))), 433 | vec![ExprOrSpread { 434 | spread: None, 435 | expr: Box::new(create_ident_expression("props")), 436 | }], 437 | )), 438 | span: swc_core::common::DUMMY_SP, 439 | }); 440 | } 441 | 442 | // ```jsx 443 | // function _createMdxContent(props) { 444 | // return xxx 445 | // } 446 | // ``` 447 | let create_mdx_content = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { 448 | ident: create_ident("_createMdxContent").into(), 449 | declare: false, 450 | function: Box::new(Function { 451 | params: vec![Param { 452 | pat: Pat::Ident(BindingIdent { 453 | id: create_ident("props").into(), 454 | type_ann: None, 455 | }), 456 | decorators: vec![], 457 | span: swc_core::common::DUMMY_SP, 458 | }], 459 | decorators: vec![], 460 | body: Some(BlockStmt { 461 | stmts: vec![Stmt::Return(ReturnStmt { 462 | arg: Some(Box::new(expr.unwrap_or_else(create_null_expression))), 463 | span: swc_core::common::DUMMY_SP, 464 | })], 465 | span: swc_core::common::DUMMY_SP, 466 | ctxt: SyntaxContext::empty(), 467 | }), 468 | is_generator: false, 469 | is_async: false, 470 | type_params: None, 471 | return_type: None, 472 | span: swc_core::common::DUMMY_SP, 473 | ctxt: SyntaxContext::empty(), 474 | }), 475 | }))); 476 | 477 | // ```jsx 478 | // function MDXContent(props = {}) { 479 | // return xxx 480 | // } 481 | // ``` 482 | let mdx_content = ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { 483 | ident: create_ident("MDXContent").into(), 484 | declare: false, 485 | function: Box::new(Function { 486 | params: vec![Param { 487 | pat: Pat::Assign(AssignPat { 488 | left: Box::new(Pat::Ident(BindingIdent { 489 | id: create_ident("props").into(), 490 | type_ann: None, 491 | })), 492 | right: Box::new(create_object_expression(vec![])), 493 | span: swc_core::common::DUMMY_SP, 494 | }), 495 | decorators: vec![], 496 | span: swc_core::common::DUMMY_SP, 497 | }], 498 | decorators: vec![], 499 | body: Some(BlockStmt { 500 | stmts: vec![Stmt::Return(ReturnStmt { 501 | arg: Some(Box::new(result)), 502 | span: swc_core::common::DUMMY_SP, 503 | })], 504 | span: swc_core::common::DUMMY_SP, 505 | ctxt: SyntaxContext::empty(), 506 | }), 507 | is_generator: false, 508 | is_async: false, 509 | type_params: None, 510 | return_type: None, 511 | span: swc_core::common::DUMMY_SP, 512 | ctxt: SyntaxContext::empty(), 513 | }), 514 | }))); 515 | 516 | vec![create_mdx_content, mdx_content] 517 | } 518 | 519 | /// Create a layout, inside the document. 520 | fn create_layout_decl(expr: Expr) -> ModuleItem { 521 | // ```jsx 522 | // const MDXLayout = xxx 523 | // ``` 524 | ModuleItem::Stmt(Stmt::Decl(Decl::Var(Box::new(VarDecl { 525 | kind: VarDeclKind::Const, 526 | declare: false, 527 | decls: vec![VarDeclarator { 528 | name: Pat::Ident(BindingIdent { 529 | id: create_ident("MDXLayout").into(), 530 | type_ann: None, 531 | }), 532 | init: Some(Box::new(expr)), 533 | span: swc_core::common::DUMMY_SP, 534 | definite: false, 535 | }], 536 | span: swc_core::common::DUMMY_SP, 537 | ctxt: SyntaxContext::empty(), 538 | })))) 539 | } 540 | 541 | /// Create an error about multiple layouts. 542 | fn err_for_double_layout( 543 | layout: bool, 544 | previous: Option<&Position>, 545 | at: Option<&Point>, 546 | ) -> Result<(), markdown::message::Message> { 547 | if layout { 548 | Err(markdown::message::Message { 549 | reason: format!( 550 | "Cannot specify multiple layouts (previous: {})", 551 | position_opt_to_string(previous) 552 | ), 553 | place: at.map(|p| Box::new(markdown::message::Place::Point(p.clone()))), 554 | source: Box::new("mdxjs-rs".into()), 555 | rule_id: Box::new("double-layout".into()), 556 | }) 557 | } else { 558 | Ok(()) 559 | } 560 | } 561 | 562 | #[cfg(test)] 563 | mod tests { 564 | use super::*; 565 | use crate::hast_util_to_swc::hast_util_to_swc; 566 | use crate::mdast_util_to_hast::mdast_util_to_hast; 567 | use crate::mdx_plugin_recma_document::{mdx_plugin_recma_document, Options as DocumentOptions}; 568 | use crate::swc::{parse_esm, parse_expression, serialize}; 569 | use crate::swc_utils::create_bool_expression; 570 | use markdown::{to_mdast, ParseOptions}; 571 | use pretty_assertions::assert_eq; 572 | use rustc_hash::FxHashSet; 573 | use swc_core::ecma::ast::{ 574 | EmptyStmt, ExportDefaultDecl, ExprStmt, JSXClosingFragment, JSXFragment, 575 | JSXOpeningFragment, JSXText, Module, TsInterfaceBody, TsInterfaceDecl, WhileStmt, 576 | }; 577 | 578 | fn compile(value: &str) -> Result { 579 | let location = Location::new(value.as_bytes()); 580 | let mdast = to_mdast( 581 | value, 582 | &ParseOptions { 583 | mdx_esm_parse: Some(Box::new(parse_esm)), 584 | mdx_expression_parse: Some(Box::new(parse_expression)), 585 | ..ParseOptions::mdx() 586 | }, 587 | )?; 588 | let hast = mdast_util_to_hast(&mdast); 589 | let mut program = 590 | hast_util_to_swc(&hast, None, Some(&location), &mut FxHashSet::default())?; 591 | mdx_plugin_recma_document(&mut program, &DocumentOptions::default(), Some(&location))?; 592 | Ok(serialize(&mut program.module, Some(&program.comments))) 593 | } 594 | 595 | #[test] 596 | fn small() -> Result<(), markdown::message::Message> { 597 | assert_eq!( 598 | compile("# hi\n\nAlpha *bravo* **charlie**.")?, 599 | "function _createMdxContent(props) { 600 | return <>

{\"hi\"}

{\"\\n\"}

{\"Alpha \"}{\"bravo\"}{\" \"}{\"charlie\"}{\".\"}

; 601 | } 602 | function MDXContent(props = {}) { 603 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 604 | } 605 | export default MDXContent; 606 | ", 607 | "should support a small program", 608 | ); 609 | 610 | Ok(()) 611 | } 612 | 613 | #[test] 614 | fn import() -> Result<(), markdown::message::Message> { 615 | assert_eq!( 616 | compile("import a from 'b'\n\n# {a}")?, 617 | "import a from 'b'; 618 | function _createMdxContent(props) { 619 | return

{a}

; 620 | } 621 | function MDXContent(props = {}) { 622 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 623 | } 624 | export default MDXContent; 625 | ", 626 | "should support an import", 627 | ); 628 | 629 | Ok(()) 630 | } 631 | 632 | #[test] 633 | fn export() -> Result<(), markdown::message::Message> { 634 | assert_eq!( 635 | compile("export * from 'a'\n\n# b")?, 636 | "export * from 'a'; 637 | function _createMdxContent(props) { 638 | return

{\"b\"}

; 639 | } 640 | function MDXContent(props = {}) { 641 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 642 | } 643 | export default MDXContent; 644 | ", 645 | "should support an export all", 646 | ); 647 | 648 | assert_eq!( 649 | compile("export function a() {}")?, 650 | "export function a() {} 651 | function _createMdxContent(props) { 652 | return <>; 653 | } 654 | function MDXContent(props = {}) { 655 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 656 | } 657 | export default MDXContent; 658 | ", 659 | "should support an export declaration", 660 | ); 661 | 662 | assert_eq!( 663 | compile("export class A {}")?, 664 | "export class A { 665 | } 666 | function _createMdxContent(props) { 667 | return <>; 668 | } 669 | function MDXContent(props = {}) { 670 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 671 | } 672 | export default MDXContent; 673 | ", 674 | "should support an export class", 675 | ); 676 | 677 | Ok(()) 678 | } 679 | 680 | #[test] 681 | fn export_default() -> Result<(), markdown::message::Message> { 682 | assert_eq!( 683 | compile("export default a")?, 684 | "const MDXLayout = a; 685 | function _createMdxContent(props) { 686 | return <>; 687 | } 688 | function MDXContent(props = {}) { 689 | return <_createMdxContent {...props}/>; 690 | } 691 | export default MDXContent; 692 | ", 693 | "should support an export default expression", 694 | ); 695 | 696 | assert_eq!( 697 | compile("export default function () {}")?, 698 | "const MDXLayout = function() {}; 699 | function _createMdxContent(props) { 700 | return <>; 701 | } 702 | function MDXContent(props = {}) { 703 | return <_createMdxContent {...props}/>; 704 | } 705 | export default MDXContent; 706 | ", 707 | "should support an export default declaration", 708 | ); 709 | 710 | assert_eq!( 711 | compile("export default class A {}")?, 712 | "const MDXLayout = class A { 713 | }; 714 | function _createMdxContent(props) { 715 | return <>; 716 | } 717 | function MDXContent(props = {}) { 718 | return <_createMdxContent {...props}/>; 719 | } 720 | export default MDXContent; 721 | ", 722 | "should support an export default class", 723 | ); 724 | 725 | Ok(()) 726 | } 727 | 728 | #[test] 729 | fn named_exports() -> Result<(), markdown::message::Message> { 730 | assert_eq!( 731 | compile("export {a, b as default}")?, 732 | "export { a }; 733 | const MDXLayout = b; 734 | function _createMdxContent(props) { 735 | return <>; 736 | } 737 | function MDXContent(props = {}) { 738 | return <_createMdxContent {...props}/>; 739 | } 740 | export default MDXContent; 741 | ", 742 | "should support a named export w/o source, w/ a default specifier", 743 | ); 744 | 745 | assert_eq!( 746 | compile("export {a}")?, 747 | "export { a }; 748 | function _createMdxContent(props) { 749 | return <>; 750 | } 751 | function MDXContent(props = {}) { 752 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 753 | } 754 | export default MDXContent; 755 | ", 756 | "should support a named export w/o source, w/o a default specifier", 757 | ); 758 | 759 | assert_eq!( 760 | compile("export {}")?, 761 | "export { }; 762 | function _createMdxContent(props) { 763 | return <>; 764 | } 765 | function MDXContent(props = {}) { 766 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 767 | } 768 | export default MDXContent; 769 | ", 770 | "should support a named export w/o source, w/o a specifiers", 771 | ); 772 | 773 | assert_eq!( 774 | compile("export {a, b as default} from 'c'")?, 775 | "export { a } from 'c'; 776 | import { b as MDXLayout } from 'c'; 777 | function _createMdxContent(props) { 778 | return <>; 779 | } 780 | function MDXContent(props = {}) { 781 | return <_createMdxContent {...props}/>; 782 | } 783 | export default MDXContent; 784 | ", 785 | "should support a named export w/ source, w/ a default specifier", 786 | ); 787 | 788 | assert_eq!( 789 | compile("export {a} from 'b'")?, 790 | "export { a } from 'b'; 791 | function _createMdxContent(props) { 792 | return <>; 793 | } 794 | function MDXContent(props = {}) { 795 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 796 | } 797 | export default MDXContent; 798 | ", 799 | "should support a named export w/ source, w/o a default specifier", 800 | ); 801 | 802 | assert_eq!( 803 | compile("export {} from 'a'")?, 804 | "export { } from 'a'; 805 | function _createMdxContent(props) { 806 | return <>; 807 | } 808 | function MDXContent(props = {}) { 809 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 810 | } 811 | export default MDXContent; 812 | ", 813 | "should support a named export w/ source, w/o a specifiers", 814 | ); 815 | 816 | Ok(()) 817 | } 818 | 819 | #[test] 820 | fn multiple_layouts() { 821 | assert_eq!( 822 | compile("export default a = 1\n\nexport default b = 2") 823 | .err() 824 | .unwrap() 825 | .to_string(), 826 | "3:1: Cannot specify multiple layouts (previous: 1:1-1:21) (mdxjs-rs:double-layout)", 827 | "should crash on multiple layouts" 828 | ); 829 | } 830 | 831 | #[test] 832 | fn ts_default_interface_declaration() { 833 | assert_eq!( 834 | mdx_plugin_recma_document( 835 | &mut Program { 836 | path: None, 837 | comments: vec![], 838 | module: Module { 839 | span: swc_core::common::DUMMY_SP, 840 | shebang: None, 841 | body: vec![ModuleItem::ModuleDecl( 842 | ModuleDecl::ExportDefaultDecl( 843 | ExportDefaultDecl { 844 | span: swc_core::common::DUMMY_SP, 845 | decl: DefaultDecl::TsInterfaceDecl(Box::new( 846 | TsInterfaceDecl { 847 | span: swc_core::common::DUMMY_SP, 848 | id: create_ident("a").into(), 849 | declare: true, 850 | type_params: None, 851 | extends: vec![], 852 | body: TsInterfaceBody { 853 | span: swc_core::common::DUMMY_SP, 854 | body: vec![] 855 | } 856 | } 857 | )) 858 | } 859 | ) 860 | )] 861 | } 862 | }, 863 | &Options::default(), 864 | None 865 | ) 866 | .err() 867 | .unwrap().to_string(), 868 | "Cannot use TypeScript interface declarations as default export in MDX files. The default export is reserved for a layout, which must be a component (mdxjs-rs:ts-interface)", 869 | "should crash on a TypeScript default interface declaration" 870 | ); 871 | } 872 | 873 | #[test] 874 | fn statement_pass_through() -> Result<(), markdown::message::Message> { 875 | let mut program = Program { 876 | path: None, 877 | comments: vec![], 878 | module: Module { 879 | span: swc_core::common::DUMMY_SP, 880 | shebang: None, 881 | body: vec![ModuleItem::Stmt(Stmt::While(WhileStmt { 882 | span: swc_core::common::DUMMY_SP, 883 | test: Box::new(create_bool_expression(true)), 884 | body: Box::new(Stmt::Empty(EmptyStmt { 885 | span: swc_core::common::DUMMY_SP, 886 | })), 887 | }))], 888 | }, 889 | }; 890 | 891 | mdx_plugin_recma_document(&mut program, &Options::default(), None)?; 892 | 893 | assert_eq!( 894 | serialize(&mut program.module, None), 895 | "while(true); 896 | function _createMdxContent(props) { 897 | return null; 898 | } 899 | function MDXContent(props = {}) { 900 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 901 | } 902 | export default MDXContent; 903 | ", 904 | "should pass statements through" 905 | ); 906 | 907 | Ok(()) 908 | } 909 | 910 | #[test] 911 | fn expression_pass_through() -> Result<(), markdown::message::Message> { 912 | let mut program = Program { 913 | path: None, 914 | comments: vec![], 915 | module: Module { 916 | span: swc_core::common::DUMMY_SP, 917 | shebang: None, 918 | body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt { 919 | span: swc_core::common::DUMMY_SP, 920 | expr: Box::new(create_bool_expression(true)), 921 | }))], 922 | }, 923 | }; 924 | 925 | mdx_plugin_recma_document(&mut program, &Options::default(), None)?; 926 | 927 | assert_eq!( 928 | serialize(&mut program.module, None), 929 | "true; 930 | function _createMdxContent(props) { 931 | return null; 932 | } 933 | function MDXContent(props = {}) { 934 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 935 | } 936 | export default MDXContent; 937 | ", 938 | "should pass expressions through" 939 | ); 940 | 941 | Ok(()) 942 | } 943 | 944 | #[test] 945 | fn fragment_non_element_single_child() -> Result<(), markdown::message::Message> { 946 | let mut program = Program { 947 | path: None, 948 | comments: vec![], 949 | module: Module { 950 | span: swc_core::common::DUMMY_SP, 951 | shebang: None, 952 | body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt { 953 | span: swc_core::common::DUMMY_SP, 954 | expr: Box::new(Expr::JSXFragment(JSXFragment { 955 | span: swc_core::common::DUMMY_SP, 956 | opening: JSXOpeningFragment { 957 | span: swc_core::common::DUMMY_SP, 958 | }, 959 | closing: JSXClosingFragment { 960 | span: swc_core::common::DUMMY_SP, 961 | }, 962 | children: vec![JSXElementChild::JSXText(JSXText { 963 | value: "a".into(), 964 | span: swc_core::common::DUMMY_SP, 965 | raw: "a".into(), 966 | })], 967 | })), 968 | }))], 969 | }, 970 | }; 971 | 972 | mdx_plugin_recma_document(&mut program, &Options::default(), None)?; 973 | 974 | assert_eq!( 975 | serialize(&mut program.module, None), 976 | "function _createMdxContent(props) { 977 | return <>a; 978 | } 979 | function MDXContent(props = {}) { 980 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 981 | } 982 | export default MDXContent; 983 | ", 984 | "should pass a fragment with a single child that isn’t an element through" 985 | ); 986 | 987 | Ok(()) 988 | } 989 | 990 | #[test] 991 | fn element() -> Result<(), markdown::message::Message> { 992 | let mut program = Program { 993 | path: None, 994 | comments: vec![], 995 | module: Module { 996 | span: swc_core::common::DUMMY_SP, 997 | shebang: None, 998 | body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt { 999 | span: swc_core::common::DUMMY_SP, 1000 | expr: Box::new(Expr::JSXElement(Box::new(JSXElement { 1001 | span: swc_core::common::DUMMY_SP, 1002 | opening: JSXOpeningElement { 1003 | name: JSXElementName::Ident(create_ident("a").into()), 1004 | attrs: vec![], 1005 | self_closing: false, 1006 | type_args: None, 1007 | span: swc_core::common::DUMMY_SP, 1008 | }, 1009 | closing: Some(JSXClosingElement { 1010 | name: JSXElementName::Ident(create_ident("a").into()), 1011 | span: swc_core::common::DUMMY_SP, 1012 | }), 1013 | children: vec![JSXElementChild::JSXText(JSXText { 1014 | value: "b".into(), 1015 | span: swc_core::common::DUMMY_SP, 1016 | raw: "b".into(), 1017 | })], 1018 | }))), 1019 | }))], 1020 | }, 1021 | }; 1022 | 1023 | mdx_plugin_recma_document(&mut program, &Options::default(), None)?; 1024 | 1025 | assert_eq!( 1026 | serialize(&mut program.module, None), 1027 | "function _createMdxContent(props) { 1028 | return
b; 1029 | } 1030 | function MDXContent(props = {}) { 1031 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 1032 | } 1033 | export default MDXContent; 1034 | ", 1035 | "should pass an element through" 1036 | ); 1037 | 1038 | Ok(()) 1039 | } 1040 | } 1041 | -------------------------------------------------------------------------------- /src/swc.rs: -------------------------------------------------------------------------------- 1 | //! Bridge between `markdown-rs` and SWC. 2 | 3 | extern crate markdown; 4 | 5 | use crate::swc_utils::{create_span, DropContext, RewritePrefixContext, RewriteStopsContext}; 6 | use markdown::{mdast::Stop, Location, MdxExpressionKind, MdxSignal}; 7 | use std::rc::Rc; 8 | use swc_core::common::{ 9 | comments::{Comment, Comments, SingleThreadedComments, SingleThreadedCommentsMap}, 10 | source_map::SmallPos, 11 | sync::Lrc, 12 | BytePos, FileName, FilePathMapping, SourceFile, SourceMap, Span, Spanned, 13 | }; 14 | use swc_core::ecma::ast::{EsVersion, Expr, Module, PropOrSpread}; 15 | use swc_core::ecma::codegen::{text_writer::JsWriter, Emitter}; 16 | use swc_core::ecma::parser::{ 17 | error::Error as SwcError, parse_file_as_expr, parse_file_as_module, EsSyntax, Syntax, 18 | }; 19 | use swc_core::ecma::visit::VisitMutWith; 20 | 21 | /// Lex ESM in MDX with SWC. 22 | pub fn parse_esm(value: &str) -> MdxSignal { 23 | let result = parse_esm_core(value); 24 | 25 | match result { 26 | Err((span, message)) => swc_error_to_signal(span, &message, value.len()), 27 | Ok(_) => MdxSignal::Ok, 28 | } 29 | } 30 | 31 | /// Parse ESM in MDX with SWC. 32 | pub fn parse_esm_to_tree( 33 | value: &str, 34 | stops: &[Stop], 35 | location: Option<&Location>, 36 | ) -> Result { 37 | let result = parse_esm_core(value); 38 | let mut rewrite_context = RewriteStopsContext { stops, location }; 39 | 40 | match result { 41 | Err((span, reason)) => Err(swc_error_to_error(span, &reason, &rewrite_context)), 42 | Ok(mut module) => { 43 | module.visit_mut_with(&mut rewrite_context); 44 | Ok(module) 45 | } 46 | } 47 | } 48 | 49 | /// Core to parse ESM. 50 | fn parse_esm_core(value: &str) -> Result { 51 | let (file, syntax, version) = create_config(value.into()); 52 | let mut errors = vec![]; 53 | let result = parse_file_as_module(&file, syntax, version, None, &mut errors); 54 | 55 | match result { 56 | Err(error) => Err(( 57 | fix_span(error.span(), 1), 58 | format!( 59 | "Could not parse esm with swc: {}", 60 | swc_error_to_string(&error) 61 | ), 62 | )), 63 | Ok(module) => { 64 | if errors.is_empty() { 65 | let mut index = 0; 66 | while index < module.body.len() { 67 | let node = &module.body[index]; 68 | 69 | if !node.is_module_decl() { 70 | return Err(( 71 | fix_span(node.span(), 1), 72 | "Unexpected statement in code: only import/exports are supported" 73 | .into(), 74 | )); 75 | } 76 | 77 | index += 1; 78 | } 79 | 80 | Ok(module) 81 | } else { 82 | Err(( 83 | fix_span(errors[0].span(), 1), 84 | format!( 85 | "Could not parse esm with swc: {}", 86 | swc_error_to_string(&errors[0]) 87 | ), 88 | )) 89 | } 90 | } 91 | } 92 | } 93 | 94 | fn parse_expression_core( 95 | value: &str, 96 | kind: &MdxExpressionKind, 97 | ) -> Result>, (Span, String)> { 98 | // Empty expressions are OK. 99 | if matches!(kind, MdxExpressionKind::Expression) && whitespace_and_comments(0, value).is_ok() { 100 | return Ok(None); 101 | } 102 | 103 | // For attribute expression, a spread is needed, for which we have to prefix 104 | // and suffix the input. 105 | // See `check_expression_ast` for how the AST is verified. 106 | let (prefix, suffix) = if matches!(kind, MdxExpressionKind::AttributeExpression) { 107 | ("({", "})") 108 | } else { 109 | ("", "") 110 | }; 111 | 112 | let (file, syntax, version) = create_config(format!("{}{}{}", prefix, value, suffix)); 113 | let mut errors = vec![]; 114 | let result = parse_file_as_expr(&file, syntax, version, None, &mut errors); 115 | 116 | match result { 117 | Err(error) => Err(( 118 | fix_span(error.span(), prefix.len() + 1), 119 | format!( 120 | "Could not parse expression with swc: {}", 121 | swc_error_to_string(&error) 122 | ), 123 | )), 124 | Ok(mut expr) => { 125 | if errors.is_empty() { 126 | let expression_end = expr.span().hi.to_usize() - 1; 127 | if let Err((span, reason)) = whitespace_and_comments(expression_end, value) { 128 | return Err((span, reason)); 129 | } 130 | 131 | expr.visit_mut_with(&mut RewritePrefixContext { 132 | prefix_len: prefix.len() as u32, 133 | }); 134 | 135 | if matches!(kind, MdxExpressionKind::AttributeExpression) { 136 | let expr_span = expr.span(); 137 | 138 | if let Expr::Paren(d) = *expr { 139 | if let Expr::Object(mut obj) = *d.expr { 140 | if obj.props.len() > 1 { 141 | return Err((obj.span, "Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`)".into())); 142 | } 143 | 144 | if let Some(PropOrSpread::Spread(d)) = obj.props.pop() { 145 | return Ok(Some(d.expr)); 146 | } 147 | } 148 | } 149 | 150 | return Err(( 151 | expr_span, 152 | "Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`)".into(), 153 | )); 154 | } 155 | 156 | Ok(Some(expr)) 157 | } else { 158 | Err(( 159 | fix_span(errors[0].span(), prefix.len() + 1), 160 | format!( 161 | "Could not parse expression with swc: {}", 162 | swc_error_to_string(&errors[0]) 163 | ), 164 | )) 165 | } 166 | } 167 | } 168 | } 169 | 170 | /// Lex expressions in MDX with SWC. 171 | pub fn parse_expression(value: &str, kind: &MdxExpressionKind) -> MdxSignal { 172 | let result = parse_expression_core(value, kind); 173 | 174 | match result { 175 | Err((span, message)) => swc_error_to_signal(span, &message, value.len()), 176 | Ok(_) => MdxSignal::Ok, 177 | } 178 | } 179 | 180 | /// Parse ESM in MDX with SWC. 181 | pub fn parse_expression_to_tree( 182 | value: &str, 183 | kind: &MdxExpressionKind, 184 | stops: &[Stop], 185 | location: Option<&Location>, 186 | ) -> Result>, markdown::message::Message> { 187 | let result = parse_expression_core(value, kind); 188 | let mut rewrite_context = RewriteStopsContext { stops, location }; 189 | 190 | match result { 191 | Err((span, reason)) => Err(swc_error_to_error(span, &reason, &rewrite_context)), 192 | Ok(expr_opt) => { 193 | if let Some(mut expr) = expr_opt { 194 | expr.visit_mut_with(&mut rewrite_context); 195 | Ok(Some(expr)) 196 | } else { 197 | Ok(None) 198 | } 199 | } 200 | } 201 | } 202 | 203 | /// Serialize an SWC module. 204 | pub fn serialize(module: &mut Module, comments: Option<&Vec>) -> String { 205 | let single_threaded_comments = SingleThreadedComments::default(); 206 | if let Some(comments) = comments { 207 | for c in comments { 208 | single_threaded_comments.add_leading(c.span.lo, c.clone()); 209 | } 210 | } 211 | module.visit_mut_with(&mut DropContext {}); 212 | let mut buf = vec![]; 213 | let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); 214 | { 215 | let mut emitter = Emitter { 216 | cfg: swc_core::ecma::codegen::Config::default(), 217 | cm: cm.clone(), 218 | comments: Some(&single_threaded_comments), 219 | wr: JsWriter::new(cm, "\n", &mut buf, None), 220 | }; 221 | 222 | emitter.emit_module(module).unwrap(); 223 | } 224 | 225 | String::from_utf8_lossy(&buf).into() 226 | } 227 | 228 | // To do: remove this attribute, use it somewhere. 229 | #[allow(dead_code)] 230 | /// Turn SWC comments into a flat vec. 231 | pub fn flat_comments(single_threaded_comments: SingleThreadedComments) -> Vec { 232 | let raw_comments = single_threaded_comments.take_all(); 233 | let take = |list: SingleThreadedCommentsMap| { 234 | Rc::try_unwrap(list) 235 | .unwrap() 236 | .into_inner() 237 | .into_values() 238 | .flatten() 239 | .collect::>() 240 | }; 241 | let mut list = take(raw_comments.0); 242 | list.append(&mut take(raw_comments.1)); 243 | list 244 | } 245 | 246 | /// Turn an SWC error into an `MdxSignal`. 247 | /// 248 | /// * If the error happens at `value_len`, yields `MdxSignal::Eof` 249 | /// * Else, yields `MdxSignal::Error`. 250 | fn swc_error_to_signal(span: Span, reason: &str, value_len: usize) -> MdxSignal { 251 | let error_end = span.hi.to_usize(); 252 | let source = Box::new("mdxjs-rs".into()); 253 | let rule_id = Box::new("swc".into()); 254 | 255 | if error_end >= value_len { 256 | MdxSignal::Eof(reason.into(), source, rule_id) 257 | } else { 258 | MdxSignal::Error(reason.into(), span.lo.to_usize(), source, rule_id) 259 | } 260 | } 261 | 262 | /// Turn an SWC error into a flat error. 263 | fn swc_error_to_error( 264 | span: Span, 265 | reason: &str, 266 | context: &RewriteStopsContext, 267 | ) -> markdown::message::Message { 268 | let point = context 269 | .location 270 | .and_then(|location| location.relative_to_point(context.stops, span.lo.to_usize())); 271 | 272 | markdown::message::Message { 273 | reason: reason.into(), 274 | place: point.map(|point| Box::new(markdown::message::Place::Point(point))), 275 | source: Box::new("mdxjs-rs".into()), 276 | rule_id: Box::new("swc".into()), 277 | } 278 | } 279 | 280 | /// Turn an SWC error into a string. 281 | fn swc_error_to_string(error: &SwcError) -> String { 282 | error.kind().msg().into() 283 | } 284 | 285 | /// Move past JavaScript whitespace (well, actually ASCII whitespace) and 286 | /// comments. 287 | /// 288 | /// This is needed because for expressions, we use an API that parses up to 289 | /// a valid expression, but there may be more expressions after it, which we 290 | /// don’t alow. 291 | fn whitespace_and_comments(mut index: usize, value: &str) -> Result<(), (Span, String)> { 292 | let bytes = value.as_bytes(); 293 | let len = bytes.len(); 294 | let mut in_multiline = false; 295 | let mut in_line = false; 296 | 297 | while index < len { 298 | // In a multiline comment: `/* a */`. 299 | if in_multiline { 300 | if index + 1 < len && bytes[index] == b'*' && bytes[index + 1] == b'/' { 301 | index += 1; 302 | in_multiline = false; 303 | } 304 | } 305 | // In a line comment: `// a`. 306 | else if in_line { 307 | if bytes[index] == b'\r' || bytes[index] == b'\n' { 308 | in_line = false; 309 | } 310 | } 311 | // Not in a comment, opening a multiline comment: `/* a */`. 312 | else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'*' { 313 | index += 1; 314 | in_multiline = true; 315 | } 316 | // Not in a comment, opening a line comment: `// a`. 317 | else if index + 1 < len && bytes[index] == b'/' && bytes[index + 1] == b'/' { 318 | index += 1; 319 | in_line = true; 320 | } 321 | // Outside comment, whitespace. 322 | else if bytes[index].is_ascii_whitespace() { 323 | // Fine! 324 | } 325 | // Outside comment, not whitespace. 326 | else { 327 | return Err(( 328 | create_span(index as u32, value.len() as u32), 329 | "Could not parse expression with swc: Unexpected content after expression".into(), 330 | )); 331 | } 332 | 333 | index += 1; 334 | } 335 | 336 | if in_multiline { 337 | return Err(( 338 | create_span(index as u32, value.len() as u32), "Could not parse expression with swc: Unexpected unclosed multiline comment, expected closing: `*/`".into())); 339 | } 340 | 341 | if in_line { 342 | // EOF instead of EOL is specifically not allowed, because that would 343 | // mean the closing brace is on the commented-out line 344 | 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())); 345 | } 346 | 347 | Ok(()) 348 | } 349 | 350 | /// Create configuration for SWC, shared between ESM and expressions. 351 | /// 352 | /// This enables modern JavaScript (ES2022) + JSX. 353 | fn create_config(source: String) -> (SourceFile, Syntax, EsVersion) { 354 | ( 355 | // File. 356 | SourceFile::new( 357 | FileName::Anon.into(), 358 | false, 359 | FileName::Anon.into(), 360 | source, 361 | BytePos::from_usize(1), 362 | ), 363 | // Syntax. 364 | Syntax::Es(EsSyntax { 365 | jsx: true, 366 | ..EsSyntax::default() 367 | }), 368 | // Version. 369 | EsVersion::Es2022, 370 | ) 371 | } 372 | 373 | fn fix_span(mut span: Span, offset: usize) -> Span { 374 | span.lo = BytePos::from_usize(span.lo.to_usize() - offset); 375 | span.hi = BytePos::from_usize(span.hi.to_usize() - offset); 376 | span 377 | } 378 | -------------------------------------------------------------------------------- /src/swc_util_build_jsx.rs: -------------------------------------------------------------------------------- 1 | //! Turn JSX into function calls. 2 | 3 | use crate::hast_util_to_swc::Program; 4 | use crate::mdx_plugin_recma_document::JsxRuntime; 5 | use crate::swc_utils::{ 6 | bytepos_to_point, create_bool_expression, create_call_expression, create_ident, 7 | create_ident_expression, create_member_expression_from_str, create_null_expression, 8 | create_num_expression, create_object_expression, create_prop_name, create_str, 9 | create_str_expression, jsx_attribute_name_to_prop_name, jsx_element_name_to_expression, 10 | span_to_position, 11 | }; 12 | use core::str; 13 | use markdown::{message::Message, Location}; 14 | use swc_core::common::SyntaxContext; 15 | use swc_core::common::{ 16 | comments::{Comment, CommentKind}, 17 | util::take::Take, 18 | }; 19 | use swc_core::ecma::ast::{ 20 | ArrayLit, CallExpr, Callee, Expr, ExprOrSpread, ImportDecl, ImportNamedSpecifier, ImportPhase, 21 | ImportSpecifier, JSXAttrName, JSXAttrOrSpread, JSXAttrValue, JSXElement, JSXElementChild, 22 | JSXExpr, JSXFragment, KeyValueProp, Lit, ModuleDecl, ModuleExportName, ModuleItem, Prop, 23 | PropName, PropOrSpread, ThisExpr, 24 | }; 25 | use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut, VisitMutWith}; 26 | 27 | /// Configuration. 28 | #[derive(Debug, Default, Clone)] 29 | pub struct Options { 30 | /// Whether to add extra information to error messages in generated code. 31 | pub development: bool, 32 | } 33 | 34 | /// Compile JSX away to function calls. 35 | pub fn swc_util_build_jsx( 36 | program: &mut Program, 37 | options: &Options, 38 | location: Option<&Location>, 39 | ) -> Result<(), markdown::message::Message> { 40 | let directives = find_directives(&program.comments, location)?; 41 | 42 | let mut state = State { 43 | development: options.development, 44 | filepath: program.path.clone(), 45 | location, 46 | automatic: !matches!(directives.runtime, Some(JsxRuntime::Classic)), 47 | import_fragment: false, 48 | import_jsx: false, 49 | import_jsxs: false, 50 | import_jsx_dev: false, 51 | create_element_expression: create_member_expression_from_str( 52 | &directives 53 | .pragma 54 | .unwrap_or_else(|| "React.createElement".into()), 55 | ), 56 | fragment_expression: create_member_expression_from_str( 57 | &directives 58 | .pragma_frag 59 | .unwrap_or_else(|| "React.Fragment".into()), 60 | ), 61 | error: None, 62 | }; 63 | 64 | // Rewrite JSX and gather specifiers to import. 65 | program.module.visit_mut_with(&mut state); 66 | 67 | if let Some(err) = state.error.take() { 68 | return Err(err); 69 | } 70 | 71 | let mut specifiers = vec![]; 72 | 73 | if state.import_fragment { 74 | specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { 75 | local: create_ident("_Fragment").into(), 76 | imported: Some(ModuleExportName::Ident(create_ident("Fragment").into())), 77 | span: swc_core::common::DUMMY_SP, 78 | is_type_only: false, 79 | })); 80 | } 81 | 82 | if state.import_jsx { 83 | specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { 84 | local: create_ident("_jsx").into(), 85 | imported: Some(ModuleExportName::Ident(create_ident("jsx").into())), 86 | span: swc_core::common::DUMMY_SP, 87 | is_type_only: false, 88 | })); 89 | } 90 | 91 | if state.import_jsxs { 92 | specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { 93 | local: create_ident("_jsxs").into(), 94 | imported: Some(ModuleExportName::Ident(create_ident("jsxs").into())), 95 | span: swc_core::common::DUMMY_SP, 96 | is_type_only: false, 97 | })); 98 | } 99 | 100 | if state.import_jsx_dev { 101 | specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier { 102 | local: create_ident("_jsxDEV").into(), 103 | imported: Some(ModuleExportName::Ident(create_ident("jsxDEV").into())), 104 | span: swc_core::common::DUMMY_SP, 105 | is_type_only: false, 106 | })); 107 | } 108 | 109 | if !specifiers.is_empty() { 110 | program.module.body.insert( 111 | 0, 112 | ModuleItem::ModuleDecl(ModuleDecl::Import(ImportDecl { 113 | specifiers, 114 | src: Box::new(create_str(&format!( 115 | "{}{}", 116 | directives.import_source.unwrap_or_else(|| "react".into()), 117 | if options.development { 118 | "/jsx-dev-runtime" 119 | } else { 120 | "/jsx-runtime" 121 | } 122 | ))), 123 | type_only: false, 124 | with: None, 125 | phase: ImportPhase::default(), 126 | span: swc_core::common::DUMMY_SP, 127 | })), 128 | ); 129 | } 130 | 131 | Ok(()) 132 | } 133 | 134 | /// Info gathered from comments. 135 | #[derive(Debug, Default, Clone)] 136 | struct Directives { 137 | /// Inferred JSX runtime. 138 | runtime: Option, 139 | /// Inferred automatic JSX import source. 140 | import_source: Option, 141 | /// Inferred classic JSX pragma. 142 | pragma: Option, 143 | /// Inferred classic JSX pragma fragment. 144 | pragma_frag: Option, 145 | } 146 | 147 | /// Context. 148 | #[derive(Debug, Clone)] 149 | struct State<'a> { 150 | /// Location info. 151 | location: Option<&'a Location>, 152 | /// Whether walking the tree produced an error. 153 | error: Option, 154 | /// Path to file. 155 | filepath: Option, 156 | /// Whether the user is in development mode. 157 | development: bool, 158 | /// Whether to import `Fragment`. 159 | import_fragment: bool, 160 | /// Whether to import `jsx`. 161 | import_jsx: bool, 162 | /// Whether to import `jsxs`. 163 | import_jsxs: bool, 164 | /// Whether to import `jsxDEV`. 165 | import_jsx_dev: bool, 166 | /// Whether we’re building in the automatic or classic runtime. 167 | automatic: bool, 168 | /// Expression (ident or member) to use for `createElement` calls in 169 | /// the classic runtime. 170 | create_element_expression: Expr, 171 | /// Expression (ident or member) to use as fragment symbol in the classic 172 | /// runtime. 173 | fragment_expression: Expr, 174 | } 175 | 176 | impl State<'_> { 177 | /// Turn an attribute value into an expression. 178 | fn jsx_attribute_value_to_expression( 179 | &mut self, 180 | value: Option, 181 | ) -> Result { 182 | match value { 183 | // Boolean prop. 184 | None => Ok(create_bool_expression(true)), 185 | Some(JSXAttrValue::JSXExprContainer(expression_container)) => { 186 | match expression_container.expr { 187 | JSXExpr::JSXEmptyExpr(_) => { 188 | unreachable!("Cannot use empty JSX expressions in attribute values"); 189 | } 190 | JSXExpr::Expr(expression) => Ok(*expression), 191 | } 192 | } 193 | Some(JSXAttrValue::Lit(mut literal)) => { 194 | // Remove `raw` so we don’t get character references in strings. 195 | if let Lit::Str(string_literal) = &mut literal { 196 | string_literal.raw = None; 197 | } 198 | 199 | Ok(Expr::Lit(literal)) 200 | } 201 | Some(JSXAttrValue::JSXFragment(fragment)) => self.jsx_fragment_to_expression(fragment), 202 | Some(JSXAttrValue::JSXElement(element)) => self.jsx_element_to_expression(*element), 203 | } 204 | } 205 | 206 | /// Turn children of elements or fragments into expressions. 207 | fn jsx_children_to_expressions( 208 | &mut self, 209 | mut children: Vec, 210 | ) -> Result, markdown::message::Message> { 211 | let mut result = vec![]; 212 | children.reverse(); 213 | while let Some(child) = children.pop() { 214 | match child { 215 | JSXElementChild::JSXSpreadChild(child) => { 216 | let lo = child.span.lo; 217 | return Err( 218 | markdown::message::Message { 219 | reason: "Unexpected spread child, which is not supported in Babel, SWC, or React".into(), 220 | place: bytepos_to_point(lo, self.location).map(|p| Box::new(markdown::message::Place::Point(p))), 221 | source: Box::new("mdxjs-rs".into()), 222 | rule_id: Box::new("spread".into()), 223 | } 224 | ); 225 | } 226 | JSXElementChild::JSXExprContainer(container) => { 227 | if let JSXExpr::Expr(expression) = container.expr { 228 | result.push(*expression); 229 | } 230 | } 231 | JSXElementChild::JSXText(text) => { 232 | let value = jsx_text_to_value(text.value.as_ref()); 233 | if !value.is_empty() { 234 | result.push(create_str_expression(&value)); 235 | } 236 | } 237 | JSXElementChild::JSXElement(element) => { 238 | result.push(self.jsx_element_to_expression(*element)?); 239 | } 240 | JSXElementChild::JSXFragment(fragment) => { 241 | result.push(self.jsx_fragment_to_expression(fragment)?); 242 | } 243 | } 244 | } 245 | 246 | Ok(result) 247 | } 248 | 249 | /// Turn optional attributes, and perhaps children (when automatic), into props. 250 | fn jsx_attributes_to_expressions( 251 | &mut self, 252 | attributes: Option>, 253 | children: Option>, 254 | ) -> Result<(Option, Option), markdown::message::Message> { 255 | let mut objects = vec![]; 256 | let mut fields = vec![]; 257 | let mut spread = false; 258 | let mut key = None; 259 | 260 | if let Some(mut attributes) = attributes { 261 | attributes.reverse(); 262 | 263 | // Place props in the right order, because we might have duplicates 264 | // in them and what’s spread in. 265 | while let Some(attribute) = attributes.pop() { 266 | match attribute { 267 | JSXAttrOrSpread::SpreadElement(spread_element) => { 268 | if !fields.is_empty() { 269 | objects.push(create_object_expression(fields)); 270 | fields = vec![]; 271 | } 272 | 273 | objects.push(*spread_element.expr); 274 | spread = true; 275 | } 276 | JSXAttrOrSpread::JSXAttr(jsx_attribute) => { 277 | let value = self.jsx_attribute_value_to_expression(jsx_attribute.value)?; 278 | let mut value = Some(value); 279 | 280 | if let JSXAttrName::Ident(ident) = &jsx_attribute.name { 281 | if self.automatic && &ident.sym == "key" { 282 | if spread { 283 | let lo = jsx_attribute.span.lo; 284 | return Err(markdown::message::Message { 285 | reason: 286 | "Expected `key` to come before any spread expressions" 287 | .into(), 288 | place: bytepos_to_point(lo, self.location) 289 | .map(|p| Box::new(markdown::message::Place::Point(p))), 290 | source: Box::new("mdxjs-rs".into()), 291 | rule_id: Box::new("key".into()), 292 | }); 293 | } 294 | 295 | // Take the value out, so we don’t add it as a prop. 296 | key = value.take(); 297 | } 298 | } 299 | 300 | if let Some(value) = value { 301 | fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue( 302 | KeyValueProp { 303 | key: jsx_attribute_name_to_prop_name(jsx_attribute.name), 304 | value: Box::new(value), 305 | }, 306 | )))); 307 | } 308 | } 309 | } 310 | } 311 | } 312 | 313 | // In the automatic runtime, add children as a prop. 314 | if let Some(mut children) = children { 315 | let value = if children.is_empty() { 316 | None 317 | } else if children.len() == 1 { 318 | Some(children.pop().unwrap()) 319 | } else { 320 | let mut elements = vec![]; 321 | children.reverse(); 322 | while let Some(child) = children.pop() { 323 | elements.push(Some(ExprOrSpread { 324 | spread: None, 325 | expr: Box::new(child), 326 | })); 327 | } 328 | let lit = ArrayLit { 329 | elems: elements, 330 | span: swc_core::common::DUMMY_SP, 331 | }; 332 | Some(Expr::Array(lit)) 333 | }; 334 | 335 | if let Some(value) = value { 336 | fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 337 | key: create_prop_name("children"), 338 | value: Box::new(value), 339 | })))); 340 | } 341 | } 342 | 343 | // Add remaining fields. 344 | if !fields.is_empty() { 345 | objects.push(create_object_expression(fields)); 346 | } 347 | 348 | let props = if objects.is_empty() { 349 | None 350 | } else if objects.len() == 1 { 351 | Some(objects.pop().unwrap()) 352 | } else { 353 | let mut args = vec![]; 354 | objects.reverse(); 355 | 356 | // Don’t mutate the first object, shallow clone into a new 357 | // object instead. 358 | if !matches!(objects.last(), Some(Expr::Object(_))) { 359 | objects.push(create_object_expression(vec![])); 360 | } 361 | 362 | while let Some(object) = objects.pop() { 363 | args.push(ExprOrSpread { 364 | spread: None, 365 | expr: Box::new(object), 366 | }); 367 | } 368 | 369 | let callee = Callee::Expr(Box::new(create_member_expression_from_str("Object.assign"))); 370 | Some(create_call_expression(callee, args)) 371 | }; 372 | 373 | Ok((props, key)) 374 | } 375 | 376 | /// Turn the parsed parts from fragments or elements into a call. 377 | fn jsx_expressions_to_call( 378 | &mut self, 379 | span: swc_core::common::Span, 380 | name: Expr, 381 | attributes: Option>, 382 | mut children: Vec, 383 | ) -> Result { 384 | let (callee, parameters) = if self.automatic { 385 | let is_static_children = children.len() > 1; 386 | let (props, key) = self.jsx_attributes_to_expressions(attributes, Some(children))?; 387 | let mut parameters = vec![ 388 | // Component name. 389 | // 390 | // ```javascript 391 | // Component 392 | // ``` 393 | ExprOrSpread { 394 | spread: None, 395 | expr: Box::new(name), 396 | }, 397 | // Props (including children) or empty object. 398 | // 399 | // ```javascript 400 | // Object.assign({x: true, y: 'z'}, {children: […]}) 401 | // {x: true, y: 'z'} 402 | // {} 403 | // ``` 404 | ExprOrSpread { 405 | spread: None, 406 | expr: Box::new(props.unwrap_or_else(|| create_object_expression(vec![]))), 407 | }, 408 | ]; 409 | 410 | // Key or, in development, undefined. 411 | // 412 | // ```javascript 413 | // "xyz" 414 | // ``` 415 | if let Some(key) = key { 416 | parameters.push(ExprOrSpread { 417 | spread: None, 418 | expr: Box::new(key), 419 | }); 420 | } else if self.development { 421 | parameters.push(ExprOrSpread { 422 | spread: None, 423 | expr: Box::new(create_ident_expression("undefined")), 424 | }); 425 | } 426 | 427 | if self.development { 428 | // Static children (or not). 429 | // 430 | // ```javascript 431 | // true 432 | // ``` 433 | parameters.push(ExprOrSpread { 434 | spread: None, 435 | expr: Box::new(create_bool_expression(is_static_children)), 436 | }); 437 | 438 | let filename = if let Some(value) = &self.filepath { 439 | create_str_expression(value) 440 | } else { 441 | create_str_expression("") 442 | }; 443 | let prop = PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 444 | key: PropName::Ident(create_ident("fileName")), 445 | value: Box::new(filename), 446 | }))); 447 | let mut meta_fields = vec![prop]; 448 | 449 | if let Some(position) = span_to_position(span, self.location) { 450 | meta_fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 451 | key: create_prop_name("lineNumber"), 452 | value: Box::new(create_num_expression(position.start.line as f64)), 453 | })))); 454 | 455 | meta_fields.push(PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp { 456 | key: create_prop_name("columnNumber"), 457 | value: Box::new(create_num_expression(position.start.column as f64)), 458 | })))); 459 | } 460 | 461 | // File name and positional info. 462 | // 463 | // ```javascript 464 | // { 465 | // fileName: "example.jsx", 466 | // lineNumber: 1, 467 | // columnNumber: 3 468 | // } 469 | // ``` 470 | parameters.push(ExprOrSpread { 471 | spread: None, 472 | expr: Box::new(create_object_expression(meta_fields)), 473 | }); 474 | 475 | // Context object. 476 | // 477 | // ```javascript 478 | // this 479 | // ``` 480 | let this_expression = ThisExpr { 481 | span: swc_core::common::DUMMY_SP, 482 | }; 483 | parameters.push(ExprOrSpread { 484 | spread: None, 485 | expr: Box::new(Expr::This(this_expression)), 486 | }); 487 | } 488 | 489 | let callee = if self.development { 490 | self.import_jsx_dev = true; 491 | "_jsxDEV" 492 | } else if is_static_children { 493 | self.import_jsxs = true; 494 | "_jsxs" 495 | } else { 496 | self.import_jsx = true; 497 | "_jsx" 498 | }; 499 | 500 | (create_ident_expression(callee), parameters) 501 | } else { 502 | // Classic runtime. 503 | let (props, key) = self.jsx_attributes_to_expressions(attributes, None)?; 504 | debug_assert!(key.is_none(), "key should not be extracted"); 505 | let mut parameters = vec![ 506 | // Component name. 507 | // 508 | // ```javascript 509 | // Component 510 | // ``` 511 | ExprOrSpread { 512 | spread: None, 513 | expr: Box::new(name), 514 | }, 515 | ]; 516 | 517 | // Props or, if with children, null. 518 | // 519 | // ```javascript 520 | // {x: true, y: 'z'} 521 | // ``` 522 | if let Some(props) = props { 523 | parameters.push(ExprOrSpread { 524 | spread: None, 525 | expr: Box::new(props), 526 | }); 527 | } else if !children.is_empty() { 528 | parameters.push(ExprOrSpread { 529 | spread: None, 530 | expr: Box::new(create_null_expression()), 531 | }); 532 | } 533 | 534 | // Each child as a parameter. 535 | children.reverse(); 536 | while let Some(child) = children.pop() { 537 | parameters.push(ExprOrSpread { 538 | spread: None, 539 | expr: Box::new(child), 540 | }); 541 | } 542 | 543 | (self.create_element_expression.clone(), parameters) 544 | }; 545 | 546 | let call_expression = CallExpr { 547 | callee: Callee::Expr(Box::new(callee)), 548 | args: parameters, 549 | type_args: None, 550 | span, 551 | ctxt: SyntaxContext::empty(), 552 | }; 553 | 554 | Ok(Expr::Call(call_expression)) 555 | } 556 | 557 | /// Turn a JSX element into an expression. 558 | fn jsx_element_to_expression( 559 | &mut self, 560 | element: JSXElement, 561 | ) -> Result { 562 | let children = self.jsx_children_to_expressions(element.children)?; 563 | let mut name = jsx_element_name_to_expression(element.opening.name); 564 | 565 | // If the name could be an identifier, but start with a lowercase letter, 566 | // it’s not a component. 567 | if let Expr::Ident(ident) = &name { 568 | let head = ident.as_ref().as_bytes(); 569 | if matches!(head.first(), Some(b'a'..=b'z')) { 570 | name = create_str_expression(&ident.sym); 571 | } 572 | } 573 | 574 | self.jsx_expressions_to_call(element.span, name, Some(element.opening.attrs), children) 575 | } 576 | 577 | /// Turn a JSX fragment into an expression. 578 | fn jsx_fragment_to_expression( 579 | &mut self, 580 | fragment: JSXFragment, 581 | ) -> Result { 582 | let name = if self.automatic { 583 | self.import_fragment = true; 584 | create_ident_expression("_Fragment") 585 | } else { 586 | self.fragment_expression.clone() 587 | }; 588 | let children = self.jsx_children_to_expressions(fragment.children)?; 589 | self.jsx_expressions_to_call(fragment.span, name, None, children) 590 | } 591 | } 592 | 593 | impl VisitMut for State<'_> { 594 | noop_visit_mut_type!(); 595 | 596 | /// Visit expressions, rewriting JSX, and walking deeper. 597 | fn visit_mut_expr(&mut self, expr: &mut Expr) { 598 | let result = match expr { 599 | Expr::JSXElement(element) => Some(self.jsx_element_to_expression(*element.take())), 600 | Expr::JSXFragment(fragment) => Some(self.jsx_fragment_to_expression(fragment.take())), 601 | _ => None, 602 | }; 603 | 604 | if let Some(result) = result { 605 | match result { 606 | Ok(expression) => { 607 | *expr = expression; 608 | expr.visit_mut_children_with(self); 609 | } 610 | Err(err) => { 611 | self.error = Some(err); 612 | } 613 | } 614 | } else { 615 | expr.visit_mut_children_with(self); 616 | } 617 | } 618 | } 619 | 620 | /// Find directives in comments. 621 | /// 622 | /// This looks for block comments (`/* */`) and checks each line that starts 623 | /// with `@jsx`. 624 | /// Then it looks for key/value pairs (each words split by whitespace). 625 | /// Known keys are used for directives. 626 | fn find_directives( 627 | comments: &Vec, 628 | location: Option<&Location>, 629 | ) -> Result { 630 | let mut directives = Directives::default(); 631 | 632 | for comment in comments { 633 | if comment.kind != CommentKind::Block { 634 | continue; 635 | } 636 | 637 | let lines = comment.text.lines(); 638 | 639 | for line in lines { 640 | let bytes = line.as_bytes(); 641 | let mut index = 0; 642 | // Skip initial whitespace. 643 | while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') { 644 | index += 1; 645 | } 646 | // Skip star. 647 | if index < bytes.len() && bytes[index] == b'*' { 648 | index += 1; 649 | // Skip more whitespace. 650 | while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') { 651 | index += 1; 652 | } 653 | } 654 | // Peek if this looks like a JSX directive. 655 | if !(index + 4 < bytes.len() 656 | && bytes[index] == b'@' 657 | && bytes[index + 1] == b'j' 658 | && bytes[index + 2] == b's' 659 | && bytes[index + 3] == b'x') 660 | { 661 | // Exit if not. 662 | continue; 663 | } 664 | 665 | loop { 666 | let mut key_range = (index, index); 667 | while index < bytes.len() && !matches!(bytes[index], b' ' | b'\t') { 668 | index += 1; 669 | } 670 | key_range.1 = index; 671 | // Skip whitespace. 672 | while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') { 673 | index += 1; 674 | } 675 | let mut value_range = (index, index); 676 | while index < bytes.len() && !matches!(bytes[index], b' ' | b'\t') { 677 | index += 1; 678 | } 679 | value_range.1 = index; 680 | 681 | let key = String::from_utf8_lossy(&bytes[key_range.0..key_range.1]); 682 | let value = String::from_utf8_lossy(&bytes[value_range.0..value_range.1]); 683 | 684 | // Handle the key/value. 685 | match key.as_ref() { 686 | "@jsxRuntime" => match value.as_ref() { 687 | "automatic" => directives.runtime = Some(JsxRuntime::Automatic), 688 | "classic" => directives.runtime = Some(JsxRuntime::Classic), 689 | "" => {} 690 | value => { 691 | return Err(markdown::message::Message { 692 | reason: format!( 693 | "Runtime must be either `automatic` or `classic`, not {}", 694 | value 695 | ), 696 | place: bytepos_to_point(comment.span.lo, location) 697 | .map(|p| Box::new(markdown::message::Place::Point(p))), 698 | source: Box::new("mdxjs-rs".into()), 699 | rule_id: Box::new("runtime".into()), 700 | }); 701 | } 702 | }, 703 | "@jsxImportSource" => { 704 | match value.as_ref() { 705 | "" => {} 706 | value => { 707 | // SWC sets runtime too, not sure if that’s great. 708 | directives.runtime = Some(JsxRuntime::Automatic); 709 | directives.import_source = Some(value.into()); 710 | } 711 | } 712 | } 713 | "@jsxFrag" => match value.as_ref() { 714 | "" => {} 715 | value => directives.pragma_frag = Some(value.into()), 716 | }, 717 | "@jsx" => match value.as_ref() { 718 | "" => {} 719 | value => directives.pragma = Some(value.into()), 720 | }, 721 | "" => { 722 | // No directive, stop looking for key/value pairs 723 | // on this line. 724 | break; 725 | } 726 | _ => {} 727 | } 728 | 729 | // Skip more whitespace. 730 | while index < bytes.len() && matches!(bytes[index], b' ' | b'\t') { 731 | index += 1; 732 | } 733 | } 734 | } 735 | } 736 | 737 | Ok(directives) 738 | } 739 | 740 | /// Turn JSX text into a string. 741 | fn jsx_text_to_value(value: &str) -> String { 742 | let mut result = String::with_capacity(value.len()); 743 | // Replace tabs w/ spaces. 744 | let value = value.replace('\t', " "); 745 | let bytes = value.as_bytes(); 746 | let mut index = 0; 747 | let mut start = 0; 748 | 749 | while index < bytes.len() { 750 | if !matches!(bytes[index], b'\r' | b'\n') { 751 | index += 1; 752 | continue; 753 | } 754 | 755 | // We have an eol, move back past whitespace. 756 | let mut before = index; 757 | while before > start && bytes[before - 1] == b' ' { 758 | before -= 1; 759 | } 760 | 761 | if start != before { 762 | if !result.is_empty() { 763 | result.push(' '); 764 | } 765 | result.push_str(str::from_utf8(&bytes[start..before]).unwrap()); 766 | } 767 | 768 | // Move past whitespace. 769 | index += 1; 770 | while index < bytes.len() && bytes[index] == b' ' { 771 | index += 1; 772 | } 773 | start = index; 774 | } 775 | 776 | if start != bytes.len() { 777 | // Without line endings, if it’s just whitespace, ignore it. 778 | if result.is_empty() { 779 | index = 0; 780 | 781 | while index < bytes.len() && bytes[index] == b' ' { 782 | index += 1; 783 | } 784 | 785 | if index == bytes.len() { 786 | return result; 787 | } 788 | } else { 789 | result.push(' '); 790 | } 791 | 792 | result.push_str(str::from_utf8(&bytes[start..]).unwrap()); 793 | } 794 | 795 | result 796 | } 797 | 798 | #[cfg(test)] 799 | mod tests { 800 | use super::*; 801 | use crate::hast_util_to_swc::Program; 802 | use crate::swc::{flat_comments, serialize}; 803 | use pretty_assertions::assert_eq; 804 | use swc_core::common::Spanned; 805 | use swc_core::common::{ 806 | comments::SingleThreadedComments, source_map::SmallPos, BytePos, FileName, SourceFile, 807 | }; 808 | use swc_core::ecma::ast::{ 809 | EsVersion, ExprStmt, JSXClosingElement, JSXElementName, JSXOpeningElement, JSXSpreadChild, 810 | Module, Stmt, 811 | }; 812 | use swc_core::ecma::parser::{parse_file_as_module, EsSyntax, Syntax}; 813 | 814 | fn compile(value: &str, options: &Options) -> Result { 815 | let location = Location::new(value.as_bytes()); 816 | let mut errors = vec![]; 817 | let comments = SingleThreadedComments::default(); 818 | let result = parse_file_as_module( 819 | &SourceFile::new( 820 | FileName::Anon.into(), 821 | false, 822 | FileName::Anon.into(), 823 | value.into(), 824 | BytePos::from_usize(1), 825 | ), 826 | Syntax::Es(EsSyntax { 827 | jsx: true, 828 | ..EsSyntax::default() 829 | }), 830 | EsVersion::Es2022, 831 | Some(&comments), 832 | &mut errors, 833 | ); 834 | 835 | match result { 836 | Err(error) => Err(markdown::message::Message { 837 | reason: error.kind().msg().into(), 838 | place: bytepos_to_point(error.span().lo, Some(&location)) 839 | .map(|p| Box::new(markdown::message::Place::Point(p))), 840 | source: Box::new("mdxjs-rs".into()), 841 | rule_id: Box::new("swc".into()), 842 | }), 843 | Ok(module) => { 844 | let mut program = Program { 845 | path: Some("example.jsx".into()), 846 | module, 847 | comments: flat_comments(comments), 848 | }; 849 | swc_util_build_jsx(&mut program, options, Some(&location))?; 850 | Ok(serialize(&mut program.module, Some(&program.comments))) 851 | } 852 | } 853 | } 854 | 855 | #[test] 856 | fn small_default() -> Result<(), markdown::message::Message> { 857 | assert_eq!( 858 | compile("let a = ", &Options::default())?, 859 | "import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n", 860 | "should compile JSX away" 861 | ); 862 | 863 | Ok(()) 864 | } 865 | 866 | #[test] 867 | fn directive_runtime_automatic() -> Result<(), markdown::message::Message> { 868 | assert_eq!( 869 | compile( 870 | "/* @jsxRuntime automatic */\nlet a = ", 871 | &Options::default() 872 | )?, 873 | "import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n", 874 | "should support a `@jsxRuntime automatic` directive" 875 | ); 876 | 877 | Ok(()) 878 | } 879 | 880 | #[test] 881 | fn directive_runtime_classic() -> Result<(), markdown::message::Message> { 882 | assert_eq!( 883 | compile( 884 | "/* @jsxRuntime classic */\nlet a = ", 885 | &Options::default() 886 | )?, 887 | "let a = React.createElement(\"b\");\n", 888 | "should support a `@jsxRuntime classic` directive" 889 | ); 890 | 891 | Ok(()) 892 | } 893 | 894 | #[test] 895 | fn directive_runtime_empty() -> Result<(), markdown::message::Message> { 896 | assert_eq!( 897 | compile("/* @jsxRuntime */\nlet a = ", &Options::default())?, 898 | "import { jsx as _jsx } from \"react/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n", 899 | "should support an empty `@jsxRuntime` directive" 900 | ); 901 | 902 | Ok(()) 903 | } 904 | 905 | #[test] 906 | fn directive_runtime_invalid() { 907 | assert_eq!( 908 | compile( 909 | "/* @jsxRuntime unknown */\nlet a = ", 910 | &Options::default() 911 | ) 912 | .err() 913 | .unwrap() 914 | .to_string(), 915 | "1:1: Runtime must be either `automatic` or `classic`, not unknown (mdxjs-rs:runtime)", 916 | "should crash on a non-automatic, non-classic `@jsxRuntime` directive" 917 | ); 918 | } 919 | 920 | #[test] 921 | fn directive_import_source() -> Result<(), markdown::message::Message> { 922 | assert_eq!( 923 | compile( 924 | "/* @jsxImportSource aaa */\nlet a = ", 925 | &Options::default() 926 | )?, 927 | "import { jsx as _jsx } from \"aaa/jsx-runtime\";\nlet a = _jsx(\"b\", {});\n", 928 | "should support a `@jsxImportSource` directive" 929 | ); 930 | 931 | Ok(()) 932 | } 933 | 934 | #[test] 935 | fn directive_jsx() -> Result<(), markdown::message::Message> { 936 | assert_eq!( 937 | compile( 938 | "/* @jsxRuntime classic @jsx a */\nlet b = ", 939 | &Options::default() 940 | )?, 941 | "let b = a(\"c\");\n", 942 | "should support a `@jsx` directive" 943 | ); 944 | 945 | Ok(()) 946 | } 947 | 948 | #[test] 949 | fn directive_jsx_empty() -> Result<(), markdown::message::Message> { 950 | assert_eq!( 951 | compile( 952 | "/* @jsxRuntime classic @jsx */\nlet a = ", 953 | &Options::default() 954 | )?, 955 | "let a = React.createElement(\"b\");\n", 956 | "should support an empty `@jsx` directive" 957 | ); 958 | 959 | Ok(()) 960 | } 961 | 962 | #[test] 963 | fn directive_jsx_non_identifier() -> Result<(), markdown::message::Message> { 964 | assert_eq!( 965 | compile( 966 | "/* @jsxRuntime classic @jsx a.b-c.d! */\n", 967 | &Options::default() 968 | )?, 969 | "a[\"b-c\"][\"d!\"](\"x\");\n", 970 | "should support an `@jsx` directive set to an invalid identifier" 971 | ); 972 | 973 | Ok(()) 974 | } 975 | 976 | #[test] 977 | fn directive_jsx_frag() -> Result<(), markdown::message::Message> { 978 | assert_eq!( 979 | compile( 980 | "/* @jsxRuntime classic @jsxFrag a */\nlet b = <>", 981 | &Options::default() 982 | )?, 983 | "let b = React.createElement(a);\n", 984 | "should support a `@jsxFrag` directive" 985 | ); 986 | 987 | Ok(()) 988 | } 989 | 990 | #[test] 991 | fn directive_jsx_frag_empty() -> Result<(), markdown::message::Message> { 992 | assert_eq!( 993 | compile( 994 | "/* @jsxRuntime classic @jsxFrag */\nlet a = <>", 995 | &Options::default() 996 | )?, 997 | "let a = React.createElement(React.Fragment);\n", 998 | "should support an empty `@jsxFrag` directive" 999 | ); 1000 | 1001 | Ok(()) 1002 | } 1003 | 1004 | #[test] 1005 | fn directive_non_first_line() -> Result<(), markdown::message::Message> { 1006 | assert_eq!( 1007 | compile( 1008 | "/*\n first line\n @jsxRuntime classic\n */\n", 1009 | &Options::default() 1010 | )?, 1011 | "React.createElement(\"b\");\n", 1012 | "should support a directive on a non-first line" 1013 | ); 1014 | 1015 | Ok(()) 1016 | } 1017 | 1018 | #[test] 1019 | fn directive_asterisked_line() -> Result<(), markdown::message::Message> { 1020 | assert_eq!( 1021 | compile( 1022 | "/*\n * first line\n * @jsxRuntime classic\n */\n", 1023 | &Options::default() 1024 | )?, 1025 | "React.createElement(\"b\");\n", 1026 | "should support a directive on an asterisk’ed line" 1027 | ); 1028 | 1029 | Ok(()) 1030 | } 1031 | 1032 | #[test] 1033 | fn jsx_element_self_closing() -> Result<(), markdown::message::Message> { 1034 | assert_eq!( 1035 | compile("", &Options::default())?, 1036 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {});\n", 1037 | "should support a self-closing element" 1038 | ); 1039 | 1040 | Ok(()) 1041 | } 1042 | 1043 | #[test] 1044 | fn jsx_element_self_closing_classic() -> Result<(), markdown::message::Message> { 1045 | assert_eq!( 1046 | compile("/* @jsxRuntime classic */\n", &Options::default())?, 1047 | "React.createElement(\"a\");\n", 1048 | "should support a self-closing element (classic)" 1049 | ); 1050 | 1051 | Ok(()) 1052 | } 1053 | 1054 | #[test] 1055 | fn jsx_element_closed() -> Result<(), markdown::message::Message> { 1056 | assert_eq!( 1057 | compile( 1058 | "b", 1059 | &Options::default() 1060 | )?, 1061 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n children: \"b\"\n});\n", 1062 | "should support a closed element" 1063 | ); 1064 | 1065 | Ok(()) 1066 | } 1067 | 1068 | #[test] 1069 | fn jsx_element_member_name() -> Result<(), markdown::message::Message> { 1070 | assert_eq!( 1071 | compile("", &Options::default())?, 1072 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a.b.c, {});\n", 1073 | "should support an element with a member name" 1074 | ); 1075 | 1076 | Ok(()) 1077 | } 1078 | 1079 | #[test] 1080 | fn jsx_element_member_name_dashes() -> Result<(), markdown::message::Message> { 1081 | assert_eq!( 1082 | compile("", &Options::default())?, 1083 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a[\"b-c\"], {});\n", 1084 | "should support an element with a member name and dashes" 1085 | ); 1086 | 1087 | Ok(()) 1088 | } 1089 | #[test] 1090 | fn jsx_element_member_name_many() -> Result<(), markdown::message::Message> { 1091 | assert_eq!( 1092 | compile("", &Options::default())?, 1093 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(a.b.c.d, {});\n", 1094 | "should support an element with a member name of lots of names" 1095 | ); 1096 | 1097 | Ok(()) 1098 | } 1099 | 1100 | #[test] 1101 | fn jsx_element_namespace_name() -> Result<(), markdown::message::Message> { 1102 | assert_eq!( 1103 | compile("", &Options::default())?, 1104 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a:b\", {});\n", 1105 | "should support an element with a namespace name" 1106 | ); 1107 | 1108 | Ok(()) 1109 | } 1110 | 1111 | #[test] 1112 | fn jsx_element_name_dashes() -> Result<(), markdown::message::Message> { 1113 | assert_eq!( 1114 | compile("", &Options::default())?, 1115 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a-b\", {});\n", 1116 | "should support an element with a dash in the name" 1117 | ); 1118 | 1119 | Ok(()) 1120 | } 1121 | 1122 | #[test] 1123 | fn jsx_element_name_capital() -> Result<(), markdown::message::Message> { 1124 | assert_eq!( 1125 | compile("", &Options::default())?, 1126 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(Abc, {});\n", 1127 | "should support an element with a non-lowercase first character in the name" 1128 | ); 1129 | 1130 | Ok(()) 1131 | } 1132 | 1133 | #[test] 1134 | fn jsx_element_attribute_boolean() -> Result<(), markdown::message::Message> { 1135 | assert_eq!( 1136 | compile("", &Options::default())?, 1137 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: true\n});\n", 1138 | "should support an element with a boolean attribute" 1139 | ); 1140 | 1141 | Ok(()) 1142 | } 1143 | 1144 | #[test] 1145 | fn jsx_element_attribute_boolean_classic() -> Result<(), markdown::message::Message> { 1146 | assert_eq!( 1147 | compile("/* @jsxRuntime classic */\n", &Options::default())?, 1148 | "React.createElement(\"a\", {\n b: true\n});\n", 1149 | "should support an element with a boolean attribute (classic" 1150 | ); 1151 | 1152 | Ok(()) 1153 | } 1154 | 1155 | #[test] 1156 | fn jsx_element_attribute_name_namespace() -> Result<(), markdown::message::Message> { 1157 | assert_eq!( 1158 | compile("", &Options::default())?, 1159 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n \"b:c\": true\n});\n", 1160 | "should support an element with colons in an attribute name" 1161 | ); 1162 | 1163 | Ok(()) 1164 | } 1165 | 1166 | #[test] 1167 | fn jsx_element_attribute_name_non_identifier() -> Result<(), markdown::message::Message> { 1168 | assert_eq!( 1169 | compile("", &Options::default())?, 1170 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n \"b-c\": true\n});\n", 1171 | "should support an element with non-identifier characters in an attribute name" 1172 | ); 1173 | 1174 | Ok(()) 1175 | } 1176 | 1177 | #[test] 1178 | fn jsx_element_attribute_value() -> Result<(), markdown::message::Message> { 1179 | assert_eq!( 1180 | compile("", &Options::default())?, 1181 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: \"c\"\n});\n", 1182 | "should support an element with an attribute with a value" 1183 | ); 1184 | 1185 | Ok(()) 1186 | } 1187 | 1188 | #[test] 1189 | fn jsx_element_attribute_value_expression() -> Result<(), markdown::message::Message> { 1190 | assert_eq!( 1191 | compile("", &Options::default())?, 1192 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: c\n});\n", 1193 | "should support an element with an attribute with a value expression" 1194 | ); 1195 | 1196 | Ok(()) 1197 | } 1198 | 1199 | #[test] 1200 | fn jsx_element_attribute_value_fragment() -> Result<(), markdown::message::Message> { 1201 | assert_eq!( 1202 | compile("c />", &Options::default())?, 1203 | "import { Fragment as _Fragment, jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: _jsx(_Fragment, {\n children: \"c\"\n })\n});\n", 1204 | "should support an element with an attribute with a fragment as value" 1205 | ); 1206 | 1207 | Ok(()) 1208 | } 1209 | 1210 | #[test] 1211 | fn jsx_element_attribute_value_element() -> Result<(), markdown::message::Message> { 1212 | assert_eq!( 1213 | compile(" />", &Options::default())?, 1214 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", {\n b: _jsx(\"c\", {})\n});\n", 1215 | "should support an element with an attribute with an element as value" 1216 | ); 1217 | 1218 | Ok(()) 1219 | } 1220 | 1221 | #[test] 1222 | fn jsx_element_spread_attribute() -> Result<(), markdown::message::Message> { 1223 | assert_eq!( 1224 | compile("", &Options::default())?, 1225 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", b);\n", 1226 | "should support an element with a spread attribute" 1227 | ); 1228 | 1229 | Ok(()) 1230 | } 1231 | 1232 | #[test] 1233 | fn jsx_element_spread_attribute_then_prop() -> Result<(), markdown::message::Message> { 1234 | assert_eq!( 1235 | compile("", &Options::default())?, 1236 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({}, b, {\n c: true\n}));\n", 1237 | "should support an element with a spread attribute and then a prop" 1238 | ); 1239 | 1240 | Ok(()) 1241 | } 1242 | 1243 | #[test] 1244 | fn jsx_element_prop_then_spread_attribute() -> Result<(), markdown::message::Message> { 1245 | assert_eq!( 1246 | compile("", &Options::default())?, 1247 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({\n b: true\n}, c));\n", 1248 | "should support an element with a prop and then a spread attribute" 1249 | ); 1250 | 1251 | Ok(()) 1252 | } 1253 | 1254 | #[test] 1255 | fn jsx_element_two_spread_attributes() -> Result<(), markdown::message::Message> { 1256 | assert_eq!( 1257 | compile("", &Options::default())?, 1258 | "import { jsx as _jsx } from \"react/jsx-runtime\";\n_jsx(\"a\", Object.assign({}, b, c));\n", 1259 | "should support an element two spread attributes" 1260 | ); 1261 | 1262 | Ok(()) 1263 | } 1264 | 1265 | #[test] 1266 | fn jsx_element_complex_spread_attribute() -> Result<(), markdown::message::Message> { 1267 | assert_eq!( 1268 | compile("", &Options::default())?, 1269 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1270 | _jsx(\"a\", { 1271 | b: 1, 1272 | ...c, 1273 | d: 2 1274 | }); 1275 | ", 1276 | "should support more complex spreads" 1277 | ); 1278 | 1279 | Ok(()) 1280 | } 1281 | 1282 | #[test] 1283 | fn jsx_element_child_expression() -> Result<(), markdown::message::Message> { 1284 | assert_eq!( 1285 | compile("{1}", &Options::default())?, 1286 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1287 | _jsx(\"a\", { 1288 | children: 1 1289 | }); 1290 | ", 1291 | "should support a child expression" 1292 | ); 1293 | 1294 | Ok(()) 1295 | } 1296 | 1297 | #[test] 1298 | fn jsx_element_child_expression_classic() -> Result<(), markdown::message::Message> { 1299 | assert_eq!( 1300 | compile("/* @jsxRuntime classic */\n{1}", &Options::default())?, 1301 | "React.createElement(\"a\", null, 1);\n", 1302 | "should support a child expression (classic)" 1303 | ); 1304 | 1305 | Ok(()) 1306 | } 1307 | 1308 | #[test] 1309 | fn jsx_element_child_expression_empty() -> Result<(), markdown::message::Message> { 1310 | assert_eq!( 1311 | compile("{}", &Options::default())?, 1312 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1313 | _jsx(\"a\", {}); 1314 | ", 1315 | "should support an empty child expression" 1316 | ); 1317 | 1318 | Ok(()) 1319 | } 1320 | 1321 | #[test] 1322 | fn jsx_element_child_expression_empty_classic() -> Result<(), markdown::message::Message> { 1323 | assert_eq!( 1324 | compile("/* @jsxRuntime classic */\n{}", &Options::default())?, 1325 | "React.createElement(\"a\");\n", 1326 | "should support an empty child expression (classic)" 1327 | ); 1328 | 1329 | Ok(()) 1330 | } 1331 | 1332 | #[test] 1333 | fn jsx_element_child_fragment() -> Result<(), markdown::message::Message> { 1334 | assert_eq!( 1335 | compile("<>b", &Options::default())?, 1336 | "import { Fragment as _Fragment, jsx as _jsx } from \"react/jsx-runtime\"; 1337 | _jsx(\"a\", { 1338 | children: _jsx(_Fragment, { 1339 | children: \"b\" 1340 | }) 1341 | }); 1342 | ", 1343 | "should support a fragment as a child" 1344 | ); 1345 | 1346 | Ok(()) 1347 | } 1348 | 1349 | #[test] 1350 | fn jsx_element_child_spread() { 1351 | let mut program = Program { 1352 | path: None, 1353 | comments: vec![], 1354 | module: Module { 1355 | span: swc_core::common::DUMMY_SP, 1356 | shebang: None, 1357 | body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt { 1358 | span: swc_core::common::DUMMY_SP, 1359 | expr: Box::new(Expr::JSXElement(Box::new(JSXElement { 1360 | span: swc_core::common::DUMMY_SP, 1361 | opening: JSXOpeningElement { 1362 | name: JSXElementName::Ident(create_ident("a").into()), 1363 | attrs: vec![], 1364 | self_closing: false, 1365 | type_args: None, 1366 | span: swc_core::common::DUMMY_SP, 1367 | }, 1368 | closing: Some(JSXClosingElement { 1369 | name: JSXElementName::Ident(create_ident("a").into()), 1370 | span: swc_core::common::DUMMY_SP, 1371 | }), 1372 | children: vec![JSXElementChild::JSXSpreadChild(JSXSpreadChild { 1373 | expr: Box::new(create_ident_expression("a")), 1374 | span: swc_core::common::DUMMY_SP, 1375 | })], 1376 | }))), 1377 | }))], 1378 | }, 1379 | }; 1380 | 1381 | assert_eq!( 1382 | swc_util_build_jsx(&mut program, &Options::default(), None) 1383 | .err() 1384 | .unwrap() 1385 | .to_string(), 1386 | "Unexpected spread child, which is not supported in Babel, SWC, or React (mdxjs-rs:spread)", 1387 | "should not support a spread child" 1388 | ); 1389 | } 1390 | 1391 | #[test] 1392 | fn jsx_element_child_text_padded_start() -> Result<(), markdown::message::Message> { 1393 | assert_eq!( 1394 | compile(" b", &Options::default())?, 1395 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1396 | _jsx(\"a\", { 1397 | children: \" b\" 1398 | }); 1399 | ", 1400 | "should support initial spaces in content" 1401 | ); 1402 | 1403 | Ok(()) 1404 | } 1405 | 1406 | #[test] 1407 | fn jsx_element_child_text_padded_end() -> Result<(), markdown::message::Message> { 1408 | assert_eq!( 1409 | compile("b ", &Options::default())?, 1410 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1411 | _jsx(\"a\", { 1412 | children: \"b \" 1413 | }); 1414 | ", 1415 | "should support final spaces in content" 1416 | ); 1417 | 1418 | Ok(()) 1419 | } 1420 | 1421 | #[test] 1422 | fn jsx_element_child_text_padded() -> Result<(), markdown::message::Message> { 1423 | assert_eq!( 1424 | compile(" b ", &Options::default())?, 1425 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1426 | _jsx(\"a\", { 1427 | children: \" b \" 1428 | }); 1429 | ", 1430 | "should support initial and final spaces in content" 1431 | ); 1432 | 1433 | Ok(()) 1434 | } 1435 | 1436 | #[test] 1437 | fn jsx_element_child_text_line_endings_padded() -> Result<(), markdown::message::Message> { 1438 | assert_eq!( 1439 | compile(" b \r c \n d \n ", &Options::default())?, 1440 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1441 | _jsx(\"a\", { 1442 | children: \" b c d\" 1443 | }); 1444 | ", 1445 | "should support spaces around line endings in content" 1446 | ); 1447 | 1448 | Ok(()) 1449 | } 1450 | 1451 | #[test] 1452 | fn jsx_element_child_text_blank_lines() -> Result<(), markdown::message::Message> { 1453 | assert_eq!( 1454 | compile(" b \r \n c \n\n d \n ", &Options::default())?, 1455 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1456 | _jsx(\"a\", { 1457 | children: \" b c d\" 1458 | }); 1459 | ", 1460 | "should support blank lines in content" 1461 | ); 1462 | 1463 | Ok(()) 1464 | } 1465 | 1466 | #[test] 1467 | fn jsx_element_child_whitespace_only() -> Result<(), markdown::message::Message> { 1468 | assert_eq!( 1469 | compile(" \t\n ", &Options::default())?, 1470 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1471 | _jsx(\"a\", {}); 1472 | ", 1473 | "should support whitespace-only in content" 1474 | ); 1475 | 1476 | Ok(()) 1477 | } 1478 | 1479 | #[test] 1480 | fn jsx_element_key_automatic() -> Result<(), markdown::message::Message> { 1481 | assert_eq!( 1482 | compile("", &Options::default())?, 1483 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1484 | _jsx(\"a\", { 1485 | b: true, 1486 | d: true 1487 | }, \"c\"); 1488 | ", 1489 | "should support a key in the automatic runtime" 1490 | ); 1491 | 1492 | Ok(()) 1493 | } 1494 | 1495 | #[test] 1496 | fn jsx_element_key_classic() -> Result<(), markdown::message::Message> { 1497 | assert_eq!( 1498 | compile( 1499 | "/* @jsxRuntime classic */\n", 1500 | &Options::default() 1501 | )?, 1502 | "React.createElement(\"a\", { 1503 | b: true, 1504 | key: \"c\", 1505 | d: true 1506 | }); 1507 | ", 1508 | "should support a key in the classic runtime" 1509 | ); 1510 | 1511 | Ok(()) 1512 | } 1513 | #[test] 1514 | fn jsx_element_key_after_spread_automatic() { 1515 | assert_eq!( 1516 | compile("", &Options::default()) 1517 | .err() 1518 | .unwrap() 1519 | .to_string(), 1520 | "1:11: Expected `key` to come before any spread expressions (mdxjs-rs:key)", 1521 | "should crash on a key after a spread in the automatic runtime" 1522 | ); 1523 | } 1524 | 1525 | #[test] 1526 | fn jsx_element_key_before_spread_automatic() -> Result<(), markdown::message::Message> { 1527 | assert_eq!( 1528 | compile("", &Options::default())?, 1529 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 1530 | _jsx(\"a\", c, \"b\"); 1531 | ", 1532 | "should support a key before a spread in the automatic runtime" 1533 | ); 1534 | 1535 | Ok(()) 1536 | } 1537 | 1538 | #[test] 1539 | fn jsx_element_development() -> Result<(), markdown::message::Message> { 1540 | assert_eq!( 1541 | compile("<>", &Options { development: true })?, 1542 | "import { Fragment as _Fragment, jsxDEV as _jsxDEV } from \"react/jsx-dev-runtime\"; 1543 | _jsxDEV(_Fragment, { 1544 | children: _jsxDEV(\"a\", {}, undefined, false, { 1545 | fileName: \"example.jsx\", 1546 | lineNumber: 1, 1547 | columnNumber: 3 1548 | }, this) 1549 | }, undefined, false, { 1550 | fileName: \"example.jsx\", 1551 | lineNumber: 1, 1552 | columnNumber: 1 1553 | }, this); 1554 | ", 1555 | "should support the automatic development runtime if `development` is on" 1556 | ); 1557 | 1558 | Ok(()) 1559 | } 1560 | 1561 | #[test] 1562 | fn jsx_element_development_no_filepath() -> Result<(), markdown::message::Message> { 1563 | let mut program = Program { 1564 | path: None, 1565 | comments: vec![], 1566 | module: Module { 1567 | span: swc_core::common::DUMMY_SP, 1568 | shebang: None, 1569 | body: vec![ModuleItem::Stmt(Stmt::Expr(ExprStmt { 1570 | span: swc_core::common::DUMMY_SP, 1571 | expr: Box::new(Expr::JSXElement(Box::new(JSXElement { 1572 | span: swc_core::common::DUMMY_SP, 1573 | opening: JSXOpeningElement { 1574 | name: JSXElementName::Ident(create_ident("a").into()), 1575 | attrs: vec![], 1576 | self_closing: true, 1577 | type_args: None, 1578 | span: swc_core::common::DUMMY_SP, 1579 | }, 1580 | closing: None, 1581 | children: vec![], 1582 | }))), 1583 | }))], 1584 | }, 1585 | }; 1586 | 1587 | swc_util_build_jsx(&mut program, &Options { development: true }, None)?; 1588 | 1589 | assert_eq!( 1590 | serialize(&mut program.module, Some(&program.comments)), 1591 | "import { jsxDEV as _jsxDEV } from \"react/jsx-dev-runtime\"; 1592 | _jsxDEV(\"a\", {}, undefined, false, { 1593 | fileName: \"\" 1594 | }, this); 1595 | ", 1596 | "should support the automatic development runtime without a file path" 1597 | ); 1598 | 1599 | Ok(()) 1600 | } 1601 | 1602 | #[test] 1603 | fn jsx_text() { 1604 | assert_eq!(jsx_text_to_value("a"), "a", "should support jsx text"); 1605 | assert_eq!( 1606 | jsx_text_to_value(" a\t"), 1607 | " a ", 1608 | "should support jsx text w/ initial, final whitespace" 1609 | ); 1610 | assert_eq!( 1611 | jsx_text_to_value(" \t"), 1612 | "", 1613 | "should support jsx text that’s just whitespace" 1614 | ); 1615 | assert_eq!( 1616 | jsx_text_to_value("a\r\r\n\nb"), 1617 | "a b", 1618 | "should support jsx text with line endings" 1619 | ); 1620 | assert_eq!( 1621 | jsx_text_to_value(" a \n b \n c "), 1622 | " a b c ", 1623 | "should support jsx text with line endings with white space" 1624 | ); 1625 | assert_eq!( 1626 | jsx_text_to_value(" \n a \n "), 1627 | "a", 1628 | "should support jsx text with blank initial and final lines" 1629 | ); 1630 | assert_eq!( 1631 | jsx_text_to_value(" a \n \n \t \n b "), 1632 | " a b ", 1633 | "should support jsx text with blank lines in between" 1634 | ); 1635 | assert_eq!( 1636 | jsx_text_to_value(" \n \n \t \n "), 1637 | "", 1638 | "should support jsx text with only spaces, tabs, and line endings" 1639 | ); 1640 | } 1641 | } 1642 | -------------------------------------------------------------------------------- /src/swc_utils.rs: -------------------------------------------------------------------------------- 1 | //! Lots of helpers for dealing with SWC, particularly from unist, and for 2 | //! building its ES AST. 3 | 4 | use markdown::{ 5 | id_cont, id_start, 6 | mdast::Stop, 7 | unist::{Point, Position}, 8 | Location, 9 | }; 10 | 11 | use swc_core::ecma::ast::{ 12 | BinExpr, BinaryOp, Bool, CallExpr, Callee, ComputedPropName, Expr, ExprOrSpread, Ident, 13 | JSXAttrName, JSXElementName, JSXMemberExpr, JSXNamespacedName, JSXObject, Lit, MemberExpr, 14 | MemberProp, Null, Number, ObjectLit, PropName, PropOrSpread, Str, 15 | }; 16 | use swc_core::ecma::visit::{noop_visit_mut_type, VisitMut}; 17 | use swc_core::{ 18 | common::{BytePos, Span, SyntaxContext, DUMMY_SP}, 19 | ecma::ast::IdentName, 20 | }; 21 | 22 | /// Turn a unist position, into an SWC span, of two byte positions. 23 | /// 24 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 25 | /// > are missing or incremented by `1` when valid. 26 | pub fn position_to_span(position: Option<&Position>) -> Span { 27 | position.map_or(DUMMY_SP, |d| Span { 28 | lo: point_to_bytepos(&d.start), 29 | hi: point_to_bytepos(&d.end), 30 | }) 31 | } 32 | 33 | /// Turn an SWC span, of two byte positions, into a unist position. 34 | /// 35 | /// This assumes the span comes from a fixed tree, or is a dummy. 36 | /// 37 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 38 | /// > are missing or incremented by `1` when valid. 39 | pub fn span_to_position(span: Span, location: Option<&Location>) -> Option { 40 | let lo = span.lo.0 as usize; 41 | let hi = span.hi.0 as usize; 42 | 43 | if lo > 0 && hi > 0 { 44 | if let Some(location) = location { 45 | if let Some(start) = location.to_point(lo - 1) { 46 | if let Some(end) = location.to_point(hi - 1) { 47 | return Some(Position { start, end }); 48 | } 49 | } 50 | } 51 | } 52 | 53 | None 54 | } 55 | 56 | /// Turn a unist point into an SWC byte position. 57 | /// 58 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 59 | /// > are missing or incremented by `1` when valid. 60 | pub fn point_to_bytepos(point: &Point) -> BytePos { 61 | BytePos(point.offset as u32 + 1) 62 | } 63 | 64 | /// Turn an SWC byte position into a unist point. 65 | /// 66 | /// This assumes the byte position comes from a fixed tree, or is a dummy. 67 | /// 68 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 69 | /// > are missing or incremented by `1` when valid. 70 | pub fn bytepos_to_point(bytepos: BytePos, location: Option<&Location>) -> Option { 71 | let pos = bytepos.0 as usize; 72 | 73 | if pos > 0 { 74 | if let Some(location) = location { 75 | return location.to_point(pos - 1); 76 | } 77 | } 78 | 79 | None 80 | } 81 | 82 | /// Serialize a unist position for humans. 83 | pub fn position_opt_to_string(position: Option<&Position>) -> String { 84 | if let Some(position) = position { 85 | position_to_string(position) 86 | } else { 87 | "0:0".into() 88 | } 89 | } 90 | 91 | /// Serialize a unist position for humans. 92 | pub fn position_to_string(position: &Position) -> String { 93 | format!( 94 | "{}-{}", 95 | point_to_string(&position.start), 96 | point_to_string(&position.end) 97 | ) 98 | } 99 | 100 | /// Serialize a unist point for humans. 101 | pub fn point_to_string(point: &Point) -> String { 102 | format!("{}:{}", point.line, point.column) 103 | } 104 | 105 | /// Visitor to fix SWC byte positions. 106 | /// 107 | /// This assumes the byte position comes from an **unfixed** tree. 108 | /// 109 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 110 | /// > are missing or incremented by `1` when valid. 111 | #[derive(Debug, Default, Clone)] 112 | pub struct RewriteStopsContext<'a> { 113 | /// Stops in the original source. 114 | pub stops: &'a [Stop], 115 | /// Location info. 116 | pub location: Option<&'a Location>, 117 | } 118 | 119 | impl VisitMut for RewriteStopsContext<'_> { 120 | noop_visit_mut_type!(); 121 | 122 | /// Rewrite spans. 123 | fn visit_mut_span(&mut self, span: &mut Span) { 124 | let mut result = DUMMY_SP; 125 | let lo_rel = span.lo.0 as usize; 126 | let hi_rel = span.hi.0 as usize; 127 | 128 | let lo_clean = Location::relative_to_absolute(self.stops, lo_rel - 1); 129 | let hi_clean = Location::relative_to_absolute(self.stops, hi_rel - 1); 130 | if let Some(lo_abs) = lo_clean { 131 | if let Some(hi_abs) = hi_clean { 132 | result = create_span(lo_abs as u32 + 1, hi_abs as u32 + 1); 133 | } 134 | } 135 | 136 | *span = result; 137 | } 138 | } 139 | 140 | /// Visitor to fix SWC byte positions by removing a prefix. 141 | /// 142 | /// > 👉 **Note**: SWC byte positions are offset by one: they are `0` when they 143 | /// > are missing or incremented by `1` when valid. 144 | #[derive(Debug, Default, Clone)] 145 | pub struct RewritePrefixContext { 146 | /// Size of prefix considered outside this tree. 147 | pub prefix_len: u32, 148 | } 149 | 150 | impl VisitMut for RewritePrefixContext { 151 | noop_visit_mut_type!(); 152 | 153 | /// Rewrite spans. 154 | fn visit_mut_span(&mut self, span: &mut Span) { 155 | let mut result = DUMMY_SP; 156 | if span.lo.0 > self.prefix_len && span.hi.0 > self.prefix_len { 157 | result = create_span(span.lo.0 - self.prefix_len, span.hi.0 - self.prefix_len); 158 | } 159 | 160 | *span = result; 161 | } 162 | } 163 | 164 | /// Visitor to drop SWC spans. 165 | #[derive(Debug, Default, Clone)] 166 | pub struct DropContext {} 167 | 168 | impl VisitMut for DropContext { 169 | noop_visit_mut_type!(); 170 | 171 | /// Rewrite spans. 172 | fn visit_mut_span(&mut self, span: &mut Span) { 173 | *span = DUMMY_SP; 174 | } 175 | } 176 | 177 | /// Generate a span. 178 | pub fn create_span(lo: u32, hi: u32) -> Span { 179 | Span { 180 | lo: BytePos(lo), 181 | hi: BytePos(hi), 182 | } 183 | } 184 | 185 | /// Generate an ident. 186 | /// 187 | /// ```js 188 | /// a 189 | /// ``` 190 | pub fn create_ident(sym: &str) -> IdentName { 191 | IdentName { 192 | sym: sym.into(), 193 | span: DUMMY_SP, 194 | } 195 | } 196 | 197 | /// Generate an ident expression. 198 | /// 199 | /// ```js 200 | /// a 201 | /// ``` 202 | pub fn create_ident_expression(sym: &str) -> Expr { 203 | Expr::Ident(create_ident(sym).into()) 204 | } 205 | 206 | /// Generate a null. 207 | pub fn create_null() -> Null { 208 | Null { 209 | span: swc_core::common::DUMMY_SP, 210 | } 211 | } 212 | 213 | /// Generate a null. 214 | pub fn create_null_lit() -> Lit { 215 | Lit::Null(create_null()) 216 | } 217 | 218 | /// Generate a null. 219 | pub fn create_null_expression() -> Expr { 220 | Expr::Lit(create_null_lit()) 221 | } 222 | 223 | /// Generate a null. 224 | pub fn create_str(value: &str) -> Str { 225 | value.into() 226 | } 227 | 228 | /// Generate a str. 229 | pub fn create_str_lit(value: &str) -> Lit { 230 | Lit::Str(create_str(value)) 231 | } 232 | 233 | /// Generate a str. 234 | pub fn create_str_expression(value: &str) -> Expr { 235 | Expr::Lit(create_str_lit(value)) 236 | } 237 | 238 | /// Generate a bool. 239 | pub fn create_bool(value: bool) -> Bool { 240 | value.into() 241 | } 242 | 243 | /// Generate a bool. 244 | pub fn create_bool_lit(value: bool) -> Lit { 245 | Lit::Bool(create_bool(value)) 246 | } 247 | 248 | /// Generate a bool. 249 | pub fn create_bool_expression(value: bool) -> Expr { 250 | Expr::Lit(create_bool_lit(value)) 251 | } 252 | 253 | /// Generate a number. 254 | pub fn create_num(value: f64) -> Number { 255 | value.into() 256 | } 257 | 258 | /// Generate a num. 259 | pub fn create_num_lit(value: f64) -> Lit { 260 | Lit::Num(create_num(value)) 261 | } 262 | 263 | /// Generate a num. 264 | pub fn create_num_expression(value: f64) -> Expr { 265 | Expr::Lit(create_num_lit(value)) 266 | } 267 | 268 | /// Generate an object. 269 | pub fn create_object_lit(value: Vec) -> ObjectLit { 270 | ObjectLit { 271 | props: value, 272 | span: DUMMY_SP, 273 | } 274 | } 275 | 276 | /// Generate an object. 277 | pub fn create_object_expression(value: Vec) -> Expr { 278 | Expr::Object(create_object_lit(value)) 279 | } 280 | 281 | /// Generate a call. 282 | pub fn create_call(callee: Callee, args: Vec) -> CallExpr { 283 | CallExpr { 284 | callee, 285 | args, 286 | span: DUMMY_SP, 287 | type_args: None, 288 | ctxt: SyntaxContext::empty(), 289 | } 290 | } 291 | 292 | /// Generate a call. 293 | pub fn create_call_expression(callee: Callee, args: Vec) -> Expr { 294 | Expr::Call(create_call(callee, args)) 295 | } 296 | 297 | /// Generate a binary expression. 298 | /// 299 | /// ```js 300 | /// a + b + c 301 | /// a || b 302 | /// ``` 303 | pub fn create_binary_expression(mut exprs: Vec, op: BinaryOp) -> Expr { 304 | exprs.reverse(); 305 | 306 | let mut left = None; 307 | 308 | while let Some(right_expr) = exprs.pop() { 309 | left = Some(if let Some(left_expr) = left { 310 | Expr::Bin(BinExpr { 311 | left: Box::new(left_expr), 312 | right: Box::new(right_expr), 313 | op, 314 | span: DUMMY_SP, 315 | }) 316 | } else { 317 | right_expr 318 | }); 319 | } 320 | 321 | left.expect("expected one or more expressions") 322 | } 323 | 324 | /// Generate a member expression from a string. 325 | /// 326 | /// ```js 327 | /// a.b 328 | /// a 329 | /// ``` 330 | pub fn create_member_expression_from_str(name: &str) -> Expr { 331 | match parse_js_name(name) { 332 | // `a` 333 | JsName::Normal(name) => create_ident_expression(name), 334 | // `a.b.c` 335 | JsName::Member(parts) => { 336 | let mut member = create_member( 337 | create_ident_expression(parts[0]), 338 | create_member_prop_from_str(parts[1]), 339 | ); 340 | let mut index = 2; 341 | while index < parts.len() { 342 | member = create_member( 343 | Expr::Member(member), 344 | create_member_prop_from_str(parts[index]), 345 | ); 346 | index += 1; 347 | } 348 | Expr::Member(member) 349 | } 350 | } 351 | } 352 | 353 | /// Generate a member expression from an object and prop. 354 | pub fn create_member(obj: Expr, prop: MemberProp) -> MemberExpr { 355 | MemberExpr { 356 | obj: Box::new(obj), 357 | prop, 358 | span: DUMMY_SP, 359 | } 360 | } 361 | 362 | /// Create a member prop from a string. 363 | pub fn create_member_prop_from_str(name: &str) -> MemberProp { 364 | if is_identifier_name(name) { 365 | MemberProp::Ident(create_ident(name)) 366 | } else { 367 | MemberProp::Computed(ComputedPropName { 368 | expr: Box::new(create_str_expression(name)), 369 | span: DUMMY_SP, 370 | }) 371 | } 372 | } 373 | 374 | /// Generate a member expression from a string. 375 | /// 376 | /// ```js 377 | /// a.b-c 378 | /// a 379 | /// ``` 380 | pub fn create_jsx_name_from_str(name: &str) -> JSXElementName { 381 | match parse_jsx_name(name) { 382 | // `a` 383 | JsxName::Normal(name) => JSXElementName::Ident(create_ident(name).into()), 384 | // `a:b` 385 | JsxName::Namespace(ns, name) => JSXElementName::JSXNamespacedName(JSXNamespacedName { 386 | span: DUMMY_SP, 387 | ns: create_ident(ns), 388 | name: create_ident(name), 389 | }), 390 | // `a.b.c` 391 | JsxName::Member(parts) => { 392 | let mut member = create_jsx_member( 393 | JSXObject::Ident(create_ident(parts[0]).into()), 394 | create_ident(parts[1]), 395 | ); 396 | let mut index = 2; 397 | while index < parts.len() { 398 | member = create_jsx_member( 399 | JSXObject::JSXMemberExpr(Box::new(member)), 400 | create_ident(parts[index]), 401 | ); 402 | index += 1; 403 | } 404 | JSXElementName::JSXMemberExpr(member) 405 | } 406 | } 407 | } 408 | 409 | /// Generate a member expression from an object and prop. 410 | pub fn create_jsx_member(obj: JSXObject, prop: IdentName) -> JSXMemberExpr { 411 | JSXMemberExpr { 412 | span: DUMMY_SP, 413 | obj, 414 | prop, 415 | } 416 | } 417 | 418 | /// Turn an JSX element name into an expression. 419 | pub fn jsx_element_name_to_expression(node: JSXElementName) -> Expr { 420 | match node { 421 | JSXElementName::JSXMemberExpr(member_expr) => { 422 | jsx_member_expression_to_expression(member_expr) 423 | } 424 | JSXElementName::JSXNamespacedName(namespace_name) => create_str_expression(&format!( 425 | "{}:{}", 426 | namespace_name.ns.sym, namespace_name.name.sym 427 | )), 428 | JSXElementName::Ident(ident) => create_ident_or_literal(&ident), 429 | } 430 | } 431 | 432 | /// Create a JSX attribute name. 433 | pub fn create_jsx_attr_name_from_str(name: &str) -> JSXAttrName { 434 | match parse_jsx_name(name) { 435 | JsxName::Member(_) => { 436 | unreachable!("member expressions in attribute names are not supported") 437 | } 438 | // `` 439 | JsxName::Namespace(ns, name) => JSXAttrName::JSXNamespacedName(JSXNamespacedName { 440 | span: DUMMY_SP, 441 | ns: create_ident(ns), 442 | name: create_ident(name), 443 | }), 444 | // `` 445 | JsxName::Normal(name) => JSXAttrName::Ident(create_ident(name)), 446 | } 447 | } 448 | 449 | /// Turn a JSX member expression name into a member expression. 450 | pub fn jsx_member_expression_to_expression(node: JSXMemberExpr) -> Expr { 451 | Expr::Member(create_member( 452 | jsx_object_to_expression(node.obj), 453 | ident_to_member_prop(&node.prop), 454 | )) 455 | } 456 | 457 | /// Turn an ident into a member prop. 458 | pub fn ident_to_member_prop(node: &IdentName) -> MemberProp { 459 | if is_identifier_name(node.as_ref()) { 460 | MemberProp::Ident(IdentName { 461 | sym: node.sym.clone(), 462 | span: node.span, 463 | }) 464 | } else { 465 | MemberProp::Computed(ComputedPropName { 466 | expr: Box::new(create_str_expression(&node.sym)), 467 | span: node.span, 468 | }) 469 | } 470 | } 471 | 472 | /// Turn a JSX attribute name into a prop prop. 473 | pub fn jsx_attribute_name_to_prop_name(node: JSXAttrName) -> PropName { 474 | match node { 475 | JSXAttrName::JSXNamespacedName(namespace_name) => create_prop_name(&format!( 476 | "{}:{}", 477 | namespace_name.ns.sym, namespace_name.name.sym 478 | )), 479 | JSXAttrName::Ident(ident) => create_prop_name(&ident.sym), 480 | } 481 | } 482 | 483 | /// Turn a JSX object into an expression. 484 | pub fn jsx_object_to_expression(node: JSXObject) -> Expr { 485 | match node { 486 | JSXObject::Ident(ident) => create_ident_or_literal(&ident), 487 | JSXObject::JSXMemberExpr(member_expr) => jsx_member_expression_to_expression(*member_expr), 488 | } 489 | } 490 | 491 | /// Create either an ident expression or a literal expression. 492 | pub fn create_ident_or_literal(node: &Ident) -> Expr { 493 | if is_identifier_name(node.as_ref()) { 494 | create_ident_expression(node.sym.as_ref()) 495 | } else { 496 | create_str_expression(&node.sym) 497 | } 498 | } 499 | 500 | /// Create a prop name. 501 | pub fn create_prop_name(name: &str) -> PropName { 502 | if is_identifier_name(name) { 503 | PropName::Ident(create_ident(name)) 504 | } else { 505 | PropName::Str(create_str(name)) 506 | } 507 | } 508 | 509 | /// Check if a name is a literal tag name or an identifier to a component. 510 | pub fn is_literal_name(name: &str) -> bool { 511 | matches!(name.as_bytes().first(), Some(b'a'..=b'z')) || !is_identifier_name(name) 512 | } 513 | 514 | /// Check if a name is a valid identifier name. 515 | pub fn is_identifier_name(name: &str) -> bool { 516 | for (index, char) in name.chars().enumerate() { 517 | if if index == 0 { 518 | !id_start(char) 519 | } else { 520 | !id_cont(char, false) 521 | } { 522 | return false; 523 | } 524 | } 525 | 526 | true 527 | } 528 | 529 | /// Different kinds of JS names. 530 | pub enum JsName<'a> { 531 | /// Member: `a.b.c` 532 | Member(Vec<&'a str>), 533 | /// Name: `a` 534 | Normal(&'a str), 535 | } 536 | 537 | /// Different kinds of JSX names. 538 | pub enum JsxName<'a> { 539 | /// Member: `a.b.c` 540 | Member(Vec<&'a str>), 541 | /// Namespace: `a:b` 542 | Namespace(&'a str, &'a str), 543 | /// Name: `a` 544 | Normal(&'a str), 545 | } 546 | 547 | /// Parse a JavaScript member expression or name. 548 | pub fn parse_js_name(name: &str) -> JsName { 549 | let bytes = name.as_bytes(); 550 | let mut index = 0; 551 | let mut start = 0; 552 | let mut parts = vec![]; 553 | 554 | while index < bytes.len() { 555 | if bytes[index] == b'.' { 556 | parts.push(&name[start..index]); 557 | start = index + 1; 558 | } 559 | 560 | index += 1; 561 | } 562 | 563 | // `a` 564 | if parts.is_empty() { 565 | JsName::Normal(name) 566 | } 567 | // `a.b.c` 568 | else { 569 | parts.push(&name[start..]); 570 | JsName::Member(parts) 571 | } 572 | } 573 | 574 | /// Parse a JSX name from a string. 575 | pub fn parse_jsx_name(name: &str) -> JsxName { 576 | match parse_js_name(name) { 577 | // `` 578 | JsName::Member(parts) => JsxName::Member(parts), 579 | JsName::Normal(name) => { 580 | // `` 581 | if let Some(colon) = name.as_bytes().iter().position(|d| matches!(d, b':')) { 582 | JsxName::Namespace(&name[0..colon], &name[(colon + 1)..]) 583 | } 584 | // `` 585 | else { 586 | JsxName::Normal(name) 587 | } 588 | } 589 | } 590 | } 591 | 592 | /// Get the identifiers used in a JSX member expression. 593 | /// 594 | /// `Foo.Bar` -> `vec!["Foo", "Bar"]` 595 | pub fn jsx_member_to_parts(node: &JSXMemberExpr) -> Vec<&str> { 596 | let mut parts = vec![]; 597 | let mut member_opt = Some(node); 598 | 599 | while let Some(member) = member_opt { 600 | parts.push(member.prop.sym.as_ref()); 601 | match &member.obj { 602 | JSXObject::Ident(d) => { 603 | parts.push(d.sym.as_ref()); 604 | member_opt = None; 605 | } 606 | JSXObject::JSXMemberExpr(node) => { 607 | member_opt = Some(node); 608 | } 609 | } 610 | } 611 | 612 | parts.reverse(); 613 | parts 614 | } 615 | 616 | /// Check if a text value is inter-element whitespace. 617 | /// 618 | /// See: . 619 | pub fn inter_element_whitespace(value: &str) -> bool { 620 | let bytes = value.as_bytes(); 621 | let mut index = 0; 622 | 623 | while index < bytes.len() { 624 | match bytes[index] { 625 | b'\t' | 0x0C | b'\r' | b'\n' | b' ' => {} 626 | _ => return false, 627 | } 628 | index += 1; 629 | } 630 | 631 | true 632 | } 633 | 634 | #[cfg(test)] 635 | mod tests { 636 | use super::*; 637 | use pretty_assertions::assert_eq; 638 | 639 | #[test] 640 | fn bytepos_to_point_test() { 641 | assert_eq!( 642 | bytepos_to_point(BytePos(123), None), 643 | None, 644 | "should support no location" 645 | ); 646 | } 647 | 648 | #[test] 649 | fn position_opt_to_string_test() { 650 | assert_eq!( 651 | position_opt_to_string(None), 652 | "0:0", 653 | "should support no position" 654 | ); 655 | } 656 | 657 | #[test] 658 | fn jsx_member_to_parts_test() { 659 | assert_eq!( 660 | jsx_member_to_parts(&JSXMemberExpr { 661 | span: DUMMY_SP, 662 | prop: create_ident("a"), 663 | obj: JSXObject::Ident(create_ident("b").into()) 664 | }), 665 | vec!["b", "a"], 666 | "should support a member with 2 items" 667 | ); 668 | 669 | assert_eq!( 670 | jsx_member_to_parts(&JSXMemberExpr { 671 | span: DUMMY_SP, 672 | prop: create_ident("a"), 673 | obj: JSXObject::JSXMemberExpr(Box::new(JSXMemberExpr { 674 | span: DUMMY_SP, 675 | prop: create_ident("b"), 676 | obj: JSXObject::JSXMemberExpr(Box::new(JSXMemberExpr { 677 | span: DUMMY_SP, 678 | prop: create_ident("c"), 679 | obj: JSXObject::Ident(create_ident("d").into()) 680 | })) 681 | })) 682 | }), 683 | vec!["d", "c", "b", "a"], 684 | "should support a member with 4 items" 685 | ); 686 | } 687 | } 688 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | extern crate mdxjs; 2 | use mdxjs::{compile, JsxRuntime, Options}; 3 | use pretty_assertions::assert_eq; 4 | 5 | #[test] 6 | fn simple() -> Result<(), markdown::message::Message> { 7 | assert_eq!( 8 | compile("", &Options::default())?, 9 | "import { Fragment as _Fragment, jsx as _jsx } from \"react/jsx-runtime\"; 10 | function _createMdxContent(props) { 11 | return _jsx(_Fragment, {}); 12 | } 13 | function MDXContent(props = {}) { 14 | const { wrapper: MDXLayout } = props.components || {}; 15 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 16 | children: _jsx(_createMdxContent, props) 17 | })) : _createMdxContent(props); 18 | } 19 | export default MDXContent; 20 | ", 21 | "should work", 22 | ); 23 | 24 | Ok(()) 25 | } 26 | 27 | #[test] 28 | fn development() -> Result<(), markdown::message::Message> { 29 | assert_eq!( 30 | compile("", &Options { 31 | development: true, 32 | filepath: Some("example.mdx".into()), 33 | ..Default::default() 34 | })?, 35 | "import { jsxDEV as _jsxDEV } from \"react/jsx-dev-runtime\"; 36 | function _createMdxContent(props) { 37 | const { A } = props.components || {}; 38 | if (!A) _missingMdxReference(\"A\", true, \"1:1-1:6\"); 39 | return _jsxDEV(A, {}, undefined, false, { 40 | fileName: \"example.mdx\", 41 | lineNumber: 1, 42 | columnNumber: 1 43 | }, this); 44 | } 45 | function MDXContent(props = {}) { 46 | const { wrapper: MDXLayout } = props.components || {}; 47 | return MDXLayout ? _jsxDEV(MDXLayout, Object.assign({}, props, { 48 | children: _jsxDEV(_createMdxContent, props, undefined, false, { 49 | fileName: \"example.mdx\" 50 | }, this) 51 | }), undefined, false, { 52 | fileName: \"example.mdx\" 53 | }, this) : _createMdxContent(props); 54 | } 55 | export default MDXContent; 56 | function _missingMdxReference(id, component, place) { 57 | 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`\" : \"\")); 58 | } 59 | ", 60 | "should support `options.development: true`", 61 | ); 62 | 63 | Ok(()) 64 | } 65 | 66 | #[test] 67 | fn provider() -> Result<(), markdown::message::Message> { 68 | assert_eq!( 69 | compile("", &Options { 70 | provider_import_source: Some("@mdx-js/react".into()), 71 | ..Default::default() 72 | })?, 73 | "import { jsx as _jsx } from \"react/jsx-runtime\"; 74 | import { useMDXComponents as _provideComponents } from \"@mdx-js/react\"; 75 | function _createMdxContent(props) { 76 | const { A } = Object.assign({}, _provideComponents(), props.components); 77 | if (!A) _missingMdxReference(\"A\", true); 78 | return _jsx(A, {}); 79 | } 80 | function MDXContent(props = {}) { 81 | const { wrapper: MDXLayout } = Object.assign({}, _provideComponents(), props.components); 82 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 83 | children: _jsx(_createMdxContent, props) 84 | })) : _createMdxContent(props); 85 | } 86 | export default MDXContent; 87 | function _missingMdxReference(id, component) { 88 | throw new Error(\"Expected \" + (component ? \"component\" : \"object\") + \" `\" + id + \"` to be defined: you likely forgot to import, pass, or provide it.\"); 89 | } 90 | ", 91 | "should support `options.provider_import_source`", 92 | ); 93 | 94 | Ok(()) 95 | } 96 | 97 | #[test] 98 | fn jsx() -> Result<(), markdown::message::Message> { 99 | assert_eq!( 100 | compile("", &Options { 101 | jsx: true, 102 | ..Default::default() 103 | })?, 104 | "function _createMdxContent(props) { 105 | return <>; 106 | } 107 | function MDXContent(props = {}) { 108 | const { wrapper: MDXLayout } = props.components || {}; 109 | return MDXLayout ? <_createMdxContent {...props}/> : _createMdxContent(props); 110 | } 111 | export default MDXContent; 112 | ", 113 | "should support `options.jsx: true`", 114 | ); 115 | 116 | Ok(()) 117 | } 118 | 119 | #[test] 120 | fn classic() -> Result<(), markdown::message::Message> { 121 | assert_eq!( 122 | compile("", &Options { 123 | jsx_runtime: Some(JsxRuntime::Classic), 124 | ..Default::default() 125 | })?, 126 | "import React from \"react\"; 127 | function _createMdxContent(props) { 128 | return React.createElement(React.Fragment); 129 | } 130 | function MDXContent(props = {}) { 131 | const { wrapper: MDXLayout } = props.components || {}; 132 | return MDXLayout ? React.createElement(MDXLayout, props, React.createElement(_createMdxContent, props)) : _createMdxContent(props); 133 | } 134 | export default MDXContent; 135 | ", 136 | "should support `options.jsx_runtime: JsxRuntime::Classic`", 137 | ); 138 | 139 | Ok(()) 140 | } 141 | 142 | #[test] 143 | fn import_source() -> Result<(), markdown::message::Message> { 144 | assert_eq!( 145 | compile( 146 | "", 147 | &Options { 148 | jsx_import_source: Some("preact".into()), 149 | ..Default::default() 150 | } 151 | )?, 152 | "import { Fragment as _Fragment, jsx as _jsx } from \"preact/jsx-runtime\"; 153 | function _createMdxContent(props) { 154 | return _jsx(_Fragment, {}); 155 | } 156 | function MDXContent(props = {}) { 157 | const { wrapper: MDXLayout } = props.components || {}; 158 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 159 | children: _jsx(_createMdxContent, props) 160 | })) : _createMdxContent(props); 161 | } 162 | export default MDXContent; 163 | ", 164 | "should support `options.jsx_import_source: Some(\"preact\".into())`", 165 | ); 166 | 167 | Ok(()) 168 | } 169 | 170 | #[test] 171 | fn pragmas() -> Result<(), markdown::message::Message> { 172 | assert_eq!( 173 | compile("", &Options { 174 | jsx_runtime: Some(JsxRuntime::Classic), 175 | pragma: Some("a.b".into()), 176 | pragma_frag: Some("a.c".into()), 177 | pragma_import_source: Some("d".into()), 178 | ..Default::default() 179 | })?, 180 | "import a from \"d\"; 181 | function _createMdxContent(props) { 182 | return a.b(a.c); 183 | } 184 | function MDXContent(props = {}) { 185 | const { wrapper: MDXLayout } = props.components || {}; 186 | return MDXLayout ? a.b(MDXLayout, props, a.b(_createMdxContent, props)) : _createMdxContent(props); 187 | } 188 | export default MDXContent; 189 | ", 190 | "should support `options.pragma`, `options.pragma_frag`, `options.pragma_import_source`", 191 | ); 192 | 193 | Ok(()) 194 | } 195 | 196 | #[test] 197 | fn unravel_elements() -> Result<(), markdown::message::Message> { 198 | assert_eq!( 199 | compile( 200 | "a 201 | 202 | b 203 | 204 | ", 205 | &Default::default() 206 | )?, 207 | "import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\"; 208 | function _createMdxContent(props) { 209 | const _components = Object.assign({ 210 | x: \"x\", 211 | p: \"p\" 212 | }, props.components); 213 | return _jsxs(_Fragment, { 214 | children: [ 215 | _jsx(\"x\", { 216 | children: \"a\" 217 | }), 218 | \"\\n\", 219 | _jsx(\"x\", { 220 | children: _jsx(_components.p, { 221 | children: \"b\" 222 | }) 223 | }) 224 | ] 225 | }); 226 | } 227 | function MDXContent(props = {}) { 228 | const { wrapper: MDXLayout } = props.components || {}; 229 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 230 | children: _jsx(_createMdxContent, props) 231 | })) : _createMdxContent(props); 232 | } 233 | export default MDXContent; 234 | ", 235 | "should unravel paragraphs (1)", 236 | ); 237 | 238 | Ok(()) 239 | } 240 | 241 | #[test] 242 | fn unravel_expressions() -> Result<(), markdown::message::Message> { 243 | assert_eq!( 244 | compile("{1} {2}", &Default::default())?, 245 | "import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\"; 246 | function _createMdxContent(props) { 247 | return _jsxs(_Fragment, { 248 | children: [ 249 | 1, 250 | \"\\n\", 251 | \" \", 252 | \"\\n\", 253 | 2 254 | ] 255 | }); 256 | } 257 | function MDXContent(props = {}) { 258 | const { wrapper: MDXLayout } = props.components || {}; 259 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 260 | children: _jsx(_createMdxContent, props) 261 | })) : _createMdxContent(props); 262 | } 263 | export default MDXContent; 264 | ", 265 | "should unravel paragraphs (2)", 266 | ); 267 | 268 | Ok(()) 269 | } 270 | 271 | #[test] 272 | fn explicit_jsx() -> Result<(), markdown::message::Message> { 273 | assert_eq!( 274 | compile( 275 | "

asd

276 | # qwe 277 | ", 278 | &Default::default() 279 | )?, 280 | "import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from \"react/jsx-runtime\"; 281 | function _createMdxContent(props) { 282 | const _components = Object.assign({ 283 | h1: \"h1\" 284 | }, props.components); 285 | return _jsxs(_Fragment, { 286 | children: [ 287 | _jsx(\"h1\", { 288 | children: \"asd\" 289 | }), 290 | \"\\n\", 291 | _jsx(_components.h1, { 292 | children: \"qwe\" 293 | }) 294 | ] 295 | }); 296 | } 297 | function MDXContent(props = {}) { 298 | const { wrapper: MDXLayout } = props.components || {}; 299 | return MDXLayout ? _jsx(MDXLayout, Object.assign({}, props, { 300 | children: _jsx(_createMdxContent, props) 301 | })) : _createMdxContent(props); 302 | } 303 | export default MDXContent; 304 | ", 305 | "should not support overwriting explicit JSX", 306 | ); 307 | 308 | Ok(()) 309 | } 310 | 311 | #[test] 312 | fn err_esm_invalid() { 313 | assert_eq!( 314 | compile("import 1/1", &Default::default()) 315 | .err() 316 | .unwrap() 317 | .to_string(), 318 | "1:8: Could not parse esm with swc: Expected 'from', got 'numeric literal (1, 1)' (mdxjs-rs:swc)", 319 | "should crash on invalid code in ESM", 320 | ); 321 | } 322 | 323 | #[test] 324 | fn err_expression_broken_multiline_comment_a() { 325 | assert_eq!( 326 | compile("{x/*}", &Default::default()) 327 | .err() 328 | .unwrap() 329 | .to_string(), 330 | "1:6: Could not parse expression with swc: Unterminated block comment (mdxjs-rs:swc)", 331 | "should crash on an unclosed block comment after an expression", 332 | ); 333 | } 334 | 335 | #[test] 336 | fn err_expression_broken_multiline_comment_b() { 337 | assert_eq!( 338 | compile("{/*x}", &Default::default()) 339 | .err() 340 | .unwrap() 341 | .to_string(), 342 | "1:6: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 343 | "should crash on an unclosed block comment in an empty expression", 344 | ); 345 | } 346 | 347 | #[test] 348 | fn err_expression_broken_multiline_comment_c() { 349 | assert!( 350 | compile("{/*a*/}", &Default::default()).is_ok(), 351 | "should support a valid multiline comment", 352 | ); 353 | } 354 | 355 | #[test] 356 | fn err_expression_broken_line_comment_a() { 357 | assert_eq!( 358 | compile("{x//}", &Default::default()).err().unwrap().to_string(), 359 | "1:6: Could not parse expression with swc: Unexpected unclosed line comment, expected line ending: `\\n` (mdxjs-rs:swc)", 360 | "should crash on an unclosed line comment after an expression", 361 | ); 362 | } 363 | 364 | #[test] 365 | fn err_expression_broken_line_comment_b() { 366 | assert_eq!( 367 | compile("{//x}", &Default::default()) 368 | .err() 369 | .unwrap() 370 | .to_string(), 371 | "1:6: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 372 | "should crash on an unclosed line comment in an empty expression", 373 | ); 374 | } 375 | 376 | #[test] 377 | fn err_expression_broken_line_comment_c() { 378 | assert!( 379 | compile("{//a\n}", &Default::default()).is_ok(), 380 | "should support a valid line comment", 381 | ); 382 | } 383 | 384 | #[test] 385 | fn err_esm_stmt() { 386 | assert_eq!( 387 | compile("export let a = 1\nlet b = 2", &Default::default()) 388 | .err() 389 | .unwrap() 390 | .to_string(), 391 | "2:10: Unexpected statement in code: only import/exports are supported (mdxjs-rs:swc)", 392 | "should crash on statements in ESM", 393 | ); 394 | } 395 | 396 | #[test] 397 | fn err_expression_invalid() { 398 | assert_eq!( 399 | compile("{!}", &Default::default()) 400 | .err() 401 | .unwrap() 402 | .to_string(), 403 | "1:4: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 404 | "should crash on invalid code in an expression", 405 | ); 406 | } 407 | 408 | #[test] 409 | fn err_expression_multi() { 410 | assert_eq!( 411 | compile("{x; y}", &Default::default()) 412 | .err() 413 | .unwrap() 414 | .to_string(), 415 | "1:7: Could not parse expression with swc: Unexpected content after expression (mdxjs-rs:swc)", 416 | "should crash on more content after an expression", 417 | ); 418 | } 419 | 420 | #[test] 421 | fn err_expression_empty() { 422 | assert!( 423 | compile("a {} b", &Default::default()).is_ok(), 424 | "should support an empty expression", 425 | ); 426 | } 427 | 428 | #[test] 429 | fn err_expression_comment() { 430 | assert!( 431 | compile("a { /* b */ } c", &Default::default()).is_ok(), 432 | "should support a comment in an empty expression", 433 | ); 434 | } 435 | 436 | #[test] 437 | fn err_expression_value_empty() { 438 | assert_eq!( 439 | compile("
", &Default::default()) 440 | .err() 441 | .unwrap() 442 | .to_string(), 443 | "1:12: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 444 | "should crash on an empty value expression", 445 | ); 446 | } 447 | 448 | #[test] 449 | fn err_expression_value_invalid() { 450 | assert_eq!( 451 | compile("", &Default::default()) 452 | .err() 453 | .unwrap() 454 | .to_string(), 455 | "1:12: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 456 | "should crash on an invalid value expression", 457 | ); 458 | } 459 | 460 | #[test] 461 | fn err_expression_value_comment() { 462 | assert_eq!( 463 | compile("", &Default::default()) 464 | .err() 465 | .unwrap() 466 | .to_string(), 467 | "1:18: Could not parse expression with swc: Unexpected eof (mdxjs-rs:swc)", 468 | "should crash on a value expression with just a comment", 469 | ); 470 | } 471 | 472 | #[test] 473 | fn err_expression_value_extra_comment() { 474 | assert!( 475 | compile("", &Default::default()).is_ok(), 476 | "should support a value expression with a comment", 477 | ); 478 | } 479 | 480 | #[test] 481 | fn err_expression_spread_none() { 482 | assert_eq!( 483 | compile("", &Default::default()).err().unwrap().to_string(), 484 | "1:5: Unexpected prop in spread (such as `{x}`): only a spread is supported (such as `{...x}`) (mdxjs-rs:swc)", 485 | "should crash on a non-spread", 486 | ); 487 | } 488 | 489 | #[test] 490 | fn err_expression_spread_multi_1() { 491 | assert_eq!( 492 | compile("", &Default::default()) 493 | .err() 494 | .unwrap() 495 | .to_string(), 496 | "1:9: Could not parse expression with swc: Expected ',', got ';' (mdxjs-rs:swc)", 497 | "should crash on more content after a (spread) expression (1)", 498 | ); 499 | } 500 | 501 | #[test] 502 | fn err_expression_spread_multi_2() { 503 | assert_eq!( 504 | compile("", &Default::default()).err().unwrap().to_string(), 505 | "1:5: Unexpected extra content in spread (such as `{...x,y}`): only a single spread is supported (such as `{...x}`) (mdxjs-rs:swc)", 506 | "should crash on more content after a (spread) expression (2)", 507 | ); 508 | } 509 | 510 | #[test] 511 | fn err_expression_spread_empty() { 512 | assert_eq!( 513 | compile("", &Default::default()) 514 | .err() 515 | .unwrap() 516 | .to_string(), 517 | "1:12: Could not parse expression with swc: Expression expected (mdxjs-rs:swc)", 518 | "should crash on an empty spread expression", 519 | ); 520 | } 521 | 522 | #[test] 523 | fn err_expression_spread_invalid() { 524 | assert_eq!( 525 | compile("", &Default::default()) 526 | .err() 527 | .unwrap() 528 | .to_string(), 529 | "1:13: Could not parse expression with swc: Expression expected (mdxjs-rs:swc)", 530 | "should crash on an invalid spread expression", 531 | ); 532 | } 533 | 534 | #[test] 535 | fn err_expression_spread_extra_comment() { 536 | assert!( 537 | compile("", &Default::default()).is_ok(), 538 | "should support a spread expression with a comment", 539 | ); 540 | } 541 | --------------------------------------------------------------------------------