├── .editorconfig
├── .github
└── workflows
│ ├── bb.yml
│ └── main.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── index.d.ts
├── index.js
├── lib
└── index.js
├── license
├── package.json
├── readme.md
├── test.js
└── tsconfig.json
/.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 |
--------------------------------------------------------------------------------
/.github/workflows/bb.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | main:
3 | runs-on: ubuntu-latest
4 | steps:
5 | - uses: unifiedjs/beep-boop-beta@main
6 | with:
7 | repo-token: ${{secrets.GITHUB_TOKEN}}
8 | name: bb
9 | on:
10 | issues:
11 | types: [closed, edited, labeled, opened, reopened, unlabeled]
12 | pull_request_target:
13 | types: [closed, edited, labeled, opened, reopened, unlabeled]
14 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | jobs:
2 | main:
3 | name: ${{matrix.node}}
4 | runs-on: ubuntu-latest
5 | steps:
6 | - uses: actions/checkout@v4
7 | - uses: actions/setup-node@v4
8 | with:
9 | node-version: ${{matrix.node}}
10 | - run: npm install
11 | - run: npm test
12 | - uses: codecov/codecov-action@v5
13 | strategy:
14 | matrix:
15 | node:
16 | - lts/hydrogen
17 | - node
18 | name: main
19 | on:
20 | - pull_request
21 | - push
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | node_modules/
3 | *.d.ts.map
4 | *.d.ts
5 | *.log
6 | *.tsbuildinfo
7 | .DS_Store
8 | yarn.lock
9 | !/index.d.ts
10 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-scripts=true
2 | package-lock=false
3 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | coverage/
2 | *.md
3 | *.mdx
4 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | import type {Program} from 'estree-jsx'
2 | import type {Data as HastData, ElementContent, Parent as HastParent} from 'hast'
3 | import type {
4 | BlockContent,
5 | Data as MdastData,
6 | DefinitionContent,
7 | Parent as MdastParent,
8 | PhrasingContent
9 | } from 'mdast'
10 | import type {Data, Node} from 'unist'
11 | import type {Tag} from './lib/index.js'
12 |
13 | // Expose JavaScript API.
14 | export {mdxJsxFromMarkdown, mdxJsxToMarkdown} from './lib/index.js'
15 |
16 | // Expose options.
17 | export type {ToMarkdownOptions} from './lib/index.js'
18 |
19 | // Expose node types.
20 | /**
21 | * MDX JSX attribute value set to an expression.
22 | *
23 | * ```markdown
24 | * > |
25 | * ^^^
26 | * ```
27 | */
28 | export interface MdxJsxAttributeValueExpression extends Node {
29 | /**
30 | * Node type.
31 | */
32 | type: 'mdxJsxAttributeValueExpression'
33 |
34 | /**
35 | * Value.
36 | */
37 | value: string
38 |
39 | /**
40 | * Data associated with the mdast MDX JSX attribute value expression.
41 | */
42 | data?: MdxJsxAttributeValueExpressionData | undefined
43 | }
44 |
45 | /**
46 | * Info associated with mdast MDX JSX attribute value expression nodes by the
47 | * ecosystem.
48 | */
49 | export interface MdxJsxAttributeValueExpressionData extends Data {
50 | /**
51 | * Program node from estree.
52 | */
53 | estree?: Program | null | undefined
54 | }
55 |
56 | /**
57 | * MDX JSX attribute as an expression.
58 | *
59 | * ```markdown
60 | * > |
61 | * ^^^^^^
62 | * ```
63 | */
64 | export interface MdxJsxExpressionAttribute extends Node {
65 | /**
66 | * Node type.
67 | */
68 | type: 'mdxJsxExpressionAttribute'
69 |
70 | /**
71 | * Value.
72 | */
73 | value: string
74 |
75 | /**
76 | * Data associated with the mdast MDX JSX expression attributes.
77 | */
78 | data?: MdxJsxExpressionAttributeData | undefined
79 | }
80 |
81 | /**
82 | * Info associated with mdast MDX JSX expression attribute nodes by the
83 | * ecosystem.
84 | */
85 | export interface MdxJsxExpressionAttributeData extends Data {
86 | /**
87 | * Program node from estree.
88 | */
89 | estree?: Program | null | undefined
90 | }
91 |
92 | /**
93 | * MDX JSX attribute with a key.
94 | *
95 | * ```markdown
96 | * > |
97 | * ^^^^^
98 | * ```
99 | */
100 | export interface MdxJsxAttribute extends Node {
101 | /**
102 | * Node type.
103 | */
104 | type: 'mdxJsxAttribute'
105 | /**
106 | * Attribute name.
107 | */
108 | name: string
109 | /**
110 | * Attribute value.
111 | */
112 | value?: MdxJsxAttributeValueExpression | string | null | undefined
113 | /**
114 | * Data associated with the mdast MDX JSX attribute.
115 | */
116 | data?: MdxJsxAttributeData | undefined
117 | }
118 |
119 | /**
120 | * Info associated with mdast MDX JSX attribute nodes by the
121 | * ecosystem.
122 | */
123 | export interface MdxJsxAttributeData extends Data {}
124 |
125 | /**
126 | * MDX JSX element node, occurring in flow (block).
127 | */
128 | export interface MdxJsxFlowElement extends MdastParent {
129 | /**
130 | * Node type.
131 | */
132 | type: 'mdxJsxFlowElement'
133 | /**
134 | * MDX JSX element name (`null` for fragments).
135 | */
136 | name: string | null
137 | /**
138 | * MDX JSX element attributes.
139 | */
140 | attributes: Array
141 | /**
142 | * Content.
143 | */
144 | children: Array
145 | /**
146 | * Data associated with the mdast MDX JSX elements (flow).
147 | */
148 | data?: MdxJsxFlowElementData | undefined
149 | }
150 |
151 | /**
152 | * Info associated with mdast MDX JSX element (flow) nodes by the
153 | * ecosystem.
154 | */
155 | export interface MdxJsxFlowElementData extends MdastData {}
156 |
157 | /**
158 | * MDX JSX element node, occurring in text (phrasing).
159 | */
160 | export interface MdxJsxTextElement extends MdastParent {
161 | /**
162 | * Node type.
163 | */
164 | type: 'mdxJsxTextElement'
165 | /**
166 | * MDX JSX element name (`null` for fragments).
167 | */
168 | name: string | null
169 | /**
170 | * MDX JSX element attributes.
171 | */
172 | attributes: Array
173 | /**
174 | * Content.
175 | */
176 | children: PhrasingContent[]
177 | /**
178 | * Data associated with the mdast MDX JSX elements (text).
179 | */
180 | data?: MdxJsxTextElementData | undefined
181 | }
182 |
183 | /**
184 | * Info associated with mdast MDX JSX element (text) nodes by the
185 | * ecosystem.
186 | */
187 | export interface MdxJsxTextElementData extends MdastData {}
188 |
189 | /**
190 | * MDX JSX element node, occurring in flow (block), for hast.
191 | */
192 | export interface MdxJsxFlowElementHast extends HastParent {
193 | /**
194 | * Node type.
195 | */
196 | type: 'mdxJsxFlowElement'
197 | /**
198 | * MDX JSX element name (`null` for fragments).
199 | */
200 | name: string | null
201 | /**
202 | * MDX JSX element attributes.
203 | */
204 | attributes: Array
205 | /**
206 | * Content.
207 | */
208 | children: ElementContent[]
209 | /**
210 | * Data associated with the hast MDX JSX elements (flow).
211 | */
212 | data?: MdxJsxFlowElementHastData | undefined
213 | }
214 |
215 | /**
216 | * Info associated with hast MDX JSX element (flow) nodes by the
217 | * ecosystem.
218 | */
219 | export interface MdxJsxFlowElementHastData extends HastData {}
220 |
221 | /**
222 | * MDX JSX element node, occurring in text (phrasing), for hast.
223 | */
224 | export interface MdxJsxTextElementHast extends HastParent {
225 | /**
226 | * Node type.
227 | */
228 | type: 'mdxJsxTextElement'
229 | /**
230 | * MDX JSX element name (`null` for fragments).
231 | */
232 | name: string | null
233 | /**
234 | * MDX JSX element attributes.
235 | */
236 | attributes: Array
237 | /**
238 | * Content.
239 | */
240 | children: ElementContent[]
241 | /**
242 | * Data associated with the hast MDX JSX elements (text).
243 | */
244 | data?: MdxJsxTextElementHastData | undefined
245 | }
246 |
247 | /**
248 | * Info associated with hast MDX JSX element (text) nodes by the
249 | * ecosystem.
250 | */
251 | export interface MdxJsxTextElementHastData extends HastData {}
252 |
253 | // Add nodes to mdast content.
254 | declare module 'mdast' {
255 | interface BlockContentMap {
256 | /**
257 | * MDX JSX element node, occurring in flow (block).
258 | */
259 | mdxJsxFlowElement: MdxJsxFlowElement
260 | }
261 |
262 | interface PhrasingContentMap {
263 | /**
264 | * MDX JSX element node, occurring in text (phrasing).
265 | */
266 | mdxJsxTextElement: MdxJsxTextElement
267 | }
268 |
269 | interface RootContentMap {
270 | /**
271 | * MDX JSX element node, occurring in flow (block).
272 | */
273 | mdxJsxFlowElement: MdxJsxFlowElement
274 | /**
275 | * MDX JSX element node, occurring in text (phrasing).
276 | */
277 | mdxJsxTextElement: MdxJsxTextElement
278 | }
279 | }
280 |
281 | // Add nodes to hast content.
282 | declare module 'hast' {
283 | interface ElementContentMap {
284 | /**
285 | * MDX JSX element node, occurring in text (phrasing).
286 | */
287 | mdxJsxTextElement: MdxJsxTextElementHast
288 | /**
289 | * MDX JSX element node, occurring in flow (block).
290 | */
291 | mdxJsxFlowElement: MdxJsxFlowElementHast
292 | }
293 |
294 | interface RootContentMap {
295 | /**
296 | * MDX JSX element node, occurring in text (phrasing).
297 | */
298 | mdxJsxTextElement: MdxJsxTextElementHast
299 | /**
300 | * MDX JSX element node, occurring in flow (block).
301 | */
302 | mdxJsxFlowElement: MdxJsxFlowElementHast
303 | }
304 | }
305 |
306 | // Add custom data tracked to turn markdown into a tree.
307 | declare module 'mdast-util-from-markdown' {
308 | interface CompileData {
309 | /**
310 | * Current MDX JSX tag.
311 | */
312 | mdxJsxTag?: Tag | undefined
313 |
314 | /**
315 | * Current stack of open MDX JSX tags.
316 | */
317 | mdxJsxTagStack?: Tag[] | undefined
318 | }
319 | }
320 |
321 | // Add custom data tracked to turn a syntax tree into markdown.
322 | declare module 'mdast-util-to-markdown' {
323 | interface ConstructNameMap {
324 | /**
325 | * Whole JSX element, in flow.
326 | *
327 | * ```markdown
328 | * > |
329 | * ^^^^^
330 | * ```
331 | */
332 | mdxJsxFlowElement: 'mdxJsxFlowElement'
333 |
334 | /**
335 | * Whole JSX element, in text.
336 | *
337 | * ```markdown
338 | * > | a .
339 | * ^^^^^
340 | * ```
341 | */
342 | mdxJsxTextElement: 'mdxJsxTextElement'
343 | }
344 | }
345 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | // Note: types exposed from `index.d.ts`.
2 | export {mdxJsxFromMarkdown, mdxJsxToMarkdown} from './lib/index.js'
3 |
--------------------------------------------------------------------------------
/lib/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @import {CompileContext, Extension as FromMarkdownExtension, Handle as FromMarkdownHandle, OnEnterError, OnExitError, Token} from 'mdast-util-from-markdown'
3 | * @import {Handle as ToMarkdownHandle, Options as ToMarkdownExtension, State, Tracker} from 'mdast-util-to-markdown'
4 | * @import {Point} from 'unist'
5 | * @import {MdxJsxAttribute, MdxJsxAttributeValueExpression, MdxJsxExpressionAttribute, MdxJsxFlowElement, MdxJsxTextElement} from '../index.js'
6 | */
7 |
8 | /**
9 | * @typedef Tag
10 | * Single tag.
11 | * @property {string | undefined} name
12 | * Name of tag, or `undefined` for fragment.
13 | *
14 | * > 👉 **Note**: `null` is used in the AST for fragments, as it serializes in
15 | * > JSON.
16 | * @property {Array} attributes
17 | * Attributes.
18 | * @property {boolean} close
19 | * Whether the tag is closing (``).
20 | * @property {boolean} selfClosing
21 | * Whether the tag is self-closing (``).
22 | * @property {Token['start']} start
23 | * Start point.
24 | * @property {Token['start']} end
25 | * End point.
26 | *
27 | * @typedef ToMarkdownOptions
28 | * Configuration.
29 | * @property {'"' | "'" | null | undefined} [quote='"']
30 | * Preferred quote to use around attribute values (default: `'"'`).
31 | * @property {boolean | null | undefined} [quoteSmart=false]
32 | * Use the other quote if that results in less bytes (default: `false`).
33 | * @property {boolean | null | undefined} [tightSelfClosing=false]
34 | * Do not use an extra space when closing self-closing elements: `
`
35 | * instead of `
` (default: `false`).
36 | * @property {number | null | undefined} [printWidth=Infinity]
37 | * Try and wrap syntax at this width (default: `Infinity`).
38 | *
39 | * When set to a finite number (say, `80`), the formatter will print
40 | * attributes on separate lines when a tag doesn’t fit on one line.
41 | * The normal behavior is to print attributes with spaces between them
42 | * instead of line endings.
43 | */
44 |
45 | import {ccount} from 'ccount'
46 | import {ok as assert} from 'devlop'
47 | import {parseEntities} from 'parse-entities'
48 | import {stringifyEntitiesLight} from 'stringify-entities'
49 | import {stringifyPosition} from 'unist-util-stringify-position'
50 | import {VFileMessage} from 'vfile-message'
51 |
52 | const indent = ' '
53 |
54 | /**
55 | * Create an extension for `mdast-util-from-markdown` to enable MDX JSX.
56 | *
57 | * @returns {FromMarkdownExtension}
58 | * Extension for `mdast-util-from-markdown` to enable MDX JSX.
59 | *
60 | * When using the syntax extension with `addResult`, nodes will have a
61 | * `data.estree` field set to an ESTree `Program` node.
62 | */
63 | export function mdxJsxFromMarkdown() {
64 | return {
65 | canContainEols: ['mdxJsxTextElement'],
66 | enter: {
67 | mdxJsxFlowTag: enterMdxJsxTag,
68 | mdxJsxFlowTagClosingMarker: enterMdxJsxTagClosingMarker,
69 | mdxJsxFlowTagAttribute: enterMdxJsxTagAttribute,
70 | mdxJsxFlowTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
71 | mdxJsxFlowTagAttributeValueLiteral: buffer,
72 | mdxJsxFlowTagAttributeValueExpression: buffer,
73 | mdxJsxFlowTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker,
74 |
75 | mdxJsxTextTag: enterMdxJsxTag,
76 | mdxJsxTextTagClosingMarker: enterMdxJsxTagClosingMarker,
77 | mdxJsxTextTagAttribute: enterMdxJsxTagAttribute,
78 | mdxJsxTextTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
79 | mdxJsxTextTagAttributeValueLiteral: buffer,
80 | mdxJsxTextTagAttributeValueExpression: buffer,
81 | mdxJsxTextTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker
82 | },
83 | exit: {
84 | mdxJsxFlowTagClosingMarker: exitMdxJsxTagClosingMarker,
85 | mdxJsxFlowTagNamePrimary: exitMdxJsxTagNamePrimary,
86 | mdxJsxFlowTagNameMember: exitMdxJsxTagNameMember,
87 | mdxJsxFlowTagNameLocal: exitMdxJsxTagNameLocal,
88 | mdxJsxFlowTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
89 | mdxJsxFlowTagExpressionAttributeValue: data,
90 | mdxJsxFlowTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
91 | mdxJsxFlowTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
92 | mdxJsxFlowTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
93 | mdxJsxFlowTagAttributeValueLiteralValue: data,
94 | mdxJsxFlowTagAttributeValueExpression:
95 | exitMdxJsxTagAttributeValueExpression,
96 | mdxJsxFlowTagAttributeValueExpressionValue: data,
97 | mdxJsxFlowTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
98 | mdxJsxFlowTag: exitMdxJsxTag,
99 |
100 | mdxJsxTextTagClosingMarker: exitMdxJsxTagClosingMarker,
101 | mdxJsxTextTagNamePrimary: exitMdxJsxTagNamePrimary,
102 | mdxJsxTextTagNameMember: exitMdxJsxTagNameMember,
103 | mdxJsxTextTagNameLocal: exitMdxJsxTagNameLocal,
104 | mdxJsxTextTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
105 | mdxJsxTextTagExpressionAttributeValue: data,
106 | mdxJsxTextTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
107 | mdxJsxTextTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
108 | mdxJsxTextTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
109 | mdxJsxTextTagAttributeValueLiteralValue: data,
110 | mdxJsxTextTagAttributeValueExpression:
111 | exitMdxJsxTagAttributeValueExpression,
112 | mdxJsxTextTagAttributeValueExpressionValue: data,
113 | mdxJsxTextTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
114 | mdxJsxTextTag: exitMdxJsxTag
115 | }
116 | }
117 |
118 | /**
119 | * @this {CompileContext}
120 | * @type {FromMarkdownHandle}
121 | */
122 | function buffer() {
123 | this.buffer()
124 | }
125 |
126 | /**
127 | * Copy a point-like value.
128 | *
129 | * @param {Point} d
130 | * Point-like value.
131 | * @returns {Point}
132 | * unist point.
133 | */
134 | function point(d) {
135 | return {line: d.line, column: d.column, offset: d.offset}
136 | }
137 |
138 | /**
139 | * @this {CompileContext}
140 | * @type {FromMarkdownHandle}
141 | */
142 | function data(token) {
143 | this.config.enter.data.call(this, token)
144 | this.config.exit.data.call(this, token)
145 | }
146 |
147 | /**
148 | * @this {CompileContext}
149 | * @type {FromMarkdownHandle}
150 | */
151 | function enterMdxJsxTag(token) {
152 | /** @type {Tag} */
153 | const tag = {
154 | name: undefined,
155 | attributes: [],
156 | close: false,
157 | selfClosing: false,
158 | start: token.start,
159 | end: token.end
160 | }
161 | if (!this.data.mdxJsxTagStack) this.data.mdxJsxTagStack = []
162 | this.data.mdxJsxTag = tag
163 | this.buffer()
164 | }
165 |
166 | /**
167 | * @this {CompileContext}
168 | * @type {FromMarkdownHandle}
169 | */
170 | function enterMdxJsxTagClosingMarker(token) {
171 | const stack = this.data.mdxJsxTagStack
172 | assert(stack, 'expected `mdxJsxTagStack`')
173 |
174 | if (stack.length === 0) {
175 | throw new VFileMessage(
176 | 'Unexpected closing slash `/` in tag, expected an open tag first',
177 | {start: token.start, end: token.end},
178 | 'mdast-util-mdx-jsx:unexpected-closing-slash'
179 | )
180 | }
181 | }
182 |
183 | /**
184 | * @this {CompileContext}
185 | * @type {FromMarkdownHandle}
186 | */
187 | function enterMdxJsxTagAnyAttribute(token) {
188 | const tag = this.data.mdxJsxTag
189 | assert(tag, 'expected `mdxJsxTag`')
190 |
191 | if (tag.close) {
192 | throw new VFileMessage(
193 | 'Unexpected attribute in closing tag, expected the end of the tag',
194 | {start: token.start, end: token.end},
195 | 'mdast-util-mdx-jsx:unexpected-attribute'
196 | )
197 | }
198 | }
199 |
200 | /**
201 | * @this {CompileContext}
202 | * @type {FromMarkdownHandle}
203 | */
204 | function enterMdxJsxTagSelfClosingMarker(token) {
205 | const tag = this.data.mdxJsxTag
206 | assert(tag, 'expected `mdxJsxTag`')
207 |
208 | if (tag.close) {
209 | throw new VFileMessage(
210 | 'Unexpected self-closing slash `/` in closing tag, expected the end of the tag',
211 | {start: token.start, end: token.end},
212 | 'mdast-util-mdx-jsx:unexpected-self-closing-slash'
213 | )
214 | }
215 | }
216 |
217 | /**
218 | * @this {CompileContext}
219 | * @type {FromMarkdownHandle}
220 | */
221 | function exitMdxJsxTagClosingMarker() {
222 | const tag = this.data.mdxJsxTag
223 | assert(tag, 'expected `mdxJsxTag`')
224 | tag.close = true
225 | }
226 |
227 | /**
228 | * @this {CompileContext}
229 | * @type {FromMarkdownHandle}
230 | */
231 | function exitMdxJsxTagNamePrimary(token) {
232 | const tag = this.data.mdxJsxTag
233 | assert(tag, 'expected `mdxJsxTag`')
234 | tag.name = this.sliceSerialize(token)
235 | }
236 |
237 | /**
238 | * @this {CompileContext}
239 | * @type {FromMarkdownHandle}
240 | */
241 | function exitMdxJsxTagNameMember(token) {
242 | const tag = this.data.mdxJsxTag
243 | assert(tag, 'expected `mdxJsxTag`')
244 | tag.name += '.' + this.sliceSerialize(token)
245 | }
246 |
247 | /**
248 | * @this {CompileContext}
249 | * @type {FromMarkdownHandle}
250 | */
251 | function exitMdxJsxTagNameLocal(token) {
252 | const tag = this.data.mdxJsxTag
253 | assert(tag, 'expected `mdxJsxTag`')
254 | tag.name += ':' + this.sliceSerialize(token)
255 | }
256 |
257 | /**
258 | * @this {CompileContext}
259 | * @type {FromMarkdownHandle}
260 | */
261 | function enterMdxJsxTagAttribute(token) {
262 | const tag = this.data.mdxJsxTag
263 | assert(tag, 'expected `mdxJsxTag`')
264 | enterMdxJsxTagAnyAttribute.call(this, token)
265 | tag.attributes.push({
266 | type: 'mdxJsxAttribute',
267 | name: '',
268 | value: null,
269 | position: {
270 | start: point(token.start),
271 | // @ts-expect-error: `end` will be patched later.
272 | end: undefined
273 | }
274 | })
275 | }
276 |
277 | /**
278 | * @this {CompileContext}
279 | * @type {FromMarkdownHandle}
280 | */
281 | function enterMdxJsxTagExpressionAttribute(token) {
282 | const tag = this.data.mdxJsxTag
283 | assert(tag, 'expected `mdxJsxTag`')
284 | enterMdxJsxTagAnyAttribute.call(this, token)
285 | tag.attributes.push({
286 | type: 'mdxJsxExpressionAttribute',
287 | value: '',
288 | position: {
289 | start: point(token.start),
290 | // @ts-expect-error: `end` will be patched later.
291 | end: undefined
292 | }
293 | })
294 | this.buffer()
295 | }
296 |
297 | /**
298 | * @this {CompileContext}
299 | * @type {FromMarkdownHandle}
300 | */
301 | function exitMdxJsxTagExpressionAttribute(token) {
302 | const tag = this.data.mdxJsxTag
303 | assert(tag, 'expected `mdxJsxTag`')
304 | const tail = tag.attributes[tag.attributes.length - 1]
305 | assert(tail.type === 'mdxJsxExpressionAttribute')
306 | const estree = token.estree
307 |
308 | tail.value = this.resume()
309 | assert(tail.position !== undefined)
310 | tail.position.end = point(token.end)
311 |
312 | if (estree) {
313 | tail.data = {estree}
314 | }
315 | }
316 |
317 | /**
318 | * @this {CompileContext}
319 | * @type {FromMarkdownHandle}
320 | */
321 | function exitMdxJsxTagAttributeNamePrimary(token) {
322 | const tag = this.data.mdxJsxTag
323 | assert(tag, 'expected `mdxJsxTag`')
324 | const node = tag.attributes[tag.attributes.length - 1]
325 | assert(node.type === 'mdxJsxAttribute')
326 | node.name = this.sliceSerialize(token)
327 | assert(node.position !== undefined)
328 | node.position.end = point(token.end)
329 | }
330 |
331 | /**
332 | * @this {CompileContext}
333 | * @type {FromMarkdownHandle}
334 | */
335 | function exitMdxJsxTagAttributeNameLocal(token) {
336 | const tag = this.data.mdxJsxTag
337 | assert(tag, 'expected `mdxJsxTag`')
338 | const node = tag.attributes[tag.attributes.length - 1]
339 | assert(node.type === 'mdxJsxAttribute')
340 | node.name += ':' + this.sliceSerialize(token)
341 | assert(node.position !== undefined)
342 | node.position.end = point(token.end)
343 | }
344 |
345 | /**
346 | * @this {CompileContext}
347 | * @type {FromMarkdownHandle}
348 | */
349 | function exitMdxJsxTagAttributeValueLiteral(token) {
350 | const tag = this.data.mdxJsxTag
351 | assert(tag, 'expected `mdxJsxTag`')
352 | const node = tag.attributes[tag.attributes.length - 1]
353 | node.value = parseEntities(this.resume(), {nonTerminated: false})
354 | assert(node.position !== undefined)
355 | node.position.end = point(token.end)
356 | }
357 |
358 | /**
359 | * @this {CompileContext}
360 | * @type {FromMarkdownHandle}
361 | */
362 | function exitMdxJsxTagAttributeValueExpression(token) {
363 | const tag = this.data.mdxJsxTag
364 | assert(tag, 'expected `mdxJsxTag`')
365 | const tail = tag.attributes[tag.attributes.length - 1]
366 | assert(tail.type === 'mdxJsxAttribute')
367 | /** @type {MdxJsxAttributeValueExpression} */
368 | const node = {type: 'mdxJsxAttributeValueExpression', value: this.resume()}
369 | const estree = token.estree
370 |
371 | if (estree) {
372 | node.data = {estree}
373 | }
374 |
375 | tail.value = node
376 | assert(tail.position !== undefined)
377 | tail.position.end = point(token.end)
378 | }
379 |
380 | /**
381 | * @this {CompileContext}
382 | * @type {FromMarkdownHandle}
383 | */
384 | function exitMdxJsxTagSelfClosingMarker() {
385 | const tag = this.data.mdxJsxTag
386 | assert(tag, 'expected `mdxJsxTag`')
387 |
388 | tag.selfClosing = true
389 | }
390 |
391 | /**
392 | * @this {CompileContext}
393 | * @type {FromMarkdownHandle}
394 | */
395 | function exitMdxJsxTag(token) {
396 | const tag = this.data.mdxJsxTag
397 | assert(tag, 'expected `mdxJsxTag`')
398 | const stack = this.data.mdxJsxTagStack
399 | assert(stack, 'expected `mdxJsxTagStack`')
400 | const tail = stack[stack.length - 1]
401 |
402 | if (tag.close && tail.name !== tag.name) {
403 | throw new VFileMessage(
404 | 'Unexpected closing tag `' +
405 | serializeAbbreviatedTag(tag) +
406 | '`, expected corresponding closing tag for `' +
407 | serializeAbbreviatedTag(tail) +
408 | '` (' +
409 | stringifyPosition(tail) +
410 | ')',
411 | {start: token.start, end: token.end},
412 | 'mdast-util-mdx-jsx:end-tag-mismatch'
413 | )
414 | }
415 |
416 | // End of a tag, so drop the buffer.
417 | this.resume()
418 |
419 | if (tag.close) {
420 | stack.pop()
421 | } else {
422 | this.enter(
423 | {
424 | type:
425 | token.type === 'mdxJsxTextTag'
426 | ? 'mdxJsxTextElement'
427 | : 'mdxJsxFlowElement',
428 | name: tag.name || null,
429 | attributes: tag.attributes,
430 | children: []
431 | },
432 | token,
433 | onErrorRightIsTag
434 | )
435 | }
436 |
437 | if (tag.selfClosing || tag.close) {
438 | this.exit(token, onErrorLeftIsTag)
439 | } else {
440 | stack.push(tag)
441 | }
442 | }
443 |
444 | /**
445 | * @this {CompileContext}
446 | * @type {OnEnterError}
447 | */
448 | function onErrorRightIsTag(closing, open) {
449 | const stack = this.data.mdxJsxTagStack
450 | assert(stack, 'expected `mdxJsxTagStack`')
451 | const tag = stack[stack.length - 1]
452 | assert(tag, 'expected `mdxJsxTag`')
453 | const place = closing ? ' before the end of `' + closing.type + '`' : ''
454 | const position = closing
455 | ? {start: closing.start, end: closing.end}
456 | : undefined
457 |
458 | throw new VFileMessage(
459 | 'Expected a closing tag for `' +
460 | serializeAbbreviatedTag(tag) +
461 | '` (' +
462 | stringifyPosition({start: open.start, end: open.end}) +
463 | ')' +
464 | place,
465 | position,
466 | 'mdast-util-mdx-jsx:end-tag-mismatch'
467 | )
468 | }
469 |
470 | /**
471 | * @this {CompileContext}
472 | * @type {OnExitError}
473 | */
474 | function onErrorLeftIsTag(a, b) {
475 | const tag = this.data.mdxJsxTag
476 | assert(tag, 'expected `mdxJsxTag`')
477 |
478 | throw new VFileMessage(
479 | 'Expected the closing tag `' +
480 | serializeAbbreviatedTag(tag) +
481 | '` either after the end of `' +
482 | b.type +
483 | '` (' +
484 | stringifyPosition(b.end) +
485 | ') or another opening tag after the start of `' +
486 | b.type +
487 | '` (' +
488 | stringifyPosition(b.start) +
489 | ')',
490 | {start: a.start, end: a.end},
491 | 'mdast-util-mdx-jsx:end-tag-mismatch'
492 | )
493 | }
494 |
495 | /**
496 | * Serialize a tag, excluding attributes.
497 | * `self-closing` is not supported, because we don’t need it yet.
498 | *
499 | * @param {Tag} tag
500 | * @returns {string}
501 | */
502 | function serializeAbbreviatedTag(tag) {
503 | return '<' + (tag.close ? '/' : '') + (tag.name || '') + '>'
504 | }
505 | }
506 |
507 | /**
508 | * Create an extension for `mdast-util-to-markdown` to enable MDX JSX.
509 | *
510 | * This extension configures `mdast-util-to-markdown` with
511 | * `options.fences: true` and `options.resourceLink: true` too, do not
512 | * overwrite them!
513 | *
514 | * @param {ToMarkdownOptions | null | undefined} [options]
515 | * Configuration (optional).
516 | * @returns {ToMarkdownExtension}
517 | * Extension for `mdast-util-to-markdown` to enable MDX JSX.
518 | */
519 | export function mdxJsxToMarkdown(options) {
520 | const options_ = options || {}
521 | const quote = options_.quote || '"'
522 | const quoteSmart = options_.quoteSmart || false
523 | const tightSelfClosing = options_.tightSelfClosing || false
524 | const printWidth = options_.printWidth || Number.POSITIVE_INFINITY
525 | const alternative = quote === '"' ? "'" : '"'
526 |
527 | if (quote !== '"' && quote !== "'") {
528 | throw new Error(
529 | 'Cannot serialize attribute values with `' +
530 | quote +
531 | '` for `options.quote`, expected `"`, or `\'`'
532 | )
533 | }
534 |
535 | mdxElement.peek = peekElement
536 |
537 | return {
538 | handlers: {
539 | mdxJsxFlowElement: mdxElement,
540 | mdxJsxTextElement: mdxElement
541 | },
542 | unsafe: [
543 | {character: '<', inConstruct: ['phrasing']},
544 | {atBreak: true, character: '<'}
545 | ],
546 | // Always generate fenced code (never indented code).
547 | fences: true,
548 | // Always generate links with resources (never autolinks).
549 | resourceLink: true
550 | }
551 |
552 | /**
553 | * @type {ToMarkdownHandle}
554 | * @param {MdxJsxFlowElement | MdxJsxTextElement} node
555 | */
556 | // eslint-disable-next-line complexity
557 | function mdxElement(node, _, state, info) {
558 | const flow = node.type === 'mdxJsxFlowElement'
559 | const selfClosing = node.name
560 | ? !node.children || node.children.length === 0
561 | : false
562 | const depth = inferDepth(state)
563 | const currentIndent = createIndent(depth)
564 | const trackerOneLine = state.createTracker(info)
565 | const trackerMultiLine = state.createTracker(info)
566 | /** @type {Array} */
567 | const serializedAttributes = []
568 | const prefix = (flow ? currentIndent : '') + '<' + (node.name || '')
569 | const exit = state.enter(node.type)
570 |
571 | trackerOneLine.move(prefix)
572 | trackerMultiLine.move(prefix)
573 |
574 | // None.
575 | if (node.attributes && node.attributes.length > 0) {
576 | if (!node.name) {
577 | throw new Error('Cannot serialize fragment w/ attributes')
578 | }
579 |
580 | let index = -1
581 | while (++index < node.attributes.length) {
582 | const attribute = node.attributes[index]
583 | /** @type {string} */
584 | let result
585 |
586 | if (attribute.type === 'mdxJsxExpressionAttribute') {
587 | result = '{' + (attribute.value || '') + '}'
588 | } else {
589 | if (!attribute.name) {
590 | throw new Error('Cannot serialize attribute w/o name')
591 | }
592 |
593 | const value = attribute.value
594 | const left = attribute.name
595 | /** @type {string} */
596 | let right = ''
597 |
598 | if (value === null || value === undefined) {
599 | // Empty.
600 | } else if (typeof value === 'object') {
601 | right = '{' + (value.value || '') + '}'
602 | } else {
603 | // If the alternative is less common than `quote`, switch.
604 | const appliedQuote =
605 | quoteSmart && ccount(value, quote) > ccount(value, alternative)
606 | ? alternative
607 | : quote
608 | right =
609 | appliedQuote +
610 | stringifyEntitiesLight(value, {subset: [appliedQuote]}) +
611 | appliedQuote
612 | }
613 |
614 | result = left + (right ? '=' : '') + right
615 | }
616 |
617 | serializedAttributes.push(result)
618 | }
619 | }
620 |
621 | let attributesOnTheirOwnLine = false
622 | const attributesOnOneLine = serializedAttributes.join(' ')
623 |
624 | if (
625 | // Block:
626 | flow &&
627 | // Including a line ending (expressions).
628 | (/\r?\n|\r/.test(attributesOnOneLine) ||
629 | // Current position (including ``.
635 | (selfClosing ? (tightSelfClosing ? 2 : 3) : 1) >
636 | printWidth)
637 | ) {
638 | attributesOnTheirOwnLine = true
639 | }
640 |
641 | let tracker = trackerOneLine
642 | let value = prefix
643 |
644 | if (attributesOnTheirOwnLine) {
645 | tracker = trackerMultiLine
646 |
647 | let index = -1
648 |
649 | while (++index < serializedAttributes.length) {
650 | // Only indent first line of of attributes, we can’t indent attribute
651 | // values.
652 | serializedAttributes[index] =
653 | currentIndent + indent + serializedAttributes[index]
654 | }
655 |
656 | value += tracker.move(
657 | '\n' + serializedAttributes.join('\n') + '\n' + currentIndent
658 | )
659 | } else if (attributesOnOneLine) {
660 | value += tracker.move(' ' + attributesOnOneLine)
661 | }
662 |
663 | if (selfClosing) {
664 | value += tracker.move(
665 | (tightSelfClosing || attributesOnTheirOwnLine ? '' : ' ') + '/'
666 | )
667 | }
668 |
669 | value += tracker.move('>')
670 |
671 | if (node.children && node.children.length > 0) {
672 | if (node.type === 'mdxJsxTextElement') {
673 | value += tracker.move(
674 | state.containerPhrasing(node, {
675 | ...tracker.current(),
676 | before: '>',
677 | after: '<'
678 | })
679 | )
680 | } else {
681 | tracker.shift(2)
682 | value += tracker.move('\n')
683 | value += tracker.move(containerFlow(node, state, tracker.current()))
684 | value += tracker.move('\n')
685 | }
686 | }
687 |
688 | if (!selfClosing) {
689 | value += tracker.move(
690 | (flow ? currentIndent : '') + '' + (node.name || '') + '>'
691 | )
692 | }
693 |
694 | exit()
695 | return value
696 | }
697 | }
698 |
699 | // Modified copy of:
700 | // .
701 | //
702 | // To do: add `indent` support to `mdast-util-to-markdown`.
703 | // As indents are only used for JSX, it’s fine for now, but perhaps better
704 | // there.
705 | /**
706 | * @param {MdxJsxFlowElement} parent
707 | * Parent of flow nodes.
708 | * @param {State} state
709 | * Info passed around about the current state.
710 | * @param {ReturnType} info
711 | * Info on where we are in the document we are generating.
712 | * @returns {string}
713 | * Serialized children, joined by (blank) lines.
714 | */
715 | function containerFlow(parent, state, info) {
716 | const indexStack = state.indexStack
717 | const children = parent.children
718 | const tracker = state.createTracker(info)
719 | const currentIndent = createIndent(inferDepth(state))
720 | /** @type {Array} */
721 | const results = []
722 | let index = -1
723 |
724 | indexStack.push(-1)
725 |
726 | while (++index < children.length) {
727 | const child = children[index]
728 |
729 | indexStack[indexStack.length - 1] = index
730 |
731 | const childInfo = {before: '\n', after: '\n', ...tracker.current()}
732 |
733 | const result = state.handle(child, parent, state, childInfo)
734 |
735 | const serializedChild =
736 | child.type === 'mdxJsxFlowElement'
737 | ? result
738 | : state.indentLines(result, function (line, _, blank) {
739 | return (blank ? '' : currentIndent) + line
740 | })
741 |
742 | results.push(tracker.move(serializedChild))
743 |
744 | if (child.type !== 'list') {
745 | state.bulletLastUsed = undefined
746 | }
747 |
748 | if (index < children.length - 1) {
749 | results.push(tracker.move('\n\n'))
750 | }
751 | }
752 |
753 | indexStack.pop()
754 |
755 | return results.join('')
756 | }
757 |
758 | /**
759 | * @param {State} state
760 | * @returns {number}
761 | */
762 | function inferDepth(state) {
763 | let depth = 0
764 | let index = state.stack.length
765 |
766 | while (--index > -1) {
767 | const name = state.stack[index]
768 |
769 | if (name === 'blockquote' || name === 'listItem') break
770 | if (name === 'mdxJsxFlowElement') depth++
771 | }
772 |
773 | return depth
774 | }
775 |
776 | /**
777 | * @param {number} depth
778 | * @returns {string}
779 | */
780 | function createIndent(depth) {
781 | return indent.repeat(depth)
782 | }
783 |
784 | /**
785 | * @type {ToMarkdownHandle}
786 | */
787 | function peekElement() {
788 | return '<'
789 | }
790 |
--------------------------------------------------------------------------------
/license:
--------------------------------------------------------------------------------
1 | (The MIT License)
2 |
3 | Copyright (c) 2020 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 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mdast-util-mdx-jsx",
3 | "version": "3.2.0",
4 | "description": "mdast extension to parse and serialize MDX or MDX.js JSX",
5 | "license": "MIT",
6 | "keywords": [
7 | "unist",
8 | "mdast",
9 | "mdast-util",
10 | "util",
11 | "utility",
12 | "markdown",
13 | "markup",
14 | "mdx",
15 | "mdxjs",
16 | "jsx",
17 | "extension"
18 | ],
19 | "repository": "syntax-tree/mdast-util-mdx-jsx",
20 | "bugs": "https://github.com/syntax-tree/mdast-util-mdx-jsx/issues",
21 | "funding": {
22 | "type": "opencollective",
23 | "url": "https://opencollective.com/unified"
24 | },
25 | "author": "Titus Wormer (https://wooorm.com)",
26 | "contributors": [
27 | "Titus Wormer (https://wooorm.com)"
28 | ],
29 | "sideEffects": false,
30 | "type": "module",
31 | "exports": "./index.js",
32 | "files": [
33 | "lib/",
34 | "index.d.ts.map",
35 | "index.d.ts",
36 | "index.js"
37 | ],
38 | "dependencies": {
39 | "@types/estree-jsx": "^1.0.0",
40 | "@types/hast": "^3.0.0",
41 | "@types/mdast": "^4.0.0",
42 | "@types/unist": "^3.0.0",
43 | "ccount": "^2.0.0",
44 | "devlop": "^1.1.0",
45 | "mdast-util-from-markdown": "^2.0.0",
46 | "mdast-util-to-markdown": "^2.0.0",
47 | "parse-entities": "^4.0.0",
48 | "stringify-entities": "^4.0.0",
49 | "unist-util-stringify-position": "^4.0.0",
50 | "vfile-message": "^4.0.0"
51 | },
52 | "devDependencies": {
53 | "@types/node": "^22.0.0",
54 | "acorn": "^8.0.0",
55 | "c8": "^10.0.0",
56 | "micromark-extension-mdx-jsx": "^3.0.0",
57 | "micromark-extension-mdx-md": "^2.0.0",
58 | "prettier": "^3.0.0",
59 | "remark-cli": "^12.0.0",
60 | "remark-preset-wooorm": "^10.0.0",
61 | "type-coverage": "^2.0.0",
62 | "typescript": "^5.0.0",
63 | "unist-util-remove-position": "^5.0.0",
64 | "xo": "^0.60.0"
65 | },
66 | "scripts": {
67 | "prepack": "npm run build && npm run format",
68 | "build": "tsc --build --clean && tsc --build && type-coverage",
69 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix",
70 | "test-api-prod": "node --conditions production test.js",
71 | "test-api-dev": "node --conditions development test.js",
72 | "test-api": "npm run test-api-dev && npm run test-api-prod",
73 | "test-coverage": "c8 --100 --reporter lcov npm run test-api",
74 | "test": "npm run build && npm run format && npm run test-coverage"
75 | },
76 | "prettier": {
77 | "bracketSpacing": false,
78 | "semi": false,
79 | "singleQuote": true,
80 | "tabWidth": 2,
81 | "trailingComma": "none",
82 | "useTabs": false
83 | },
84 | "remarkConfig": {
85 | "plugins": [
86 | "remark-preset-wooorm"
87 | ]
88 | },
89 | "typeCoverage": {
90 | "atLeast": 100,
91 | "detail": true,
92 | "ignoreCatch": true,
93 | "strict": true
94 | },
95 | "xo": {
96 | "overrides": [
97 | {
98 | "files": [
99 | "**/*.ts"
100 | ],
101 | "rules": {
102 | "@typescript-eslint/ban-types": "off",
103 | "@typescript-eslint/consistent-type-definitions": "off"
104 | }
105 | }
106 | ],
107 | "prettier": true,
108 | "rules": {
109 | "logical-assignment-operators": "off",
110 | "unicorn/prefer-at": "off"
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # mdast-util-mdx-jsx
2 |
3 | [![Build][build-badge]][build]
4 | [![Coverage][coverage-badge]][coverage]
5 | [![Downloads][downloads-badge]][downloads]
6 | [![Size][size-badge]][size]
7 | [![Sponsors][sponsors-badge]][collective]
8 | [![Backers][backers-badge]][collective]
9 | [![Chat][chat-badge]][chat]
10 |
11 | [mdast][] extensions to parse and serialize [MDX][] JSX (``).
12 |
13 | ## Contents
14 |
15 | * [What is this?](#what-is-this)
16 | * [When to use this](#when-to-use-this)
17 | * [Install](#install)
18 | * [Use](#use)
19 | * [API](#api)
20 | * [`mdxJsxFromMarkdown()`](#mdxjsxfrommarkdown)
21 | * [`mdxJsxToMarkdown(options?)`](#mdxjsxtomarkdownoptions)
22 | * [`MdxJsxAttribute`](#mdxjsxattribute)
23 | * [`MdxJsxAttributeValueExpression`](#mdxjsxattributevalueexpression)
24 | * [`MdxJsxExpressionAttribute`](#mdxjsxexpressionattribute)
25 | * [`MdxJsxFlowElement`](#mdxjsxflowelement)
26 | * [`MdxJsxFlowElementHast`](#mdxjsxflowelementhast)
27 | * [`MdxJsxTextElement`](#mdxjsxtextelement)
28 | * [`MdxJsxTextElementHast`](#mdxjsxtextelementhast)
29 | * [`ToMarkdownOptions`](#tomarkdownoptions)
30 | * [HTML](#html)
31 | * [Syntax](#syntax)
32 | * [Syntax tree](#syntax-tree)
33 | * [Nodes](#nodes)
34 | * [Mixin](#mixin)
35 | * [Content model](#content-model)
36 | * [Types](#types)
37 | * [Compatibility](#compatibility)
38 | * [Related](#related)
39 | * [Contribute](#contribute)
40 | * [License](#license)
41 |
42 | ## What is this?
43 |
44 | This package contains two extensions that add support for MDX JSX syntax in
45 | markdown to [mdast][].
46 | These extensions plug into
47 | [`mdast-util-from-markdown`][mdast-util-from-markdown] (to support parsing
48 | JSX in markdown into a syntax tree) and
49 | [`mdast-util-to-markdown`][mdast-util-to-markdown] (to support serializing
50 | JSX in syntax trees to markdown).
51 |
52 | [JSX][] is an XML-like syntax extension to ECMAScript (JavaScript), which MDX
53 | brings to markdown.
54 | For more info on MDX, see [What is MDX?][what-is-mdx]
55 |
56 | ## When to use this
57 |
58 | You can use these extensions when you are working with
59 | `mdast-util-from-markdown` and `mdast-util-to-markdown` already.
60 |
61 | When working with `mdast-util-from-markdown`, you must combine this package
62 | with [`micromark-extension-mdx-jsx`][micromark-extension-mdx-jsx].
63 |
64 | When you are working with syntax trees and want all of MDX, use
65 | [`mdast-util-mdx`][mdast-util-mdx] instead.
66 |
67 | All these packages are used in [`remark-mdx`][remark-mdx], which
68 | focusses on making it easier to transform content by abstracting these
69 | internals away.
70 |
71 | ## Install
72 |
73 | This package is [ESM only][esm].
74 | In Node.js (version 16+), install with [npm][]:
75 |
76 | ```sh
77 | npm install mdast-util-mdx-jsx
78 | ```
79 |
80 | In Deno with [`esm.sh`][esmsh]:
81 |
82 | ```js
83 | import {mdxJsxFromMarkdown, mdxJsxToMarkdown} from 'https://esm.sh/mdast-util-mdx-jsx@3'
84 | ```
85 |
86 | In browsers with [`esm.sh`][esmsh]:
87 |
88 | ```html
89 |
92 | ```
93 |
94 | ## Use
95 |
96 | Say our document `example.mdx` contains:
97 |
98 | ```mdx
99 |
100 | - a list
101 |
102 |
103 |
104 |
105 | HTML is a lovely language.
106 | ```
107 |
108 | …and our module `example.js` looks as follows:
109 |
110 | ```js
111 | import fs from 'node:fs/promises'
112 | import * as acorn from 'acorn'
113 | import {mdxJsx} from 'micromark-extension-mdx-jsx'
114 | import {fromMarkdown} from 'mdast-util-from-markdown'
115 | import {mdxJsxFromMarkdown, mdxJsxToMarkdown} from 'mdast-util-mdx-jsx'
116 | import {toMarkdown} from 'mdast-util-to-markdown'
117 |
118 | const doc = await fs.readFile('example.mdx')
119 |
120 | const tree = fromMarkdown(doc, {
121 | extensions: [mdxJsx({acorn, addResult: true})],
122 | mdastExtensions: [mdxJsxFromMarkdown()]
123 | })
124 |
125 | console.log(tree)
126 |
127 | const out = toMarkdown(tree, {extensions: [mdxJsxToMarkdown()]})
128 |
129 | console.log(out)
130 | ```
131 |
132 | …now running `node example.js` yields (positional info removed for brevity):
133 |
134 | ```js
135 | {
136 | type: 'root',
137 | children: [
138 | {
139 | type: 'mdxJsxFlowElement',
140 | name: 'Box',
141 | attributes: [],
142 | children: [
143 | {
144 | type: 'list',
145 | ordered: false,
146 | start: null,
147 | spread: false,
148 | children: [
149 | {
150 | type: 'listItem',
151 | spread: false,
152 | checked: null,
153 | children: [
154 | {type: 'paragraph', children: [{type: 'text', value: 'a list'}]}
155 | ]
156 | }
157 | ]
158 | }
159 | ]
160 | },
161 | {
162 | type: 'mdxJsxFlowElement',
163 | name: 'MyComponent',
164 | attributes: [
165 | {
166 | type: 'mdxJsxExpressionAttribute',
167 | value: '...props',
168 | data: {
169 | estree: {
170 | type: 'Program',
171 | body: [
172 | {
173 | type: 'ExpressionStatement',
174 | expression: {
175 | type: 'ObjectExpression',
176 | properties: [
177 | {
178 | type: 'SpreadElement',
179 | argument: {type: 'Identifier', name: 'props'}
180 | }
181 | ]
182 | }
183 | }
184 | ],
185 | sourceType: 'module'
186 | }
187 | }
188 | }
189 | ],
190 | children: []
191 | },
192 | {
193 | type: 'paragraph',
194 | children: [
195 | {
196 | type: 'mdxJsxTextElement',
197 | name: 'abbr',
198 | attributes: [
199 | {
200 | type: 'mdxJsxAttribute',
201 | name: 'title',
202 | value: 'Hypertext Markup Language'
203 | }
204 | ],
205 | children: [{type: 'text', value: 'HTML'}]
206 | },
207 | {type: 'text', value: ' is a lovely language.'}
208 | ]
209 | }
210 | ]
211 | }
212 | ```
213 |
214 | ```markdown
215 |
216 | * a list
217 |
218 |
219 |
220 |
221 | HTML is a lovely language.
222 | ```
223 |
224 | ## API
225 |
226 | This package exports the identifiers
227 | [`mdxJsxFromMarkdown`][api-mdx-jsx-from-markdown] and
228 | [`mdxJsxToMarkdown`][api-mdx-jsx-to-markdown].
229 | There is no default export.
230 |
231 | ### `mdxJsxFromMarkdown()`
232 |
233 | Create an extension for
234 | [`mdast-util-from-markdown`][mdast-util-from-markdown]
235 | to enable MDX JSX.
236 |
237 | ###### Returns
238 |
239 | Extension for `mdast-util-from-markdown` to enable MDX JSX
240 | ([`FromMarkdownExtension`][from-markdown-extension]).
241 |
242 | When using the [micromark syntax extension][micromark-extension-mdx-jsx] with
243 | `addResult`, nodes will have a `data.estree` field set to an ESTree
244 | [`Program`][program] node.
245 |
246 | ### `mdxJsxToMarkdown(options?)`
247 |
248 | Create an extension for
249 | [`mdast-util-to-markdown`][mdast-util-to-markdown]
250 | to enable MDX JSX.
251 |
252 | This extension configures `mdast-util-to-markdown` with
253 | [`options.fences: true`][mdast-util-to-markdown-fences] and
254 | [`options.resourceLink: true`][mdast-util-to-markdown-resourcelink] too, do not
255 | overwrite them!
256 |
257 | ###### Parameters
258 |
259 | * `options` ([`ToMarkdownOptions`][api-to-markdown-options])
260 | — configuration
261 |
262 | ###### Returns
263 |
264 | Extension for `mdast-util-to-markdown` to enable MDX JSX
265 | ([`FromMarkdownExtension`][to-markdown-extension]).
266 |
267 | ### `MdxJsxAttribute`
268 |
269 | MDX JSX attribute with a key (TypeScript type).
270 |
271 | ###### Type
272 |
273 | ```ts
274 | import type {Literal} from 'mdast'
275 |
276 | interface MdxJsxAttribute extends Literal {
277 | type: 'mdxJsxAttribute'
278 | name: string
279 | value?: MdxJsxAttributeValueExpression | string | null | undefined
280 | }
281 | ```
282 |
283 | ### `MdxJsxAttributeValueExpression`
284 |
285 | MDX JSX attribute value set to an expression (TypeScript type).
286 |
287 | ###### Type
288 |
289 | ```ts
290 | import type {Program} from 'estree-jsx'
291 | import type {Literal} from 'mdast'
292 |
293 | interface MdxJsxAttributeValueExpression extends Literal {
294 | type: 'mdxJsxAttributeValueExpression'
295 | data?: {estree?: Program | null | undefined} & Literal['data']
296 | }
297 | ```
298 |
299 | ### `MdxJsxExpressionAttribute`
300 |
301 | MDX JSX attribute as an expression (TypeScript type).
302 |
303 | ###### Type
304 |
305 | ```ts
306 | import type {Program} from 'estree-jsx'
307 | import type {Literal} from 'mdast'
308 |
309 | interface MdxJsxExpressionAttribute extends Literal {
310 | type: 'mdxJsxExpressionAttribute'
311 | data?: {estree?: Program | null | undefined} & Literal['data']
312 | }
313 | ```
314 |
315 | ### `MdxJsxFlowElement`
316 |
317 | MDX JSX element node, occurring in flow (block) (TypeScript type).
318 |
319 | ###### Type
320 |
321 | ```ts
322 | import type {BlockContent, DefinitionContent, Parent} from 'mdast'
323 |
324 | export interface MdxJsxFlowElement extends Parent {
325 | type: 'mdxJsxFlowElement'
326 | name: string | null
327 | attributes: Array
328 | children: Array
329 | }
330 | ```
331 |
332 | ### `MdxJsxFlowElementHast`
333 |
334 | Same as [`MdxJsxFlowElement`][api-mdx-jsx-flow-element], but registered with
335 | `@types/hast` (TypeScript type).
336 |
337 | ###### Type
338 |
339 | ```ts
340 | import type {ElementContent, Parent} from 'hast'
341 |
342 | export interface MdxJsxFlowElementHast extends Parent {
343 | type: 'mdxJsxFlowElement'
344 | name: string | null
345 | attributes: Array
346 | children: Array
347 | }
348 | ```
349 |
350 | ### `MdxJsxTextElement`
351 |
352 | MDX JSX element node, occurring in text (phrasing) (TypeScript type).
353 |
354 | ###### Type
355 |
356 | ```ts
357 | import type {Parent, PhrasingContent} from 'mdast'
358 |
359 | export interface MdxJsxTextElement extends Parent {
360 | type: 'mdxJsxTextElement'
361 | name: string | null
362 | attributes: Array
363 | children: Array
364 | }
365 | ```
366 |
367 | ### `MdxJsxTextElementHast`
368 |
369 | Same as [`MdxJsxTextElement`][api-mdx-jsx-text-element], but registered with
370 | `@types/hast` (TypeScript type).
371 |
372 | ###### Type
373 |
374 | ```ts
375 | import type {ElementContent, Parent} from 'hast'
376 |
377 | export interface MdxJsxTextElementHast extends Parent {
378 | type: 'mdxJsxTextElement'
379 | name: string | null
380 | attributes: Array
381 | children: Array
382 | }
383 | ```
384 |
385 | ### `ToMarkdownOptions`
386 |
387 | Configuration (TypeScript type).
388 |
389 | ##### Fields
390 |
391 | * `quote` (`'"'` or `"'"`, default: `'"'`)
392 | — preferred quote to use around attribute values
393 | * `quoteSmart` (`boolean`, default: `false`)
394 | — use the other quote if that results in less bytes
395 | * `tightSelfClosing` (`boolean`, default: `false`)
396 | — do not use an extra space when closing self-closing elements: `
`
397 | instead of `
`
398 | * `printWidth` (`number`, default: `Infinity`)
399 | — try and wrap syntax at this width.
400 | When set to a finite number (say, `80`), the formatter will print
401 | attributes on separate lines when a tag doesn’t fit on one line.
402 | The normal behavior is to print attributes with spaces between them instead
403 | of line endings
404 |
405 | ## HTML
406 |
407 | MDX JSX has no representation in HTML.
408 | Though, when you are dealing with MDX, you will likely go *through* hast.
409 | You can enable passing MDX JSX through to hast by configuring
410 | [`mdast-util-to-hast`][mdast-util-to-hast] with
411 | `passThrough: ['mdxJsxFlowElement', 'mdxJsxTextElement']`.
412 |
413 | ## Syntax
414 |
415 | See [Syntax in `micromark-extension-mdx-jsx`][syntax].
416 |
417 | ## Syntax tree
418 |
419 | The following interfaces are added to **[mdast][]** by this utility.
420 |
421 | ### Nodes
422 |
423 | #### `MdxJsxFlowElement`
424 |
425 | ```idl
426 | interface MdxJsxFlowElement <: Parent {
427 | type: 'mdxJsxFlowElement'
428 | }
429 |
430 | MdxJsxFlowElement includes MdxJsxElement
431 | ```
432 |
433 | **MdxJsxFlowElement** (**[Parent][dfn-parent]**) represents JSX in flow (block).
434 | It can be used where **[flow][dfn-content-flow]** content is expected.
435 | It includes the mixin **[MdxJsxElement][dfn-mixin-mdx-jsx-element]**.
436 |
437 | For example, the following markdown:
438 |
439 | ```markdown
440 |
441 | z
442 |
443 | ```
444 |
445 | Yields:
446 |
447 | ```js
448 | {
449 | type: 'mdxJsxFlowElement',
450 | name: 'w',
451 | attributes: [{type: 'mdxJsxAttribute', name: 'x', value: 'y'}],
452 | children: [{type: 'paragraph', children: [{type: 'text', value: 'z'}]}]
453 | }
454 | ```
455 |
456 | #### `MdxJsxTextElement`
457 |
458 | ```idl
459 | interface MdxJsxTextElement <: Parent {
460 | type: 'mdxJsxTextElement'
461 | }
462 |
463 | MdxJsxTextElement includes MdxJsxElement
464 | ```
465 |
466 | **MdxJsxTextElement** (**[Parent][dfn-parent]**) represents JSX in text (span,
467 | inline).
468 | It can be used where **[phrasing][dfn-content-phrasing]** content is
469 | expected.
470 | It includes the mixin **[MdxJsxElement][dfn-mixin-mdx-jsx-element]**.
471 |
472 | For example, the following markdown:
473 |
474 | ```markdown
475 | a d e.
476 | ```
477 |
478 | Yields:
479 |
480 | ```js
481 | {
482 | type: 'mdxJsxTextElement',
483 | name: 'b',
484 | attributes: [{type: 'mdxJsxAttribute', name: 'c', value: null}],
485 | children: [{type: 'text', value: 'd'}]
486 | }
487 | ```
488 |
489 | ### Mixin
490 |
491 | #### `MdxJsxElement`
492 |
493 | ```idl
494 | interface mixin MdxJsxElement {
495 | name: string?
496 | attributes: [MdxJsxExpressionAttribute | MdxJsxAttribute]
497 | }
498 |
499 | interface MdxJsxExpressionAttribute <: Literal {
500 | type: 'mdxJsxExpressionAttribute'
501 | }
502 |
503 | interface MdxJsxAttribute <: Node {
504 | type: 'mdxJsxAttribute'
505 | name: string
506 | value: MdxJsxAttributeValueExpression | string?
507 | }
508 |
509 | interface MdxJsxAttributeValueExpression <: Literal {
510 | type: 'mdxJsxAttributeValueExpression'
511 | }
512 | ```
513 |
514 | **MdxJsxElement** represents a JSX element.
515 |
516 | The `name` field can be present and represents an identifier.
517 | Without `name`, the element represents a fragment, in which case no attributes
518 | must be present.
519 |
520 | The `attributes` field represents information associated with the node.
521 | The value of the `attributes` field is a list of **MdxJsxExpressionAttribute**
522 | and **MdxJsxAttribute** nodes.
523 |
524 | **MdxJsxExpressionAttribute** represents an expression (typically in a
525 | programming language) that when evaluated results in multiple attributes.
526 |
527 | **MdxJsxAttribute** represents a single attribute.
528 | The `name` field must be present.
529 | The `value` field can be present, in which case it is either a string (a static
530 | value) or an expression (typically in a programming language) that when
531 | evaluated results in an attribute value.
532 |
533 | ### Content model
534 |
535 | ###### `FlowContent` (MDX JSX)
536 |
537 | ```idl
538 | type MdxJsxFlowContent = MdxJsxFlowElement | FlowContent
539 | ```
540 |
541 | ###### `PhrasingContent` (MDX JSX)
542 |
543 | ```idl
544 | type MdxJsxPhrasingContent = MdxJsxTextElement | PhrasingContent
545 | ```
546 |
547 | ## Types
548 |
549 | This package is fully typed with [TypeScript][].
550 | It exports the additional types [`MdxJsxAttribute`][api-mdx-jsx-attribute],
551 | [`MdxJsxAttributeValueExpression`][api-mdx-jsx-attribute-value-expression],
552 | [`MdxJsxExpressionAttribute`][api-mdx-jsx-expression-attribute],
553 | [`MdxJsxFlowElement`][api-mdx-jsx-flow-element],
554 | [`MdxJsxFlowElementHast`][api-mdx-jsx-flow-element-hast],
555 | [`MdxJsxTextElement`][api-mdx-jsx-text-element],
556 | [`MdxJsxTextElementHast`][api-mdx-jsx-text-element-hast], and
557 | [`ToMarkdownOptions`][api-to-markdown-options].
558 |
559 | It also registers the node types with `@types/mdast` and `@types/hast`.
560 | If you’re working with the syntax tree, make sure to import this utility
561 | somewhere in your types, as that registers the new node types in the tree.
562 |
563 | ```js
564 | /**
565 | * @import {} from 'mdast-util-mdx-jsx'
566 | * @import {Root} from 'mdast'
567 | */
568 |
569 | import {visit} from 'unist-util-visit'
570 |
571 | /** @type {Root} */
572 | const tree = getMdastNodeSomeHow()
573 |
574 | visit(tree, function (node) {
575 | // `node` can now be one of the JSX nodes.
576 | })
577 | ```
578 |
579 | ## Compatibility
580 |
581 | Projects maintained by the unified collective are compatible with maintained
582 | versions of Node.js.
583 |
584 | When we cut a new major release, we drop support for unmaintained versions of
585 | Node.
586 | This means we try to keep the current release line, `mdast-util-mdx-jsx@3`,
587 | compatible with Node.js 16.
588 |
589 | This utility works with `mdast-util-from-markdown` version 2+ and
590 | `mdast-util-to-markdown` version 2+.
591 |
592 | ## Related
593 |
594 | * [`micromark/micromark-extension-mdx-jsx`][micromark-extension-mdx-jsx]
595 | — support MDX JSX in micromark
596 | * [`syntax-tree/mdast-util-mdx`][mdast-util-mdx]
597 | — support MDX in mdast
598 | * [`remarkjs/remark-mdx`][remark-mdx]
599 | — support MDX in remark
600 |
601 | ## Contribute
602 |
603 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for
604 | ways to get started.
605 | See [`support.md`][support] for ways to get help.
606 |
607 | This project has a [code of conduct][coc].
608 | By interacting with this repository, organization, or community you agree to
609 | abide by its terms.
610 |
611 | ## License
612 |
613 | [MIT][license] © [Titus Wormer][author]
614 |
615 | [build-badge]: https://github.com/syntax-tree/mdast-util-mdx-jsx/workflows/main/badge.svg
616 |
617 | [build]: https://github.com/syntax-tree/mdast-util-mdx-jsx/actions
618 |
619 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/mdast-util-mdx-jsx.svg
620 |
621 | [coverage]: https://codecov.io/github/syntax-tree/mdast-util-mdx-jsx
622 |
623 | [downloads-badge]: https://img.shields.io/npm/dm/mdast-util-mdx-jsx.svg
624 |
625 | [downloads]: https://www.npmjs.com/package/mdast-util-mdx-jsx
626 |
627 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=mdast-util-mdx-jsx
628 |
629 | [size]: https://bundlejs.com/?q=mdast-util-mdx-jsx
630 |
631 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg
632 |
633 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg
634 |
635 | [collective]: https://opencollective.com/unified
636 |
637 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg
638 |
639 | [chat]: https://github.com/syntax-tree/unist/discussions
640 |
641 | [npm]: https://docs.npmjs.com/cli/install
642 |
643 | [esmsh]: https://esm.sh
644 |
645 | [license]: license
646 |
647 | [author]: https://wooorm.com
648 |
649 | [health]: https://github.com/syntax-tree/.github
650 |
651 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md
652 |
653 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md
654 |
655 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md
656 |
657 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
658 |
659 | [typescript]: https://www.typescriptlang.org
660 |
661 | [mdast]: https://github.com/syntax-tree/mdast
662 |
663 | [mdast-util-to-hast]: https://github.com/syntax-tree/mdast-util-to-hast
664 |
665 | [mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown
666 |
667 | [from-markdown-extension]: https://github.com/syntax-tree/mdast-util-from-markdown#extension
668 |
669 | [mdast-util-to-markdown]: https://github.com/syntax-tree/mdast-util-to-markdown
670 |
671 | [to-markdown-extension]: https://github.com/syntax-tree/mdast-util-to-markdown#options
672 |
673 | [mdast-util-mdx]: https://github.com/syntax-tree/mdast-util-mdx
674 |
675 | [program]: https://github.com/estree/estree/blob/master/es2015.md#programs
676 |
677 | [dfn-parent]: https://github.com/syntax-tree/mdast#parent
678 |
679 | [dfn-content-flow]: #flowcontent-mdx-jsx
680 |
681 | [dfn-content-phrasing]: #phrasingcontent-mdx-jsx
682 |
683 | [dfn-mixin-mdx-jsx-element]: #mdxjsxelement
684 |
685 | [jsx]: https://facebook.github.io/jsx/
686 |
687 | [what-is-mdx]: https://mdxjs.com/docs/what-is-mdx/
688 |
689 | [micromark-extension-mdx-jsx]: https://github.com/micromark/micromark-extension-mdx-jsx
690 |
691 | [syntax]: https://github.com/micromark/micromark-extension-mdx-jsx#syntax
692 |
693 | [mdast-util-to-markdown-fences]: https://github.com/syntax-tree/mdast-util-to-markdown#optionsfences
694 |
695 | [mdast-util-to-markdown-resourcelink]: https://github.com/syntax-tree/mdast-util-to-markdown#optionsresourcelink
696 |
697 | [remark-mdx]: https://mdxjs.com/packages/remark-mdx/
698 |
699 | [mdx]: https://mdxjs.com
700 |
701 | [api-mdx-jsx-from-markdown]: #mdxjsxfrommarkdown
702 |
703 | [api-mdx-jsx-to-markdown]: #mdxjsxtomarkdownoptions
704 |
705 | [api-mdx-jsx-attribute]: #mdxjsxattribute
706 |
707 | [api-mdx-jsx-attribute-value-expression]: #mdxjsxattributevalueexpression
708 |
709 | [api-mdx-jsx-expression-attribute]: #mdxjsxexpressionattribute
710 |
711 | [api-mdx-jsx-flow-element]: #mdxjsxflowelement
712 |
713 | [api-mdx-jsx-flow-element-hast]: #mdxjsxflowelementhast
714 |
715 | [api-mdx-jsx-text-element]: #mdxjsxtextelement
716 |
717 | [api-mdx-jsx-text-element-hast]: #mdxjsxtextelementhast
718 |
719 | [api-to-markdown-options]: #tomarkdownoptions
720 |
--------------------------------------------------------------------------------
/test.js:
--------------------------------------------------------------------------------
1 | import assert from 'node:assert/strict'
2 | import test from 'node:test'
3 | import * as acorn from 'acorn'
4 | import {mdxJsx} from 'micromark-extension-mdx-jsx'
5 | import {mdxMd} from 'micromark-extension-mdx-md'
6 | import {fromMarkdown} from 'mdast-util-from-markdown'
7 | import {mdxJsxFromMarkdown, mdxJsxToMarkdown} from 'mdast-util-mdx-jsx'
8 | import {toMarkdown} from 'mdast-util-to-markdown'
9 | import {removePosition} from 'unist-util-remove-position'
10 |
11 | test('core', async function (t) {
12 | await t.test('should expose the public api', async function () {
13 | assert.deepEqual(Object.keys(await import('mdast-util-mdx-jsx')).sort(), [
14 | 'mdxJsxFromMarkdown',
15 | 'mdxJsxToMarkdown'
16 | ])
17 | })
18 | })
19 |
20 | test('mdxJsxFromMarkdown', async function (t) {
21 | await t.test('should support flow jsx (agnostic)', async function () {
22 | assert.deepEqual(
23 | fromMarkdown('', {
24 | extensions: [mdxJsx()],
25 | mdastExtensions: [mdxJsxFromMarkdown()]
26 | }),
27 | {
28 | type: 'root',
29 | children: [
30 | {
31 | type: 'mdxJsxFlowElement',
32 | name: 'a',
33 | attributes: [],
34 | children: [],
35 | position: {
36 | start: {line: 1, column: 1, offset: 0},
37 | end: {line: 1, column: 6, offset: 5}
38 | }
39 | }
40 | ],
41 | position: {
42 | start: {line: 1, column: 1, offset: 0},
43 | end: {line: 1, column: 6, offset: 5}
44 | }
45 | }
46 | )
47 | })
48 |
49 | await t.test(
50 | 'should support flow jsx (agnostic) w/ just whitespace',
51 | async function () {
52 | const tree = fromMarkdown('\t \n', {
53 | extensions: [mdxJsx()],
54 | mdastExtensions: [mdxJsxFromMarkdown()]
55 | })
56 |
57 | removePosition(tree, {force: true})
58 |
59 | assert.deepEqual(tree, {
60 | type: 'root',
61 | children: [
62 | {type: 'mdxJsxFlowElement', name: 'x', attributes: [], children: []}
63 | ]
64 | })
65 | }
66 | )
67 |
68 | await t.test(
69 | 'should support self-closing text jsx (agnostic)',
70 | async function () {
71 | const tree = fromMarkdown('a c.', {
72 | extensions: [mdxJsx()],
73 | mdastExtensions: [mdxJsxFromMarkdown()]
74 | })
75 |
76 | removePosition(tree, {force: true})
77 |
78 | assert.deepEqual(tree, {
79 | type: 'root',
80 | children: [
81 | {
82 | type: 'paragraph',
83 | children: [
84 | {type: 'text', value: 'a '},
85 | {
86 | type: 'mdxJsxTextElement',
87 | name: 'b',
88 | attributes: [],
89 | children: []
90 | },
91 | {type: 'text', value: ' c.'}
92 | ]
93 | }
94 | ]
95 | })
96 | }
97 | )
98 |
99 | await t.test(
100 | 'should support a closed text jsx (agnostic)',
101 | async function () {
102 | const tree = fromMarkdown('a c.', {
103 | extensions: [mdxJsx()],
104 | mdastExtensions: [mdxJsxFromMarkdown()]
105 | })
106 |
107 | removePosition(tree, {force: true})
108 |
109 | assert.deepEqual(tree, {
110 | type: 'root',
111 | children: [
112 | {
113 | type: 'paragraph',
114 | children: [
115 | {type: 'text', value: 'a '},
116 | {
117 | type: 'mdxJsxTextElement',
118 | name: 'b',
119 | attributes: [],
120 | children: []
121 | },
122 | {type: 'text', value: ' c.'}
123 | ]
124 | }
125 | ]
126 | })
127 | }
128 | )
129 |
130 | await t.test(
131 | 'should support text jsx (agnostic) w/ content',
132 | async function () {
133 | const tree = fromMarkdown('a c d.', {
134 | extensions: [mdxJsx()],
135 | mdastExtensions: [mdxJsxFromMarkdown()]
136 | })
137 |
138 | removePosition(tree, {force: true})
139 |
140 | assert.deepEqual(tree, {
141 | type: 'root',
142 | children: [
143 | {
144 | type: 'paragraph',
145 | children: [
146 | {type: 'text', value: 'a '},
147 | {
148 | type: 'mdxJsxTextElement',
149 | name: 'b',
150 | attributes: [],
151 | children: [{type: 'text', value: 'c'}]
152 | },
153 | {type: 'text', value: ' d.'}
154 | ]
155 | }
156 | ]
157 | })
158 | }
159 | )
160 |
161 | await t.test(
162 | 'should support text jsx (agnostic) w/ markdown content',
163 | async function () {
164 | const tree = fromMarkdown('a *c* d.', {
165 | extensions: [mdxJsx()],
166 | mdastExtensions: [mdxJsxFromMarkdown()]
167 | })
168 |
169 | removePosition(tree, {force: true})
170 |
171 | assert.deepEqual(tree, {
172 | type: 'root',
173 | children: [
174 | {
175 | type: 'paragraph',
176 | children: [
177 | {type: 'text', value: 'a '},
178 | {
179 | type: 'mdxJsxTextElement',
180 | name: 'b',
181 | attributes: [],
182 | children: [
183 | {type: 'emphasis', children: [{type: 'text', value: 'c'}]}
184 | ]
185 | },
186 | {type: 'text', value: ' d.'}
187 | ]
188 | }
189 | ]
190 | })
191 | }
192 | )
193 |
194 | await t.test(
195 | 'should support a fragment text jsx (agnostic)',
196 | async function () {
197 | const tree = fromMarkdown('a <>> b.', {
198 | extensions: [mdxJsx()],
199 | mdastExtensions: [mdxJsxFromMarkdown()]
200 | })
201 |
202 | removePosition(tree, {force: true})
203 |
204 | assert.deepEqual(tree, {
205 | type: 'root',
206 | children: [
207 | {
208 | type: 'paragraph',
209 | children: [
210 | {type: 'text', value: 'a '},
211 | {
212 | type: 'mdxJsxTextElement',
213 | name: null,
214 | attributes: [],
215 | children: []
216 | },
217 | {type: 'text', value: ' b.'}
218 | ]
219 | }
220 | ]
221 | })
222 | }
223 | )
224 |
225 | await t.test(
226 | 'should crash on an unclosed text jsx (agnostic)',
227 | async function () {
228 | assert.throws(function () {
229 | fromMarkdown('a c', {
230 | extensions: [mdxJsx()],
231 | mdastExtensions: [mdxJsxFromMarkdown()]
232 | })
233 | }, /Expected a closing tag for `` \(1:3-1:6\) before the end of `paragraph`/)
234 | }
235 | )
236 |
237 | await t.test(
238 | 'should crash on an unclosed flow jsx (agnostic)',
239 | async function () {
240 | assert.throws(function () {
241 | fromMarkdown('', {
242 | extensions: [mdxJsx()],
243 | mdastExtensions: [mdxJsxFromMarkdown()]
244 | })
245 | }, /Expected a closing tag for `` \(1:1-1:4\)/)
246 | }
247 | )
248 |
249 | await t.test(
250 | 'should crash on unclosed jsx after closed jsx',
251 | async function () {
252 | assert.throws(function () {
253 | fromMarkdown('', {
254 | extensions: [mdxJsx()],
255 | mdastExtensions: [mdxJsxFromMarkdown()]
256 | })
257 | }, /Expected a closing tag for `` \(1:1-1:4\)/)
258 | }
259 | )
260 |
261 | await t.test(
262 | 'should support an attribute expression in text jsx (agnostic)',
263 | async function () {
264 | const tree = fromMarkdown('a c', {
265 | extensions: [mdxJsx()],
266 | mdastExtensions: [mdxJsxFromMarkdown()]
267 | })
268 |
269 | removePosition(tree, {force: true})
270 |
271 | assert.deepEqual(tree, {
272 | type: 'root',
273 | children: [
274 | {
275 | type: 'paragraph',
276 | children: [
277 | {type: 'text', value: 'a '},
278 | {
279 | type: 'mdxJsxTextElement',
280 | name: 'b',
281 | attributes: [
282 | {
283 | type: 'mdxJsxExpressionAttribute',
284 | value: '1 + 1',
285 | position: {
286 | start: {line: 1, column: 6, offset: 5},
287 | end: {line: 1, column: 13, offset: 12}
288 | }
289 | }
290 | ],
291 | children: []
292 | },
293 | {type: 'text', value: ' c'}
294 | ]
295 | }
296 | ]
297 | })
298 | }
299 | )
300 |
301 | await t.test(
302 | 'should support an attribute value expression in text jsx (agnostic)',
303 | async function () {
304 | const tree = fromMarkdown('a d', {
305 | extensions: [mdxJsx()],
306 | mdastExtensions: [mdxJsxFromMarkdown()]
307 | })
308 |
309 | removePosition(tree, {force: true})
310 |
311 | assert.deepEqual(tree, {
312 | type: 'root',
313 | children: [
314 | {
315 | type: 'paragraph',
316 | children: [
317 | {type: 'text', value: 'a '},
318 | {
319 | type: 'mdxJsxTextElement',
320 | name: 'b',
321 | attributes: [
322 | {
323 | type: 'mdxJsxAttribute',
324 | name: 'c',
325 | position: {
326 | end: {
327 | column: 15,
328 | line: 1,
329 | offset: 14
330 | },
331 | start: {
332 | column: 6,
333 | line: 1,
334 | offset: 5
335 | }
336 | },
337 | value: {
338 | type: 'mdxJsxAttributeValueExpression',
339 | value: '1 + 1'
340 | }
341 | }
342 | ],
343 | children: []
344 | },
345 | {type: 'text', value: ' d'}
346 | ]
347 | }
348 | ]
349 | })
350 | }
351 | )
352 |
353 | await t.test(
354 | 'should support an attribute expression in text jsx (gnostic)',
355 | async function () {
356 | const tree = fromMarkdown('a d', {
357 | extensions: [mdxJsx({acorn})],
358 | mdastExtensions: [mdxJsxFromMarkdown()]
359 | })
360 |
361 | removePosition(tree, {force: true})
362 |
363 | assert.deepEqual(tree, {
364 | type: 'root',
365 | children: [
366 | {
367 | type: 'paragraph',
368 | children: [
369 | {type: 'text', value: 'a '},
370 | {
371 | type: 'mdxJsxTextElement',
372 | name: 'b',
373 | attributes: [
374 | {
375 | type: 'mdxJsxExpressionAttribute',
376 | value: '...c',
377 | position: {
378 | start: {line: 1, column: 6, offset: 5},
379 | end: {line: 1, column: 12, offset: 11}
380 | }
381 | }
382 | ],
383 | children: []
384 | },
385 | {type: 'text', value: ' d'}
386 | ]
387 | }
388 | ]
389 | })
390 | }
391 | )
392 |
393 | await t.test(
394 | 'should support an complex attribute expression in flow jsx (gnostic)',
395 | async function () {
396 | const tree = fromMarkdown('', {
397 | extensions: [mdxJsx({acorn})],
398 | mdastExtensions: [mdxJsxFromMarkdown()]
399 | })
400 |
401 | removePosition(tree, {force: true})
402 |
403 | assert.deepEqual(tree, {
404 | type: 'root',
405 | children: [
406 | {
407 | type: 'mdxJsxFlowElement',
408 | name: 'a',
409 | attributes: [
410 | {
411 | type: 'mdxJsxExpressionAttribute',
412 | value: '...{b: 1, c: Infinity, d: false}',
413 | position: {
414 | start: {line: 1, column: 4, offset: 3},
415 | end: {line: 1, column: 38, offset: 37}
416 | }
417 | }
418 | ],
419 | children: []
420 | }
421 | ]
422 | })
423 | }
424 | )
425 |
426 | await t.test(
427 | 'should support an `estree` for an attribute expression in flow jsx (gnostic) w/ `addResult`',
428 | async function () {
429 | let tree = fromMarkdown('', {
430 | extensions: [mdxJsx({acorn, addResult: true})],
431 | mdastExtensions: [mdxJsxFromMarkdown()]
432 | })
433 |
434 | removePosition(tree, {force: true})
435 |
436 | // eslint-disable-next-line unicorn/prefer-structured-clone -- needed to turn instances into plain objects
437 | tree = JSON.parse(JSON.stringify(tree))
438 |
439 | assert.deepEqual(tree, {
440 | type: 'root',
441 | children: [
442 | {
443 | type: 'mdxJsxFlowElement',
444 | name: 'a',
445 | attributes: [
446 | {
447 | type: 'mdxJsxExpressionAttribute',
448 | value: '...b',
449 | position: {
450 | start: {line: 1, column: 4, offset: 3},
451 | end: {line: 1, column: 10, offset: 9}
452 | },
453 | data: {
454 | estree: {
455 | type: 'Program',
456 | start: 4,
457 | end: 8,
458 | body: [
459 | {
460 | type: 'ExpressionStatement',
461 | expression: {
462 | type: 'ObjectExpression',
463 | start: 4,
464 | end: 8,
465 | loc: {
466 | start: {line: 1, column: 4, offset: 4},
467 | end: {line: 1, column: 8, offset: 8}
468 | },
469 | properties: [
470 | {
471 | type: 'SpreadElement',
472 | start: 4,
473 | end: 8,
474 | loc: {
475 | start: {line: 1, column: 4, offset: 4},
476 | end: {line: 1, column: 8, offset: 8}
477 | },
478 | argument: {
479 | type: 'Identifier',
480 | start: 7,
481 | end: 8,
482 | loc: {
483 | start: {line: 1, column: 7, offset: 7},
484 | end: {line: 1, column: 8, offset: 8}
485 | },
486 | name: 'b',
487 | range: [7, 8]
488 | },
489 | range: [4, 8]
490 | }
491 | ],
492 | range: [4, 8]
493 | },
494 | start: 4,
495 | end: 8,
496 | loc: {
497 | start: {line: 1, column: 4, offset: 4},
498 | end: {line: 1, column: 8, offset: 8}
499 | },
500 | range: [4, 8]
501 | }
502 | ],
503 | sourceType: 'module',
504 | comments: [],
505 | loc: {
506 | start: {line: 1, column: 4, offset: 4},
507 | end: {line: 1, column: 8, offset: 8}
508 | },
509 | range: [4, 8]
510 | }
511 | }
512 | }
513 | ],
514 | children: []
515 | }
516 | ]
517 | })
518 | }
519 | )
520 |
521 | await t.test(
522 | 'should support an `estree` for an attribute value expression in flow jsx (gnostic) w/ `addResult`',
523 | async function () {
524 | let tree = fromMarkdown('', {
525 | extensions: [mdxJsx({acorn, addResult: true})],
526 | mdastExtensions: [mdxJsxFromMarkdown()]
527 | })
528 |
529 | removePosition(tree, {force: true})
530 |
531 | // eslint-disable-next-line unicorn/prefer-structured-clone -- needed to turn instances into plain objects
532 | tree = JSON.parse(JSON.stringify(tree))
533 |
534 | assert.deepEqual(tree, {
535 | type: 'root',
536 | children: [
537 | {
538 | type: 'mdxJsxFlowElement',
539 | name: 'a',
540 | attributes: [
541 | {
542 | type: 'mdxJsxAttribute',
543 | name: 'b',
544 | position: {
545 | end: {
546 | column: 9,
547 | line: 1,
548 | offset: 8
549 | },
550 | start: {
551 | column: 4,
552 | line: 1,
553 | offset: 3
554 | }
555 | },
556 | value: {
557 | type: 'mdxJsxAttributeValueExpression',
558 | value: '1',
559 | data: {
560 | estree: {
561 | type: 'Program',
562 | start: 6,
563 | end: 7,
564 | body: [
565 | {
566 | type: 'ExpressionStatement',
567 | expression: {
568 | type: 'Literal',
569 | start: 6,
570 | end: 7,
571 | loc: {
572 | start: {line: 1, column: 6, offset: 6},
573 | end: {line: 1, column: 7, offset: 7}
574 | },
575 | value: 1,
576 | raw: '1',
577 | range: [6, 7]
578 | },
579 | start: 6,
580 | end: 7,
581 | loc: {
582 | start: {line: 1, column: 6, offset: 6},
583 | end: {line: 1, column: 7, offset: 7}
584 | },
585 | range: [6, 7]
586 | }
587 | ],
588 | sourceType: 'module',
589 | comments: [],
590 | loc: {
591 | start: {line: 1, column: 6, offset: 6},
592 | end: {line: 1, column: 7, offset: 7}
593 | },
594 | range: [6, 7]
595 | }
596 | }
597 | }
598 | }
599 | ],
600 | children: []
601 | }
602 | ]
603 | })
604 | }
605 | )
606 |
607 | await t.test(
608 | 'should crash on a non-spread attribute expression',
609 | async function () {
610 | assert.throws(function () {
611 | fromMarkdown('a c', {
612 | extensions: [mdxJsx({acorn})],
613 | mdastExtensions: [mdxJsxFromMarkdown()]
614 | })
615 | }, /Could not parse expression with acorn/)
616 | }
617 | )
618 |
619 | await t.test(
620 | 'should crash on invalid JS in an attribute expression',
621 | async function () {
622 | assert.throws(function () {
623 | fromMarkdown('a d', {
624 | extensions: [mdxJsx({acorn})],
625 | mdastExtensions: [mdxJsxFromMarkdown()]
626 | })
627 | }, /Could not parse expression with acorn/)
628 | }
629 | )
630 |
631 | await t.test(
632 | 'should *not* support whitespace in the opening tag (fragment)',
633 | async function () {
634 | assert.throws(function () {
635 | fromMarkdown('a < \t>b>', {
636 | extensions: [mdxJsx({acorn})],
637 | mdastExtensions: [mdxJsxFromMarkdown()]
638 | })
639 | }, /Unexpected closing slash `\/` in tag, expected an open tag first/)
640 | }
641 | )
642 |
643 | await t.test(
644 | 'should support whitespace in the opening tag (named)',
645 | async function () {
646 | const tree = fromMarkdown('a c', {
647 | extensions: [mdxJsx()],
648 | mdastExtensions: [mdxJsxFromMarkdown()]
649 | })
650 |
651 | removePosition(tree, {force: true})
652 |
653 | assert.deepEqual(tree, {
654 | type: 'root',
655 | children: [
656 | {
657 | type: 'paragraph',
658 | children: [
659 | {type: 'text', value: 'a '},
660 | {
661 | type: 'mdxJsxTextElement',
662 | name: 'b',
663 | attributes: [],
664 | children: [{type: 'text', value: 'c'}]
665 | }
666 | ]
667 | }
668 | ]
669 | })
670 | }
671 | )
672 |
673 | await t.test(
674 | 'should support non-ascii identifier start characters',
675 | async function () {
676 | const tree = fromMarkdown('<π />', {
677 | extensions: [mdxJsx()],
678 | mdastExtensions: [mdxJsxFromMarkdown()]
679 | })
680 |
681 | removePosition(tree, {force: true})
682 |
683 | assert.deepEqual(tree, {
684 | type: 'root',
685 | children: [
686 | {type: 'mdxJsxFlowElement', name: 'π', attributes: [], children: []}
687 | ]
688 | })
689 | }
690 | )
691 |
692 | await t.test(
693 | 'should support non-ascii identifier continuation characters',
694 | async function () {
695 | const tree = fromMarkdown('', {
696 | extensions: [mdxJsx()],
697 | mdastExtensions: [mdxJsxFromMarkdown()]
698 | })
699 |
700 | removePosition(tree, {force: true})
701 |
702 | assert.deepEqual(tree, {
703 | type: 'root',
704 | children: [
705 | {type: 'mdxJsxFlowElement', name: 'ab', attributes: [], children: []}
706 | ]
707 | })
708 | }
709 | )
710 |
711 | await t.test(
712 | 'should support dots in names for method names',
713 | async function () {
714 | const tree = fromMarkdown('', {
715 | extensions: [mdxJsx()],
716 | mdastExtensions: [mdxJsxFromMarkdown()]
717 | })
718 |
719 | removePosition(tree, {force: true})
720 |
721 | assert.deepEqual(tree, {
722 | type: 'root',
723 | children: [
724 | {
725 | type: 'mdxJsxFlowElement',
726 | name: 'abc.def.ghi',
727 | attributes: [],
728 | children: []
729 | }
730 | ]
731 | })
732 | }
733 | )
734 |
735 | await t.test(
736 | 'should support colons in names for local names',
737 | async function () {
738 | const tree = fromMarkdown('b svg :rect>', {
739 | extensions: [mdxJsx()],
740 | mdastExtensions: [mdxJsxFromMarkdown()]
741 | })
742 |
743 | removePosition(tree, {force: true})
744 |
745 | assert.deepEqual(tree, {
746 | type: 'root',
747 | children: [
748 | {
749 | type: 'paragraph',
750 | children: [
751 | {
752 | type: 'mdxJsxTextElement',
753 | name: 'svg:rect',
754 | attributes: [],
755 | children: [{type: 'text', value: 'b'}]
756 | }
757 | ]
758 | }
759 | ]
760 | })
761 | }
762 | )
763 |
764 | await t.test('should support attributes', async function () {
765 | const tree = fromMarkdown('a i.', {
766 | extensions: [mdxJsx()],
767 | mdastExtensions: [mdxJsxFromMarkdown()]
768 | })
769 |
770 | removePosition(tree, {force: true})
771 |
772 | assert.deepEqual(tree, {
773 | type: 'root',
774 | children: [
775 | {
776 | type: 'paragraph',
777 | children: [
778 | {
779 | type: 'text',
780 | value: 'a '
781 | },
782 | {
783 | type: 'mdxJsxTextElement',
784 | name: 'b',
785 | attributes: [
786 | {
787 | type: 'mdxJsxAttribute',
788 | name: 'c',
789 | value: null,
790 | position: {
791 | start: {
792 | line: 1,
793 | column: 6,
794 | offset: 5
795 | },
796 | end: {
797 | line: 1,
798 | column: 7,
799 | offset: 6
800 | }
801 | }
802 | },
803 | {
804 | type: 'mdxJsxAttribute',
805 | name: 'd',
806 | value: 'd',
807 | position: {
808 | start: {
809 | line: 1,
810 | column: 12,
811 | offset: 11
812 | },
813 | end: {
814 | line: 1,
815 | column: 17,
816 | offset: 16
817 | }
818 | }
819 | },
820 | {
821 | type: 'mdxJsxAttribute',
822 | name: 'efg',
823 | value: 'h',
824 | position: {
825 | start: {
826 | line: 1,
827 | column: 19,
828 | offset: 18
829 | },
830 | end: {
831 | line: 1,
832 | column: 26,
833 | offset: 25
834 | }
835 | }
836 | }
837 | ],
838 | children: [
839 | {
840 | type: 'text',
841 | value: 'i'
842 | }
843 | ]
844 | },
845 | {
846 | type: 'text',
847 | value: '.'
848 | }
849 | ]
850 | }
851 | ]
852 | })
853 | })
854 |
855 | await t.test('should support prefixed attributes', async function () {
856 | const tree = fromMarkdown('', {
857 | extensions: [mdxJsx()],
858 | mdastExtensions: [mdxJsxFromMarkdown()]
859 | })
860 |
861 | removePosition(tree, {force: true})
862 |
863 | assert.deepEqual(tree, {
864 | type: 'root',
865 | children: [
866 | {
867 | type: 'mdxJsxFlowElement',
868 | name: 'a',
869 | attributes: [
870 | {
871 | type: 'mdxJsxAttribute',
872 | name: 'xml:lang',
873 | position: {
874 | end: {
875 | column: 10,
876 | line: 2,
877 | offset: 23
878 | },
879 | start: {
880 | column: 4,
881 | line: 1,
882 | offset: 3
883 | }
884 | },
885 | value: 'de-CH'
886 | },
887 | {
888 | type: 'mdxJsxAttribute',
889 | name: 'foo:bar',
890 | position: {
891 | end: {
892 | column: 18,
893 | line: 2,
894 | offset: 31
895 | },
896 | start: {
897 | column: 11,
898 | line: 2,
899 | offset: 24
900 | }
901 | },
902 | value: null
903 | }
904 | ],
905 | children: []
906 | }
907 | ]
908 | })
909 | })
910 |
911 | await t.test(
912 | 'should support prefixed and normal attributes',
913 | async function () {
914 | const tree = fromMarkdown('', {
915 | extensions: [mdxJsx()],
916 | mdastExtensions: [mdxJsxFromMarkdown()]
917 | })
918 |
919 | removePosition(tree, {force: true})
920 |
921 | // @todo check it, includes spaces at end of tags
922 | assert.deepEqual(tree, {
923 | type: 'root',
924 | children: [
925 | {
926 | type: 'mdxJsxFlowElement',
927 | name: 'b',
928 | attributes: [
929 | {
930 | type: 'mdxJsxAttribute',
931 | name: 'a',
932 | value: null,
933 | position: {
934 | start: {
935 | line: 1,
936 | column: 4,
937 | offset: 3
938 | },
939 | end: {
940 | line: 1,
941 | column: 5,
942 | offset: 4
943 | }
944 | }
945 | },
946 | {
947 | type: 'mdxJsxAttribute',
948 | name: 'b:c',
949 | value: null,
950 | position: {
951 | start: {
952 | line: 1,
953 | column: 6,
954 | offset: 5
955 | },
956 | end: {
957 | line: 1,
958 | column: 11,
959 | offset: 10
960 | }
961 | }
962 | },
963 | {
964 | type: 'mdxJsxAttribute',
965 | name: 'd:e',
966 | value: 'f',
967 | position: {
968 | start: {
969 | line: 1,
970 | column: 12,
971 | offset: 11
972 | },
973 | end: {
974 | line: 1,
975 | column: 23,
976 | offset: 22
977 | }
978 | }
979 | },
980 | {
981 | type: 'mdxJsxAttribute',
982 | name: 'g',
983 | value: null,
984 | position: {
985 | start: {
986 | line: 1,
987 | column: 24,
988 | offset: 23
989 | },
990 | end: {
991 | line: 1,
992 | column: 25,
993 | offset: 24
994 | }
995 | }
996 | }
997 | ],
998 | children: []
999 | }
1000 | ]
1001 | })
1002 | }
1003 | )
1004 |
1005 | await t.test('should support code (text) in jsx (text)', async function () {
1006 | const tree = fromMarkdown('a <>`<`> c', {
1007 | extensions: [mdxJsx()],
1008 | mdastExtensions: [mdxJsxFromMarkdown()]
1009 | })
1010 |
1011 | removePosition(tree, {force: true})
1012 |
1013 | assert.deepEqual(tree, {
1014 | type: 'root',
1015 | children: [
1016 | {
1017 | type: 'paragraph',
1018 | children: [
1019 | {type: 'text', value: 'a '},
1020 | {
1021 | type: 'mdxJsxTextElement',
1022 | name: null,
1023 | attributes: [],
1024 | children: [{type: 'inlineCode', value: '<'}]
1025 | },
1026 | {type: 'text', value: ' c'}
1027 | ]
1028 | }
1029 | ]
1030 | })
1031 | })
1032 |
1033 | await t.test('should support code (fenced) in jsx (flow)', async function () {
1034 | const tree = fromMarkdown('<>\n```js\n<\n```\n>', {
1035 | extensions: [mdxJsx()],
1036 | mdastExtensions: [mdxJsxFromMarkdown()]
1037 | })
1038 |
1039 | removePosition(tree, {force: true})
1040 |
1041 | assert.deepEqual(tree, {
1042 | type: 'root',
1043 | children: [
1044 | {
1045 | type: 'mdxJsxFlowElement',
1046 | name: null,
1047 | attributes: [],
1048 | children: [{type: 'code', lang: 'js', meta: null, value: '<'}]
1049 | }
1050 | ]
1051 | })
1052 | })
1053 |
1054 | await t.test(
1055 | 'should crash on a closing tag w/o open elements (text)',
1056 | async function () {
1057 | assert.throws(function () {
1058 | fromMarkdown('a > c', {
1059 | extensions: [mdxJsx()],
1060 | mdastExtensions: [mdxJsxFromMarkdown()]
1061 | })
1062 | }, /Unexpected closing slash `\/` in tag, expected an open tag first/)
1063 | }
1064 | )
1065 |
1066 | await t.test(
1067 | 'should crash on a closing tag w/o open elements (flow)',
1068 | async function () {
1069 | assert.throws(function () {
1070 | fromMarkdown('>', {
1071 | extensions: [mdxJsx()],
1072 | mdastExtensions: [mdxJsxFromMarkdown()]
1073 | })
1074 | }, /Unexpected closing slash `\/` in tag, expected an open tag first/)
1075 | }
1076 | )
1077 |
1078 | await t.test('should crash on mismatched tags (1)', async function () {
1079 | assert.throws(function () {
1080 | fromMarkdown('a <>', {
1081 | extensions: [mdxJsx()],
1082 | mdastExtensions: [mdxJsxFromMarkdown()]
1083 | })
1084 | }, /Unexpected closing tag `<\/b>`, expected corresponding closing tag for `<>` \(1:3-1:5\)/)
1085 | })
1086 |
1087 | await t.test('should crash on mismatched tags (2)', async function () {
1088 | assert.throws(function () {
1089 | fromMarkdown('a >', {
1090 | extensions: [mdxJsx()],
1091 | mdastExtensions: [mdxJsxFromMarkdown()]
1092 | })
1093 | }, /Unexpected closing tag `<\/>`, expected corresponding closing tag for `` \(1:3-1:6\)/)
1094 | })
1095 |
1096 | await t.test('should crash on mismatched tags (3)', async function () {
1097 | assert.throws(function () {
1098 | fromMarkdown('a ', {
1099 | extensions: [mdxJsx()],
1100 | mdastExtensions: [mdxJsxFromMarkdown()]
1101 | })
1102 | }, /Unexpected closing tag `<\/a>`, expected corresponding closing tag for `` \(1:3-1:8\)/)
1103 | })
1104 |
1105 | await t.test('should crash on mismatched tags (4)', async function () {
1106 | assert.throws(function () {
1107 | fromMarkdown('a ', {
1108 | extensions: [mdxJsx()],
1109 | mdastExtensions: [mdxJsxFromMarkdown()]
1110 | })
1111 | }, /Unexpected closing tag `<\/a\.b>`, expected corresponding closing tag for `` \(1:3-1:6\)/)
1112 | })
1113 |
1114 | await t.test('should crash on mismatched tags (5)', async function () {
1115 | assert.throws(function () {
1116 | fromMarkdown('a ', {
1117 | extensions: [mdxJsx()],
1118 | mdastExtensions: [mdxJsxFromMarkdown()]
1119 | })
1120 | }, /Unexpected closing tag `<\/a\.c>`, expected corresponding closing tag for `` \(1:3-1:8\)/)
1121 | })
1122 |
1123 | await t.test('should crash on mismatched tags (6)', async function () {
1124 | assert.throws(function () {
1125 | fromMarkdown('a ', {
1126 | extensions: [mdxJsx()],
1127 | mdastExtensions: [mdxJsxFromMarkdown()]
1128 | })
1129 | }, /Unexpected closing tag `<\/a>`, expected corresponding closing tag for `` \(1:3-1:8\)/)
1130 | })
1131 |
1132 | await t.test('should crash on mismatched tags (7)', async function () {
1133 | assert.throws(function () {
1134 | fromMarkdown('a ', {
1135 | extensions: [mdxJsx()],
1136 | mdastExtensions: [mdxJsxFromMarkdown()]
1137 | })
1138 | }, /Unexpected closing tag `<\/a:b>`, expected corresponding closing tag for `` \(1:3-1:6\)/)
1139 | })
1140 |
1141 | await t.test('should crash on mismatched tags (8)', async function () {
1142 | assert.throws(function () {
1143 | fromMarkdown('a ', {
1144 | extensions: [mdxJsx()],
1145 | mdastExtensions: [mdxJsxFromMarkdown()]
1146 | })
1147 | }, /Unexpected closing tag `<\/a:c>`, expected corresponding closing tag for `` \(1:3-1:8\)/)
1148 | })
1149 |
1150 | await t.test('should crash on mismatched tags (9)', async function () {
1151 | assert.throws(function () {
1152 | fromMarkdown('a ', {
1153 | extensions: [mdxJsx()],
1154 | mdastExtensions: [mdxJsxFromMarkdown()]
1155 | })
1156 | }, /Unexpected closing tag `<\/a\.b>`, expected corresponding closing tag for `` \(1:3-1:8\)/)
1157 | })
1158 |
1159 | await t.test('should crash on a closing self-closing tag', async function () {
1160 | assert.throws(function () {
1161 | fromMarkdown('b', {
1162 | extensions: [mdxJsx()],
1163 | mdastExtensions: [mdxJsxFromMarkdown()]
1164 | })
1165 | }, /Unexpected self-closing slash `\/` in closing tag, expected the end of the tag/)
1166 | })
1167 |
1168 | await t.test(
1169 | 'should crash on a closing tag w/ attributes',
1170 | async function () {
1171 | assert.throws(function () {
1172 | fromMarkdown('b', {
1173 | extensions: [mdxJsx()],
1174 | mdastExtensions: [mdxJsxFromMarkdown()]
1175 | })
1176 | }, /Unexpected attribute in closing tag, expected the end of the tag/)
1177 | }
1178 | )
1179 |
1180 | await t.test('should support nested jsx (text)', async function () {
1181 | const tree = fromMarkdown('a c <>d> e', {
1182 | extensions: [mdxJsx()],
1183 | mdastExtensions: [mdxJsxFromMarkdown()]
1184 | })
1185 |
1186 | removePosition(tree, {force: true})
1187 |
1188 | assert.deepEqual(tree, {
1189 | type: 'root',
1190 | children: [
1191 | {
1192 | type: 'paragraph',
1193 | children: [
1194 | {type: 'text', value: 'a '},
1195 | {
1196 | type: 'mdxJsxTextElement',
1197 | name: 'b',
1198 | attributes: [],
1199 | children: [
1200 | {type: 'text', value: 'c '},
1201 | {
1202 | type: 'mdxJsxTextElement',
1203 | name: null,
1204 | attributes: [],
1205 | children: [{type: 'text', value: 'd'}]
1206 | },
1207 | {type: 'text', value: ' e'}
1208 | ]
1209 | }
1210 | ]
1211 | }
1212 | ]
1213 | })
1214 | })
1215 |
1216 | await t.test('should support nested jsx (flow)', async function () {
1217 | const tree = fromMarkdown(' <>\nb\n>\n', {
1218 | extensions: [mdxJsx()],
1219 | mdastExtensions: [mdxJsxFromMarkdown()]
1220 | })
1221 |
1222 | removePosition(tree, {force: true})
1223 |
1224 | assert.deepEqual(tree, {
1225 | type: 'root',
1226 | children: [
1227 | {
1228 | type: 'mdxJsxFlowElement',
1229 | name: 'a',
1230 | attributes: [],
1231 | children: [
1232 | {
1233 | type: 'mdxJsxFlowElement',
1234 | name: null,
1235 | attributes: [],
1236 | children: [
1237 | {type: 'paragraph', children: [{type: 'text', value: 'b'}]}
1238 | ]
1239 | }
1240 | ]
1241 | }
1242 | ]
1243 | })
1244 | })
1245 |
1246 | await t.test(
1247 | 'should support character references in attribute values',
1248 | async function () {
1249 | const tree = fromMarkdown(
1250 | '',
1251 | {
1252 | extensions: [mdxJsx()],
1253 | mdastExtensions: [mdxJsxFromMarkdown()]
1254 | }
1255 | )
1256 |
1257 | removePosition(tree, {force: true})
1258 |
1259 | assert.deepEqual(tree, {
1260 | type: 'root',
1261 | children: [
1262 | {
1263 | type: 'mdxJsxFlowElement',
1264 | name: 'x',
1265 | attributes: [
1266 | {
1267 | type: 'mdxJsxAttribute',
1268 | name: 'y',
1269 | position: {
1270 | end: {
1271 | column: 158,
1272 | line: 1,
1273 | offset: 157
1274 | },
1275 | start: {
1276 | column: 4,
1277 | line: 1,
1278 | offset: 3
1279 | }
1280 | },
1281 | value:
1282 | 'Character references can be used: ", \', <, >, {, and }, they can be named, decimal, or hexadecimal: © ≠ 𝌆'
1283 | }
1284 | ],
1285 | children: []
1286 | }
1287 | ]
1288 | })
1289 | }
1290 | )
1291 |
1292 | await t.test(
1293 | 'should support as text if the tag is not the last thing',
1294 | async function () {
1295 | const tree = fromMarkdown('.', {
1296 | extensions: [mdxJsx()],
1297 | mdastExtensions: [mdxJsxFromMarkdown()]
1298 | })
1299 |
1300 | removePosition(tree, {force: true})
1301 |
1302 | assert.deepEqual(tree, {
1303 | type: 'root',
1304 | children: [
1305 | {
1306 | type: 'paragraph',
1307 | children: [
1308 | {
1309 | type: 'mdxJsxTextElement',
1310 | name: 'x',
1311 | attributes: [],
1312 | children: []
1313 | },
1314 | {type: 'text', value: '.'}
1315 | ]
1316 | }
1317 | ]
1318 | })
1319 | }
1320 | )
1321 |
1322 | await t.test(
1323 | 'should support as text if the tag is not the first thing',
1324 | async function () {
1325 | const tree = fromMarkdown('.', {
1326 | extensions: [mdxJsx()],
1327 | mdastExtensions: [mdxJsxFromMarkdown()]
1328 | })
1329 |
1330 | removePosition(tree, {force: true})
1331 |
1332 | assert.deepEqual(tree, {
1333 | type: 'root',
1334 | children: [
1335 | {
1336 | type: 'paragraph',
1337 | children: [
1338 | {type: 'text', value: '.'},
1339 | {
1340 | type: 'mdxJsxTextElement',
1341 | name: 'x',
1342 | attributes: [],
1343 | children: []
1344 | }
1345 | ]
1346 | }
1347 | ]
1348 | })
1349 | }
1350 | )
1351 |
1352 | await t.test(
1353 | 'should crash when misnesting w/ attention (emphasis)',
1354 | async function () {
1355 | assert.throws(function () {
1356 | fromMarkdown('a *open close* c.', {
1357 | extensions: [mdxJsx()],
1358 | mdastExtensions: [mdxJsxFromMarkdown()]
1359 | })
1360 | }, /Expected a closing tag for `` \(1:9-1:12\) before the end of `emphasis`/)
1361 | }
1362 | )
1363 |
1364 | await t.test(
1365 | 'should crash when misnesting w/ attention (strong)',
1366 | async function () {
1367 | assert.throws(function () {
1368 | fromMarkdown('a **open close** c.', {
1369 | extensions: [mdxJsx()],
1370 | mdastExtensions: [mdxJsxFromMarkdown()]
1371 | })
1372 | }, /Expected a closing tag for `` \(1:10-1:13\) before the end of `strong`/)
1373 | }
1374 | )
1375 |
1376 | await t.test(
1377 | 'should crash when misnesting w/ label (link)',
1378 | async function () {
1379 | assert.throws(function () {
1380 | fromMarkdown('a [open close](c) d.', {
1381 | extensions: [mdxJsx()],
1382 | mdastExtensions: [mdxJsxFromMarkdown()]
1383 | })
1384 | })
1385 | }
1386 | )
1387 |
1388 | await t.test(
1389 | 'should crash when misnesting w/ label (image)',
1390 | async function () {
1391 | assert.throws(function () {
1392 | fromMarkdown('a  d.', {
1393 | extensions: [mdxJsx()],
1394 | mdastExtensions: [mdxJsxFromMarkdown()]
1395 | })
1396 | })
1397 | }
1398 | )
1399 |
1400 | await t.test(
1401 | 'should crash when misnesting w/ attention (emphasis)',
1402 | async function () {
1403 | assert.throws(function () {
1404 | fromMarkdown(' a *open close* d.', {
1405 | extensions: [mdxJsx()],
1406 | mdastExtensions: [mdxJsxFromMarkdown()]
1407 | })
1408 | }, /Expected the closing tag `<\/b>` either after the end of `emphasis` \(1:24\) or another opening tag after the start of `emphasis` \(1:7\)/)
1409 | }
1410 | )
1411 |
1412 | await t.test('should support line endings in elements', async function () {
1413 | const tree = fromMarkdown('> a \n> c d.', {
1414 | extensions: [mdxJsx()],
1415 | mdastExtensions: [mdxJsxFromMarkdown()]
1416 | })
1417 |
1418 | removePosition(tree, {force: true})
1419 |
1420 | assert.deepEqual(tree, {
1421 | type: 'root',
1422 | children: [
1423 | {
1424 | type: 'blockquote',
1425 | children: [
1426 | {
1427 | type: 'paragraph',
1428 | children: [
1429 | {type: 'text', value: 'a '},
1430 | {
1431 | type: 'mdxJsxTextElement',
1432 | name: 'b',
1433 | attributes: [],
1434 | children: [{type: 'text', value: '\nc '}]
1435 | },
1436 | {type: 'text', value: ' d.'}
1437 | ]
1438 | }
1439 | ]
1440 | }
1441 | ]
1442 | })
1443 | })
1444 |
1445 | await t.test(
1446 | 'should support line endings in attribute values',
1447 | async function () {
1448 | const tree = fromMarkdown('> a f', {
1449 | extensions: [mdxJsx()],
1450 | mdastExtensions: [mdxJsxFromMarkdown()]
1451 | })
1452 |
1453 | removePosition(tree, {force: true})
1454 |
1455 | assert.deepEqual(tree, {
1456 | type: 'root',
1457 | children: [
1458 | {
1459 | type: 'blockquote',
1460 | children: [
1461 | {
1462 | type: 'paragraph',
1463 | children: [
1464 | {type: 'text', value: 'a '},
1465 | {
1466 | type: 'mdxJsxTextElement',
1467 | name: 'b',
1468 | attributes: [
1469 | {
1470 | type: 'mdxJsxAttribute',
1471 | name: 'c',
1472 | position: {
1473 | end: {
1474 | column: 5,
1475 | line: 2,
1476 | offset: 16
1477 | },
1478 | start: {
1479 | column: 8,
1480 | line: 1,
1481 | offset: 7
1482 | }
1483 | },
1484 | value: 'd\ne'
1485 | }
1486 | ],
1487 | children: []
1488 | },
1489 | {type: 'text', value: ' f'}
1490 | ]
1491 | }
1492 | ]
1493 | }
1494 | ]
1495 | })
1496 | }
1497 | )
1498 |
1499 | await t.test(
1500 | 'should support line endings in attribute value expressions',
1501 | async function () {
1502 | const tree = fromMarkdown('> a e} /> f', {
1503 | extensions: [mdxJsx()],
1504 | mdastExtensions: [mdxJsxFromMarkdown()]
1505 | })
1506 |
1507 | removePosition(tree, {force: true})
1508 |
1509 | assert.deepEqual(tree, {
1510 | type: 'root',
1511 | children: [
1512 | {
1513 | type: 'blockquote',
1514 | children: [
1515 | {
1516 | type: 'paragraph',
1517 | children: [
1518 | {type: 'text', value: 'a '},
1519 | {
1520 | type: 'mdxJsxTextElement',
1521 | name: 'b',
1522 | attributes: [
1523 | {
1524 | type: 'mdxJsxAttribute',
1525 | name: 'c',
1526 | position: {
1527 | end: {
1528 | column: 5,
1529 | line: 2,
1530 | offset: 16
1531 | },
1532 | start: {
1533 | column: 8,
1534 | line: 1,
1535 | offset: 7
1536 | }
1537 | },
1538 | value: {
1539 | type: 'mdxJsxAttributeValueExpression',
1540 | value: 'd\ne'
1541 | }
1542 | }
1543 | ],
1544 | children: []
1545 | },
1546 | {type: 'text', value: ' f'}
1547 | ]
1548 | }
1549 | ]
1550 | }
1551 | ]
1552 | })
1553 | }
1554 | )
1555 |
1556 | await t.test(
1557 | 'should support line endings in attribute expressions',
1558 | async function () {
1559 | const tree = fromMarkdown('> a d} /> e', {
1560 | extensions: [mdxJsx()],
1561 | mdastExtensions: [mdxJsxFromMarkdown()]
1562 | })
1563 |
1564 | removePosition(tree, {force: true})
1565 |
1566 | assert.deepEqual(tree, {
1567 | type: 'root',
1568 | children: [
1569 | {
1570 | type: 'blockquote',
1571 | children: [
1572 | {
1573 | type: 'paragraph',
1574 | children: [
1575 | {type: 'text', value: 'a '},
1576 | {
1577 | type: 'mdxJsxTextElement',
1578 | name: 'b',
1579 | attributes: [
1580 | {
1581 | type: 'mdxJsxExpressionAttribute',
1582 | value: 'c\nd',
1583 | position: {
1584 | start: {line: 1, column: 8, offset: 7},
1585 | end: {line: 2, column: 5, offset: 14}
1586 | }
1587 | }
1588 | ],
1589 | children: []
1590 | },
1591 | {type: 'text', value: ' e'}
1592 | ]
1593 | }
1594 | ]
1595 | }
1596 | ]
1597 | })
1598 | }
1599 | )
1600 |
1601 | await t.test(
1602 | 'should support line endings in attribute expressions (gnostic)',
1603 | async function () {
1604 | const tree = fromMarkdown('> a 2]} /> c', {
1605 | extensions: [mdxJsx({acorn})],
1606 | mdastExtensions: [mdxJsxFromMarkdown()]
1607 | })
1608 |
1609 | removePosition(tree, {force: true})
1610 |
1611 | assert.deepEqual(tree, {
1612 | type: 'root',
1613 | children: [
1614 | {
1615 | type: 'blockquote',
1616 | children: [
1617 | {
1618 | type: 'paragraph',
1619 | children: [
1620 | {type: 'text', value: 'a '},
1621 | {
1622 | type: 'mdxJsxTextElement',
1623 | name: 'b',
1624 | attributes: [
1625 | {
1626 | type: 'mdxJsxExpressionAttribute',
1627 | value: '...[1,\n2]',
1628 | position: {
1629 | start: {line: 1, column: 8, offset: 7},
1630 | end: {line: 2, column: 6, offset: 20}
1631 | }
1632 | }
1633 | ],
1634 | children: []
1635 | },
1636 | {type: 'text', value: ' c'}
1637 | ]
1638 | }
1639 | ]
1640 | }
1641 | ]
1642 | })
1643 | }
1644 | )
1645 |
1646 | await t.test('should support block quotes in flow', async function () {
1647 | const tree = fromMarkdown('\n> b\nc\n> d\n', {
1648 | extensions: [mdxJsx({acorn})],
1649 | mdastExtensions: [mdxJsxFromMarkdown()]
1650 | })
1651 |
1652 | removePosition(tree, {force: true})
1653 |
1654 | assert.deepEqual(tree, {
1655 | type: 'root',
1656 | children: [
1657 | {
1658 | type: 'mdxJsxFlowElement',
1659 | name: 'a',
1660 | attributes: [],
1661 | children: [
1662 | {
1663 | type: 'blockquote',
1664 | children: [
1665 | {
1666 | type: 'paragraph',
1667 | children: [{type: 'text', value: 'b\nc\nd'}]
1668 | }
1669 | ]
1670 | }
1671 | ]
1672 | }
1673 | ]
1674 | })
1675 | })
1676 |
1677 | await t.test('should support lists in flow', async function () {
1678 | const tree = fromMarkdown('\n- b\nc\n- d\n', {
1679 | extensions: [mdxJsx({acorn})],
1680 | mdastExtensions: [mdxJsxFromMarkdown()]
1681 | })
1682 |
1683 | removePosition(tree, {force: true})
1684 |
1685 | assert.deepEqual(tree, {
1686 | type: 'root',
1687 | children: [
1688 | {
1689 | type: 'mdxJsxFlowElement',
1690 | name: 'a',
1691 | attributes: [],
1692 | children: [
1693 | {
1694 | type: 'list',
1695 | ordered: false,
1696 | start: null,
1697 | spread: false,
1698 | children: [
1699 | {
1700 | type: 'listItem',
1701 | spread: false,
1702 | checked: null,
1703 | children: [
1704 | {
1705 | type: 'paragraph',
1706 | children: [{type: 'text', value: 'b\nc'}]
1707 | }
1708 | ]
1709 | },
1710 | {
1711 | type: 'listItem',
1712 | spread: false,
1713 | checked: null,
1714 | children: [
1715 | {
1716 | type: 'paragraph',
1717 | children: [{type: 'text', value: 'd'}]
1718 | }
1719 | ]
1720 | }
1721 | ]
1722 | }
1723 | ]
1724 | }
1725 | ]
1726 | })
1727 | })
1728 |
1729 | await t.test('should support normal markdown w/o jsx', async function () {
1730 | const tree = fromMarkdown('> a\n- b\nc\n- d', {
1731 | extensions: [mdxJsx({acorn})],
1732 | mdastExtensions: [mdxJsxFromMarkdown()]
1733 | })
1734 |
1735 | removePosition(tree, {force: true})
1736 |
1737 | assert.deepEqual(tree, {
1738 | type: 'root',
1739 | children: [
1740 | {
1741 | type: 'blockquote',
1742 | children: [
1743 | {type: 'paragraph', children: [{type: 'text', value: 'a'}]}
1744 | ]
1745 | },
1746 | {
1747 | type: 'list',
1748 | ordered: false,
1749 | start: null,
1750 | spread: false,
1751 | children: [
1752 | {
1753 | type: 'listItem',
1754 | spread: false,
1755 | checked: null,
1756 | children: [
1757 | {type: 'paragraph', children: [{type: 'text', value: 'b\nc'}]}
1758 | ]
1759 | },
1760 | {
1761 | type: 'listItem',
1762 | spread: false,
1763 | checked: null,
1764 | children: [
1765 | {type: 'paragraph', children: [{type: 'text', value: 'd'}]}
1766 | ]
1767 | }
1768 | ]
1769 | }
1770 | ]
1771 | })
1772 | })
1773 |
1774 | await t.test(
1775 | 'should support multiple flow elements with their tags on the same line',
1776 | async function () {
1777 | const tree = fromMarkdown('\n\nz\n\n', {
1778 | extensions: [mdxJsx({acorn})],
1779 | mdastExtensions: [mdxJsxFromMarkdown()]
1780 | })
1781 |
1782 | removePosition(tree, {force: true})
1783 |
1784 | assert.deepEqual(tree, {
1785 | type: 'root',
1786 | children: [
1787 | {
1788 | type: 'mdxJsxFlowElement',
1789 | name: 'x',
1790 | attributes: [],
1791 | children: [
1792 | {
1793 | type: 'mdxJsxFlowElement',
1794 | name: 'y',
1795 | attributes: [],
1796 | children: [
1797 | {type: 'paragraph', children: [{type: 'text', value: 'z'}]}
1798 | ]
1799 | }
1800 | ]
1801 | }
1802 | ]
1803 | })
1804 | }
1805 | )
1806 | })
1807 |
1808 | test('mdxJsxToMarkdown', async function (t) {
1809 | await t.test(
1810 | 'should serialize flow jsx w/o `name`, `attributes`, or `children`',
1811 | async function () {
1812 | assert.deepEqual(
1813 | toMarkdown(
1814 | // @ts-expect-error: check how the runtime handles `attributes`, `children`, `name` missing.
1815 | {type: 'mdxJsxFlowElement'},
1816 | {extensions: [mdxJsxToMarkdown()]}
1817 | ),
1818 | '<>>\n'
1819 | )
1820 | }
1821 | )
1822 |
1823 | await t.test(
1824 | 'should serialize flow jsx w/ `name` w/o `attributes`, `children`',
1825 | async function () {
1826 | assert.deepEqual(
1827 | toMarkdown(
1828 | // @ts-expect-error: check how the runtime handles `attributes`, `children` missing.
1829 | {type: 'mdxJsxFlowElement', name: 'x'},
1830 | {extensions: [mdxJsxToMarkdown()]}
1831 | ),
1832 | '\n'
1833 | )
1834 | }
1835 | )
1836 |
1837 | await t.test(
1838 | 'should serialize flow jsx w/ `name`, `children` w/o `attributes`',
1839 | async function () {
1840 | assert.deepEqual(
1841 | toMarkdown(
1842 | // @ts-expect-error: check how the runtime handles `attributes` missing.
1843 | {
1844 | type: 'mdxJsxFlowElement',
1845 | name: 'x',
1846 | children: [
1847 | {type: 'paragraph', children: [{type: 'text', value: 'y'}]}
1848 | ]
1849 | },
1850 | {extensions: [mdxJsxToMarkdown()]}
1851 | ),
1852 | '\n y\n\n'
1853 | )
1854 | }
1855 | )
1856 |
1857 | await t.test(
1858 | 'should serialize flow jsx w/ `children` w/o `name`, `attributes`',
1859 | async function () {
1860 | assert.deepEqual(
1861 | toMarkdown(
1862 | // @ts-expect-error: check how the runtime handles `children`, `name` missing.
1863 | {
1864 | type: 'mdxJsxFlowElement',
1865 | children: [
1866 | {type: 'paragraph', children: [{type: 'text', value: 'y'}]}
1867 | ]
1868 | },
1869 | {extensions: [mdxJsxToMarkdown()]}
1870 | ),
1871 | '<>\n y\n>\n'
1872 | )
1873 | }
1874 | )
1875 |
1876 | await t.test(
1877 | 'should crash when serializing fragment w/ attributes',
1878 | async function () {
1879 | assert.throws(function () {
1880 | toMarkdown(
1881 | // @ts-expect-error: check how the runtime handles `children`, `name` missing.
1882 | {
1883 | type: 'mdxJsxFlowElement',
1884 | attributes: [{type: 'mdxJsxExpressionAttribute', value: 'x'}]
1885 | },
1886 | {extensions: [mdxJsxToMarkdown()]}
1887 | )
1888 | }, /Cannot serialize fragment w\/ attributes/)
1889 | }
1890 | )
1891 |
1892 | await t.test(
1893 | 'should serialize flow jsx w/ `name`, `attributes` w/o `children`',
1894 | async function () {
1895 | assert.deepEqual(
1896 | toMarkdown(
1897 | // @ts-expect-error: check how the runtime handles `children` missing.
1898 | {
1899 | type: 'mdxJsxFlowElement',
1900 | name: 'x',
1901 | attributes: [{type: 'mdxJsxExpressionAttribute', value: 'y'}]
1902 | },
1903 | {extensions: [mdxJsxToMarkdown()]}
1904 | ),
1905 | '\n'
1906 | )
1907 | }
1908 | )
1909 |
1910 | await t.test(
1911 | 'should serialize flow jsx w/ `name`, `attributes`, `children`',
1912 | async function () {
1913 | assert.deepEqual(
1914 | toMarkdown(
1915 | {
1916 | type: 'mdxJsxFlowElement',
1917 | name: 'x',
1918 | attributes: [{type: 'mdxJsxExpressionAttribute', value: 'y'}],
1919 | children: [
1920 | {type: 'paragraph', children: [{type: 'text', value: 'z'}]}
1921 | ]
1922 | },
1923 | {extensions: [mdxJsxToMarkdown()]}
1924 | ),
1925 | '\n z\n\n'
1926 | )
1927 | }
1928 | )
1929 |
1930 | await t.test(
1931 | 'should serialize flow jsx w/ `name`, multiple `attributes` w/o `children`',
1932 | async function () {
1933 | assert.deepEqual(
1934 | toMarkdown(
1935 | // @ts-expect-error: check how the runtime handles `children` missing.
1936 | {
1937 | type: 'mdxJsxFlowElement',
1938 | name: 'x',
1939 | attributes: [
1940 | {type: 'mdxJsxExpressionAttribute', value: 'y'},
1941 | {type: 'mdxJsxExpressionAttribute', value: 'z'}
1942 | ]
1943 | },
1944 | {extensions: [mdxJsxToMarkdown()]}
1945 | ),
1946 | '\n'
1947 | )
1948 | }
1949 | )
1950 |
1951 | await t.test('should serialize expression attributes', async function () {
1952 | assert.deepEqual(
1953 | toMarkdown(
1954 | {
1955 | type: 'mdxJsxFlowElement',
1956 | name: 'x',
1957 | attributes: [
1958 | {type: 'mdxJsxExpressionAttribute', value: '...{y: "z"}'}
1959 | ],
1960 | children: []
1961 | },
1962 | {extensions: [mdxJsxToMarkdown()]}
1963 | ),
1964 | '\n'
1965 | )
1966 | })
1967 |
1968 | await t.test(
1969 | 'should serialize expression attributes w/o `value`',
1970 | async function () {
1971 | assert.deepEqual(
1972 | toMarkdown(
1973 | {
1974 | type: 'mdxJsxFlowElement',
1975 | name: 'x',
1976 | attributes: [
1977 | // @ts-expect-error: check how the runtime handles `value` missing.
1978 | {type: 'mdxJsxExpressionAttribute'}
1979 | ]
1980 | },
1981 | {extensions: [mdxJsxToMarkdown()]}
1982 | ),
1983 | '\n'
1984 | )
1985 | }
1986 | )
1987 |
1988 | await t.test(
1989 | 'should crash when serializing attribute w/o name',
1990 | async function () {
1991 | assert.throws(function () {
1992 | toMarkdown(
1993 | {
1994 | type: 'mdxJsxFlowElement',
1995 | name: 'x',
1996 | attributes: [
1997 | // @ts-expect-error: check how the runtime handles `name` missing.
1998 | {type: 'mdxJsxAttribute', value: 'y'}
1999 | ]
2000 | },
2001 | {extensions: [mdxJsxToMarkdown()]}
2002 | )
2003 | }, / Cannot serialize attribute w\/o name/)
2004 | }
2005 | )
2006 |
2007 | await t.test('should serialize boolean attributes', async function () {
2008 | assert.deepEqual(
2009 | toMarkdown(
2010 | {
2011 | type: 'mdxJsxFlowElement',
2012 | name: 'x',
2013 | attributes: [{type: 'mdxJsxAttribute', name: 'y'}],
2014 | children: []
2015 | },
2016 | {extensions: [mdxJsxToMarkdown()]}
2017 | ),
2018 | '\n'
2019 | )
2020 | })
2021 |
2022 | await t.test('should serialize value attributes', async function () {
2023 | assert.deepEqual(
2024 | toMarkdown(
2025 | {
2026 | type: 'mdxJsxFlowElement',
2027 | name: 'x',
2028 | attributes: [{type: 'mdxJsxAttribute', name: 'y', value: 'z'}],
2029 | children: []
2030 | },
2031 | {extensions: [mdxJsxToMarkdown()]}
2032 | ),
2033 | '\n'
2034 | )
2035 | })
2036 |
2037 | await t.test(
2038 | 'should serialize value expression attributes',
2039 | async function () {
2040 | assert.deepEqual(
2041 | toMarkdown(
2042 | {
2043 | type: 'mdxJsxFlowElement',
2044 | name: 'x',
2045 | attributes: [
2046 | {
2047 | type: 'mdxJsxAttribute',
2048 | name: 'y',
2049 | value: {type: 'mdxJsxAttributeValueExpression', value: 'z'}
2050 | }
2051 | ],
2052 | children: []
2053 | },
2054 | {extensions: [mdxJsxToMarkdown()]}
2055 | ),
2056 | '\n'
2057 | )
2058 | }
2059 | )
2060 |
2061 | await t.test(
2062 | 'should serialize value expression attributes w/o `value`',
2063 | async function () {
2064 | assert.deepEqual(
2065 | toMarkdown(
2066 | {
2067 | type: 'mdxJsxFlowElement',
2068 | name: 'x',
2069 | attributes: [
2070 | {
2071 | type: 'mdxJsxAttribute',
2072 | name: 'y',
2073 | // @ts-expect-error: check how the runtime handles `value` missing.
2074 | value: {type: 'mdxJsxAttributeValueExpression'}
2075 | }
2076 | ]
2077 | },
2078 | {extensions: [mdxJsxToMarkdown()]}
2079 | ),
2080 | '\n'
2081 | )
2082 | }
2083 | )
2084 |
2085 | await t.test(
2086 | 'should serialize text jsx w/o `name`, `attributes`, or `children`',
2087 | async function () {
2088 | assert.deepEqual(
2089 | toMarkdown(
2090 | // @ts-expect-error: check how the runtime handles `attributes`, `name`, `children` missing.
2091 | {type: 'mdxJsxTextElement'},
2092 | {extensions: [mdxJsxToMarkdown()]}
2093 | ),
2094 | '<>>\n'
2095 | )
2096 | }
2097 | )
2098 |
2099 | await t.test(
2100 | 'should serialize text jsx w/ `name` w/o `attributes`, `children`',
2101 | async function () {
2102 | assert.deepEqual(
2103 | toMarkdown(
2104 | // @ts-expect-error: check how the runtime handles `attributes`, `children` missing.
2105 | {type: 'mdxJsxTextElement', name: 'x'},
2106 | {extensions: [mdxJsxToMarkdown()]}
2107 | ),
2108 | '\n'
2109 | )
2110 | }
2111 | )
2112 |
2113 | await t.test(
2114 | 'should serialize text jsx w/ `name`, `children` w/o `attributes`',
2115 | async function () {
2116 | assert.deepEqual(
2117 | toMarkdown(
2118 | // @ts-expect-error: check how the runtime handles `attributes` missing.
2119 | {
2120 | type: 'mdxJsxTextElement',
2121 | name: 'x',
2122 | children: [{type: 'strong', children: [{type: 'text', value: 'y'}]}]
2123 | },
2124 | {extensions: [mdxJsxToMarkdown()]}
2125 | ),
2126 | '**y**\n'
2127 | )
2128 | }
2129 | )
2130 |
2131 | await t.test('should serialize text jsx w/ attributes', async function () {
2132 | assert.deepEqual(
2133 | toMarkdown(
2134 | {
2135 | type: 'mdxJsxTextElement',
2136 | name: 'x',
2137 | attributes: [
2138 | {type: 'mdxJsxAttribute', name: 'y', value: 'z'},
2139 | {type: 'mdxJsxAttribute', name: 'a'}
2140 | ],
2141 | children: []
2142 | },
2143 | {extensions: [mdxJsxToMarkdown()]}
2144 | ),
2145 | '\n'
2146 | )
2147 | })
2148 |
2149 | await t.test('should serialize text jsx in flow', async function () {
2150 | assert.deepEqual(
2151 | toMarkdown(
2152 | {
2153 | type: 'paragraph',
2154 | children: [
2155 | {type: 'text', value: 'w '},
2156 | {
2157 | type: 'mdxJsxTextElement',
2158 | name: 'x',
2159 | attributes: [],
2160 | children: [{type: 'text', value: 'y'}]
2161 | },
2162 | {type: 'text', value: ' z.'}
2163 | ]
2164 | },
2165 | {extensions: [mdxJsxToMarkdown()]}
2166 | ),
2167 | 'w y z.\n'
2168 | )
2169 | })
2170 |
2171 | await t.test('should serialize flow in flow jsx', async function () {
2172 | assert.deepEqual(
2173 | toMarkdown(
2174 | {
2175 | type: 'mdxJsxFlowElement',
2176 | name: 'x',
2177 | attributes: [],
2178 | children: [
2179 | {
2180 | type: 'blockquote',
2181 | children: [
2182 | {type: 'paragraph', children: [{type: 'text', value: 'a'}]}
2183 | ]
2184 | },
2185 | {
2186 | type: 'list',
2187 | children: [
2188 | {
2189 | type: 'listItem',
2190 | children: [
2191 | {
2192 | type: 'paragraph',
2193 | children: [{type: 'text', value: 'b\nc'}]
2194 | }
2195 | ]
2196 | },
2197 | {
2198 | type: 'listItem',
2199 | children: [
2200 | {type: 'paragraph', children: [{type: 'text', value: 'd'}]}
2201 | ]
2202 | }
2203 | ]
2204 | }
2205 | ]
2206 | },
2207 | {extensions: [mdxJsxToMarkdown()]}
2208 | ),
2209 | '\n > a\n\n * b\n c\n\n * d\n\n'
2210 | )
2211 | })
2212 |
2213 | await t.test('should escape `<` in text', async function () {
2214 | assert.deepEqual(
2215 | toMarkdown(
2216 | {type: 'paragraph', children: [{type: 'text', value: 'a < b'}]},
2217 | {extensions: [mdxJsxToMarkdown()]}
2218 | ),
2219 | 'a \\< b\n'
2220 | )
2221 | })
2222 |
2223 | await t.test('should escape `<` at the start of a line', async function () {
2224 | assert.deepEqual(
2225 | toMarkdown(
2226 | {type: 'definition', identifier: 'a', url: 'x', title: 'a\n<\nb'},
2227 | {extensions: [mdxJsxToMarkdown()]}
2228 | ),
2229 | '[a]: x "a\n\\<\nb"\n'
2230 | )
2231 | })
2232 |
2233 | await t.test('should not serialize links as autolinks', async function () {
2234 | assert.deepEqual(
2235 | toMarkdown(
2236 | {
2237 | type: 'link',
2238 | url: 'svg:rect',
2239 | children: [{type: 'text', value: 'svg:rect'}]
2240 | },
2241 | {extensions: [mdxJsxToMarkdown()]}
2242 | ),
2243 | '[svg:rect](svg:rect)\n'
2244 | )
2245 | })
2246 |
2247 | await t.test('should not serialize code as indented', async function () {
2248 | assert.deepEqual(
2249 | toMarkdown(
2250 | {type: 'code', value: 'x'},
2251 | {extensions: [mdxJsxToMarkdown()]}
2252 | ),
2253 | '```\nx\n```\n'
2254 | )
2255 | })
2256 |
2257 | await t.test(
2258 | 'should support `options.quote` to quote attribute values',
2259 | async function () {
2260 | assert.deepEqual(
2261 | toMarkdown(
2262 | {
2263 | type: 'mdxJsxFlowElement',
2264 | name: 'x',
2265 | attributes: [{type: 'mdxJsxAttribute', name: 'y', value: 'z'}],
2266 | children: []
2267 | },
2268 | {extensions: [mdxJsxToMarkdown({quote: "'"})]}
2269 | ),
2270 | "\n"
2271 | )
2272 | }
2273 | )
2274 |
2275 | await t.test(
2276 | 'should crash on an unclosed text jsx (agnostic)',
2277 | async function () {
2278 | assert.throws(function () {
2279 | toMarkdown(
2280 | {
2281 | type: 'mdxJsxFlowElement',
2282 | name: 'x',
2283 | attributes: [],
2284 | children: []
2285 | },
2286 | // @ts-expect-error: check how the runtime handles `quote` being wrong.
2287 | {extensions: [mdxJsxToMarkdown({quote: '!'})]}
2288 | )
2289 | }, /Cannot serialize attribute values with `!` for `options.quote`, expected `"`, or `'`/)
2290 | }
2291 | )
2292 |
2293 | await t.test(
2294 | 'should support `options.quoteSmart`: prefer `quote` w/o quotes',
2295 | async function () {
2296 | assert.deepEqual(
2297 | toMarkdown(
2298 | {
2299 | type: 'mdxJsxFlowElement',
2300 | name: 'x',
2301 | attributes: [{type: 'mdxJsxAttribute', name: 'y', value: 'z'}],
2302 | children: []
2303 | },
2304 | {extensions: [mdxJsxToMarkdown({quoteSmart: true})]}
2305 | ),
2306 | '\n'
2307 | )
2308 | }
2309 | )
2310 |
2311 | await t.test(
2312 | 'should support `options.quoteSmart`: prefer `quote` w/ equal quotes',
2313 | async function () {
2314 | assert.deepEqual(
2315 | toMarkdown(
2316 | {
2317 | type: 'mdxJsxFlowElement',
2318 | name: 'x',
2319 | attributes: [{type: 'mdxJsxAttribute', name: 'y', value: 'z"a\'b'}],
2320 | children: []
2321 | },
2322 | {extensions: [mdxJsxToMarkdown({quoteSmart: true})]}
2323 | ),
2324 | '\n'
2325 | )
2326 | }
2327 | )
2328 |
2329 | await t.test(
2330 | 'should support `options.quoteSmart`: use alternative w/ more preferred quotes',
2331 | async function () {
2332 | assert.deepEqual(
2333 | toMarkdown(
2334 | {
2335 | type: 'mdxJsxFlowElement',
2336 | name: 'x',
2337 | attributes: [
2338 | {type: 'mdxJsxAttribute', name: 'y', value: 'z"a\'b"c'}
2339 | ],
2340 | children: []
2341 | },
2342 | {extensions: [mdxJsxToMarkdown({quoteSmart: true})]}
2343 | ),
2344 | '\n'
2345 | )
2346 | }
2347 | )
2348 |
2349 | await t.test(
2350 | 'should support `options.quoteSmart`: use quote w/ more alternative quotes',
2351 | async function () {
2352 | assert.deepEqual(
2353 | toMarkdown(
2354 | {
2355 | type: 'mdxJsxFlowElement',
2356 | name: 'x',
2357 | attributes: [
2358 | {type: 'mdxJsxAttribute', name: 'y', value: "z\"a'b'c"}
2359 | ],
2360 | children: []
2361 | },
2362 | {extensions: [mdxJsxToMarkdown({quoteSmart: true})]}
2363 | ),
2364 | '\n'
2365 | )
2366 | }
2367 | )
2368 |
2369 | await t.test(
2370 | 'should support `options.tightSelfClosing`: no space when `false`',
2371 | async function () {
2372 | assert.deepEqual(
2373 | toMarkdown(
2374 | {type: 'mdxJsxFlowElement', name: 'x', attributes: [], children: []},
2375 | {extensions: [mdxJsxToMarkdown({tightSelfClosing: false})]}
2376 | ),
2377 | '\n'
2378 | )
2379 | }
2380 | )
2381 |
2382 | await t.test(
2383 | 'should support `options.tightSelfClosing`: space when `true`',
2384 | async function () {
2385 | assert.deepEqual(
2386 | toMarkdown(
2387 | {type: 'mdxJsxFlowElement', name: 'x', attributes: [], children: []},
2388 | {extensions: [mdxJsxToMarkdown({tightSelfClosing: true})]}
2389 | ),
2390 | '\n'
2391 | )
2392 | }
2393 | )
2394 |
2395 | await t.test(
2396 | 'should support attributes on one line up to the given `options.printWidth`',
2397 | async function () {
2398 | assert.deepEqual(
2399 | toMarkdown(
2400 | {
2401 | type: 'mdxJsxFlowElement',
2402 | name: 'x',
2403 | attributes: [
2404 | {type: 'mdxJsxAttribute', name: 'y', value: 'aaa'},
2405 | {type: 'mdxJsxAttribute', name: 'z', value: 'aa'}
2406 | ],
2407 | children: []
2408 | },
2409 | {extensions: [mdxJsxToMarkdown({printWidth: 20})]}
2410 | ),
2411 | '\n'
2412 | )
2413 | }
2414 | )
2415 |
2416 | await t.test(
2417 | 'should support attributes on separate lines up to the given `options.printWidth`',
2418 | async function () {
2419 | assert.deepEqual(
2420 | toMarkdown(
2421 | {
2422 | type: 'mdxJsxFlowElement',
2423 | name: 'x',
2424 | attributes: [
2425 | {type: 'mdxJsxAttribute', name: 'y', value: 'aaa'},
2426 | {type: 'mdxJsxAttribute', name: 'z', value: 'aaa'}
2427 | ],
2428 | children: []
2429 | },
2430 | {extensions: [mdxJsxToMarkdown({printWidth: 20})]}
2431 | ),
2432 | '\n'
2433 | )
2434 | }
2435 | )
2436 |
2437 | await t.test(
2438 | 'should support attributes on separate lines if they contain line endings',
2439 | async function () {
2440 | assert.deepEqual(
2441 | toMarkdown(
2442 | {
2443 | type: 'mdxJsxFlowElement',
2444 | name: 'x',
2445 | attributes: [
2446 | {type: 'mdxJsxExpressionAttribute', value: '\n ...a\n'}
2447 | ],
2448 | children: []
2449 | },
2450 | {extensions: [mdxJsxToMarkdown({printWidth: 20})]}
2451 | ),
2452 | '\n'
2453 | )
2454 | }
2455 | )
2456 | })
2457 |
2458 | test('roundtrip', async function (t) {
2459 | await t.test('should roundtrip `attribute`', async function () {
2460 | equal('', '\n')
2461 | })
2462 |
2463 | await t.test(
2464 | 'should roundtrip `attribute in nested element`',
2465 | async function () {
2466 | equal(
2467 | '\n\n',
2468 | '\n \n\n'
2469 | )
2470 | }
2471 | )
2472 |
2473 | await t.test(
2474 | 'should roundtrip `attribute in nested elements`',
2475 | async function () {
2476 | equal(
2477 | '\n \n \n \n',
2478 | '\n \n \n \n\n'
2479 | )
2480 | }
2481 | )
2482 |
2483 | await t.test('should roundtrip `attribute expression`', async function () {
2484 | equal('', '\n')
2485 | })
2486 |
2487 | await t.test(
2488 | 'should roundtrip `attribute expression in nested element`',
2489 | async function () {
2490 | equal(
2491 | '\n\n',
2492 | '\n \n\n'
2493 | )
2494 | }
2495 | )
2496 |
2497 | await t.test(
2498 | 'should roundtrip `attribute expression in nested elements`',
2499 | async function () {
2500 | equal(
2501 | '\n \n \n \n',
2502 | '\n \n \n \n\n'
2503 | )
2504 | }
2505 | )
2506 |
2507 | await t.test('should roundtrip `expression`', async function () {
2508 | equal('', '\n')
2509 | })
2510 |
2511 | await t.test(
2512 | 'should roundtrip `expression in nested element`',
2513 | async function () {
2514 | equal(
2515 | '\n\n',
2516 | '\n \n\n'
2517 | )
2518 | }
2519 | )
2520 |
2521 | await t.test(
2522 | 'should roundtrip `expression in nested elements`',
2523 | async function () {
2524 | equal(
2525 | '\n \n \n \n',
2526 | '\n \n \n \n\n'
2527 | )
2528 | }
2529 | )
2530 |
2531 | await t.test(
2532 | 'should roundtrip `children in nested elements`',
2533 | async function () {
2534 | equal(
2535 | `
2536 |
2537 |
2538 | > # d
2539 | - e
2540 | ---
2541 | 1. f
2542 | ~~~js
2543 | g
2544 | ~~~
2545 |
2546 |
2547 |
2548 | `,
2549 | `
2550 |
2551 |
2552 | > # d
2553 |
2554 | * e
2555 |
2556 | ***
2557 |
2558 | 1. f
2559 |
2560 | \`\`\`js
2561 | g
2562 | \`\`\`
2563 |
2564 |
2565 |
2566 |
2567 |
2568 | `
2569 | )
2570 | }
2571 | )
2572 |
2573 | await t.test(
2574 | 'should roundtrip `text children in flow elements`',
2575 | async function () {
2576 | equal(
2577 | `
2581 | `,
2582 | `
2586 | `
2587 | )
2588 | }
2589 | )
2590 |
2591 | await t.test('should roundtrip `nested JSX and lists`', async function () {
2592 | const source = `
2593 | * Alpha
2594 |
2595 |
2596 | * Bravo
2597 |
2598 |
2599 |
2600 | * Charlie
2601 |
2602 | * Delta
2603 |
2604 |
2605 | Echo
2606 |
2607 |
2608 |
2609 | Foxtrot
2610 |
2611 |
2612 |
2613 |
2614 |
2615 |
2616 |
2617 | Golf
2618 |
2619 |
2620 |
2621 | `
2622 | equal(source, source)
2623 | })
2624 |
2625 | await t.test(
2626 | 'should roundtrip `nested JSX and block quotes`',
2627 | async function () {
2628 | const source = `
2629 | > Alpha
2630 | >
2631 | >
2632 | > > Bravo
2633 | > >
2634 | > >
2635 | > >
2636 | > > > Charlie
2637 | > > >
2638 | > > > > Delta
2639 | > > > >
2640 | > > > >
2641 | > > > > Echo
2642 | > > > >
2643 | > > >
2644 | > > >
2645 | > > > Foxtrot
2646 | > > >
2647 | > >
2648 | > >
2649 | >
2650 |
2651 |
2652 |
2653 | Golf
2654 |
2655 |
2656 |
2657 | `
2658 | equal(source, source)
2659 | }
2660 | )
2661 | })
2662 |
2663 | /**
2664 | * @param {string} input
2665 | * @param {string} output
2666 | */
2667 | function equal(input, output) {
2668 | const intermediate1 = process(input)
2669 | assert.equal(intermediate1, output, '#1')
2670 | const intermediate2 = process(intermediate1)
2671 | assert.equal(intermediate2, output, '#2')
2672 | const intermediate3 = process(intermediate2)
2673 | assert.equal(intermediate3, output, '#3')
2674 | const intermediate4 = process(intermediate3)
2675 | assert.equal(intermediate4, output, '#4')
2676 | }
2677 |
2678 | /**
2679 | * @param {string} input
2680 | */
2681 | function process(input) {
2682 | return toMarkdown(
2683 | fromMarkdown(input, {
2684 | extensions: [mdxMd(), mdxJsx()],
2685 | mdastExtensions: [mdxJsxFromMarkdown()]
2686 | }),
2687 | {extensions: [mdxJsxToMarkdown()]}
2688 | )
2689 | }
2690 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "checkJs": true,
4 | "customConditions": ["development"],
5 | "declaration": true,
6 | "declarationMap": true,
7 | "emitDeclarationOnly": true,
8 | "exactOptionalPropertyTypes": true,
9 | "lib": ["es2022"],
10 | "module": "node16",
11 | "strict": true,
12 | "target": "es2022"
13 | },
14 | "exclude": ["coverage/", "node_modules/"],
15 | "include": ["**/*.js", "index.d.ts"]
16 | }
17 |
--------------------------------------------------------------------------------