├── .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 : '') + '' 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: 'a‌b', 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', { 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 ![open close](c) 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 | --------------------------------------------------------------------------------