├── .prettierignore ├── .npmrc ├── test ├── types.js ├── types.d.ts └── index.js ├── packages ├── micromark-util-events-to-acorn │ ├── .npmrc │ ├── dev │ │ ├── lib │ │ │ ├── types.js │ │ │ ├── types.d.ts │ │ │ └── index.js │ │ ├── index.js │ │ └── index.d.ts │ ├── tsconfig.json │ ├── license │ ├── package.json │ └── readme.md ├── micromark-extension-mdx-expression │ ├── .npmrc │ ├── dev │ │ ├── index.js │ │ ├── index.d.ts │ │ └── lib │ │ │ └── syntax.js │ ├── tsconfig.json │ ├── license │ ├── package.json │ └── readme.md └── micromark-factory-mdx-expression │ ├── .npmrc │ ├── tsconfig.json │ ├── license │ ├── package.json │ ├── readme.md │ └── dev │ └── index.js ├── .editorconfig ├── .gitignore ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── .remarkrc.js ├── license ├── package.json └── readme.md /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /test/types.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `types.d.ts`. 2 | export {} 3 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/dev/lib/types.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `types.d.ts`. 2 | export {} 3 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "references": [] 4 | } 5 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/dev/index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {eventsToAcorn} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/dev/index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {mdxExpression} from './lib/syntax.js' 3 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "references": [{"path": "../micromark-util-events-to-acorn"}] 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "references": [ 4 | {"path": "../micromark-util-events-to-acorn"}, 5 | {"path": "../micromark-factory-mdx-expression"} 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /test/types.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare module 'micromark-util-types' { 4 | // eslint-disable-next-line @typescript-eslint/consistent-type-definitions 5 | interface TokenTypeMap { 6 | expression: 'expression' 7 | expressionChunk: 'expressionChunk' 8 | expressionMarker: 'expressionMarker' 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | *.log 3 | *.map 4 | *.tsbuildinfo 5 | .DS_Store 6 | coverage/ 7 | node_modules/ 8 | packages/*/index.js 9 | packages/*/lib/ 10 | yarn.lock 11 | !packages/micromark-extension-mdx-expression/dev/index.d.ts 12 | !packages/micromark-util-events-to-acorn/dev/index.d.ts 13 | !packages/micromark-util-events-to-acorn/dev/lib/types.d.ts 14 | !test/types.d.ts 15 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declarationMap": true, 6 | "declaration": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": [ 15 | "**/coverage/", 16 | "**/node_modules/", 17 | "packages/*/lib/", 18 | "packages/*/index.js" 19 | ], 20 | "include": [ 21 | "**/*.js", 22 | "packages/micromark-extension-mdx-expression/dev/index.d.ts", 23 | "packages/micromark-util-events-to-acorn/dev/index.d.ts", 24 | "packages/micromark-util-events-to-acorn/dev/lib/types.d.ts", 25 | "test/types.d.ts" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /.remarkrc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {CheckFlag} from 'remark-lint-fenced-code-flag' 3 | * @import {Preset} from 'unified' 4 | */ 5 | 6 | import remarkPresetWooorm from 'remark-preset-wooorm' 7 | import remarkLintFencedCodeFlag, { 8 | checkGithubLinguistFlag 9 | } from 'remark-lint-fenced-code-flag' 10 | import remarkLintMaximumHeadingLength from 'remark-lint-maximum-heading-length' 11 | 12 | /** @type {Preset} */ 13 | const remarkPresetMdx = { 14 | plugins: [ 15 | remarkPresetWooorm, 16 | [remarkLintFencedCodeFlag, check], 17 | [remarkLintMaximumHeadingLength, false] 18 | ] 19 | } 20 | 21 | export default remarkPresetMdx 22 | 23 | /** 24 | * Check according to GitHub Linguist. 25 | * 26 | * @param {string} value 27 | * Language flag to check. 28 | * @returns {string | undefined} 29 | * Whether the flag is valid (`undefined`), 30 | * or a message to warn about (`string`). 31 | * @satisfies {CheckFlag} 32 | */ 33 | function check(value) { 34 | if (value === 'mdx-invalid') return undefined 35 | return checkGithubLinguistFlag(value) 36 | } 37 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 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 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/dev/lib/types.d.ts: -------------------------------------------------------------------------------- 1 | import type {Point as MicromarkPoint} from 'micromark-util-types' 2 | import type {Program} from 'estree' 3 | 4 | /** 5 | * Point. 6 | */ 7 | interface AcornLoc { 8 | /** 9 | * Column. 10 | */ 11 | column: number 12 | /** 13 | * Line. 14 | */ 15 | line: number 16 | } 17 | 18 | export interface AcornError extends Error { 19 | /** 20 | * Location. 21 | */ 22 | loc: AcornLoc 23 | /** 24 | * Index. 25 | */ 26 | pos: number 27 | /** 28 | * Index. 29 | */ 30 | raisedAt: number 31 | } 32 | 33 | /** 34 | * See: . 35 | */ 36 | export interface Collection { 37 | stops: Array 38 | value: string 39 | } 40 | 41 | /** 42 | * Result. 43 | */ 44 | export interface Result { 45 | /** 46 | * Error if unparseable 47 | */ 48 | error: AcornError | undefined 49 | /** 50 | * Program. 51 | */ 52 | estree: Program | undefined 53 | /** 54 | * Whether the error, if there is one, can be swallowed and more JavaScript 55 | * could be valid. 56 | */ 57 | swallow: boolean 58 | } 59 | 60 | /** 61 | * Stop. 62 | */ 63 | export type Stop = [from: number, to: MicromarkPoint] 64 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/dev/index.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import type {parseExpressionAt, parse, Options as AcornOptions} from 'acorn' 3 | import type { 4 | Event, 5 | Point as MicromarkPoint, 6 | TokenType 7 | } from 'micromark-util-types' 8 | 9 | export {eventsToAcorn} from './lib/index.js' 10 | 11 | export type {Options as AcornOptions} from 'acorn' 12 | 13 | /** 14 | * Acorn-like interface. 15 | */ 16 | export interface Acorn { 17 | /** 18 | * Parse an expression. 19 | */ 20 | parseExpressionAt: typeof parseExpressionAt 21 | /** 22 | * Parse a program. 23 | */ 24 | parse: typeof parse 25 | } 26 | 27 | /** 28 | * Configuration. 29 | */ 30 | export interface Options { 31 | /** 32 | * Typically `acorn`, object with `parse` and `parseExpressionAt` fields (required). 33 | */ 34 | acorn: Acorn 35 | /** 36 | * Configuration for `acorn` (optional). 37 | */ 38 | acornOptions?: AcornOptions | null | undefined 39 | /** 40 | * Whether an empty expression is allowed (programs are always allowed to 41 | * be empty) (default: `false`). 42 | */ 43 | allowEmpty?: boolean | null | undefined 44 | /** 45 | * Whether this is a program or expression (default: `false`). 46 | */ 47 | expression?: boolean | null | undefined 48 | /** 49 | * Text to place before events (default: `''`). 50 | */ 51 | prefix?: string | null | undefined 52 | /** 53 | * Place where events start (optional, required if `allowEmpty`). 54 | */ 55 | start?: MicromarkPoint | null | undefined 56 | /** 57 | * Text to place after events (default: `''`). 58 | */ 59 | suffix?: string | null | undefined 60 | /** 61 | * Names of (void) tokens to consider as data; `'lineEnding'` is always 62 | * included (required). 63 | */ 64 | tokenTypes: Array 65 | } 66 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/dev/index.d.ts: -------------------------------------------------------------------------------- 1 | import type {Program} from 'estree' 2 | import type {Acorn, AcornOptions} from 'micromark-util-events-to-acorn' 3 | 4 | export {mdxExpression} from './lib/syntax.js' 5 | 6 | /** 7 | * Configuration (optional). 8 | */ 9 | export interface Options { 10 | /** 11 | * Acorn parser to use (optional). 12 | */ 13 | acorn?: Acorn | null | undefined 14 | /** 15 | * Configuration for acorn (default: `{ecmaVersion: 2024, locations: true, 16 | * sourceType: 'module'}`). 17 | * 18 | * All fields except `locations` can be set. 19 | */ 20 | acornOptions?: AcornOptions | null | undefined 21 | /** 22 | * Whether to add `estree` fields to tokens with results from acorn (default: 23 | * `false`). 24 | */ 25 | addResult?: boolean | null | undefined 26 | /** 27 | * Undocumented option to parse only a spread (used by 28 | * `micromark-extension-mdx-jsx` to parse spread attributes) (default: 29 | * `false`). 30 | */ 31 | spread?: boolean | null | undefined 32 | /** 33 | * Undocumented option to disallow empty attributes (used by 34 | * `micromark-extension-mdx-jsx` to prohobit empty attribute values) 35 | * (default: `false`). 36 | */ 37 | allowEmpty?: boolean | null | undefined 38 | } 39 | 40 | /** 41 | * Augment types. 42 | */ 43 | declare module 'micromark-util-types' { 44 | /** 45 | * Token fields. 46 | */ 47 | interface Token { 48 | estree?: Program 49 | } 50 | 51 | /** 52 | * Token types. 53 | */ 54 | interface TokenTypeMap { 55 | mdxFlowExpression: 'mdxFlowExpression' 56 | mdxFlowExpressionMarker: 'mdxFlowExpressionMarker' 57 | mdxFlowExpressionChunk: 'mdxFlowExpressionChunk' 58 | 59 | mdxTextExpression: 'mdxTextExpression' 60 | mdxTextExpressionMarker: 'mdxTextExpressionMarker' 61 | mdxTextExpressionChunk: 'mdxTextExpressionChunk' 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/micromark/micromark-extension-mdx-expression/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/estree": "^1.0.0", 9 | "devlop": "^1.0.0", 10 | "micromark-factory-space": "^2.0.0", 11 | "micromark-util-character": "^2.0.0", 12 | "micromark-util-events-to-acorn": "^2.0.0", 13 | "micromark-util-symbol": "^2.0.0", 14 | "micromark-util-types": "^2.0.0", 15 | "unist-util-position-from-estree": "^2.0.0", 16 | "vfile-message": "^4.0.0" 17 | }, 18 | "description": "micromark factory to parse MDX expressions (found in JSX attributes, flow, text)", 19 | "exports": { 20 | "development": "./dev/index.js", 21 | "default": "./index.js" 22 | }, 23 | "files": [ 24 | "dev/", 25 | "index.d.ts.map", 26 | "index.d.ts", 27 | "index.js" 28 | ], 29 | "funding": [ 30 | { 31 | "type": "GitHub Sponsors", 32 | "url": "https://github.com/sponsors/unifiedjs" 33 | }, 34 | { 35 | "type": "OpenCollective", 36 | "url": "https://opencollective.com/unified" 37 | } 38 | ], 39 | "keywords": [ 40 | "expression", 41 | "factory", 42 | "mdx", 43 | "micromark" 44 | ], 45 | "license": "MIT", 46 | "name": "micromark-factory-mdx-expression", 47 | "repository": "https://github.com/micromark/micromark-extension-mdx-expression/tree/main/packages/micromark-factory-mdx-expression", 48 | "scripts": { 49 | "build": "micromark-build" 50 | }, 51 | "sideEffects": false, 52 | "typeCoverage": { 53 | "atLeast": 100, 54 | "strict": true 55 | }, 56 | "type": "module", 57 | "version": "2.0.3", 58 | "xo": { 59 | "prettier": true, 60 | "rules": { 61 | "unicorn/no-this-assignment": "off" 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/micromark/micromark-extension-mdx-expression/issues", 4 | "description": "", 5 | "devDependencies": { 6 | "@types/estree": "^1.0.0", 7 | "@types/node": "^22.0.0", 8 | "acorn": "^8.0.0", 9 | "acorn-jsx": "^5.0.0", 10 | "c8": "^10.0.0", 11 | "micromark": "^4.0.0", 12 | "micromark-build": "^2.0.0", 13 | "micromark-util-character": "^2.0.0", 14 | "micromark-util-symbol": "^2.0.0", 15 | "micromark-util-types": "^2.0.0", 16 | "prettier": "^3.0.0", 17 | "remark-cli": "^12.0.0", 18 | "remark-preset-wooorm": "^11.0.0", 19 | "type-coverage": "^2.0.0", 20 | "typescript": "^5.0.0", 21 | "xo": "^0.60.0" 22 | }, 23 | "funding": [ 24 | { 25 | "type": "GitHub Sponsors", 26 | "url": "https://github.com/sponsors/unifiedjs" 27 | }, 28 | { 29 | "type": "OpenCollective", 30 | "url": "https://opencollective.com/unified" 31 | } 32 | ], 33 | "keywords": [], 34 | "license": "MIT", 35 | "name": "micromark-extension-mdx-expression-mono", 36 | "prettier": { 37 | "bracketSpacing": false, 38 | "semi": false, 39 | "singleQuote": true, 40 | "tabWidth": 2, 41 | "trailingComma": "none", 42 | "useTabs": false 43 | }, 44 | "private": true, 45 | "repository": "micromark/micromark-extension-mdx-expression", 46 | "version": "0.0.0", 47 | "scripts": { 48 | "build": "tsc --build --clean && tsc --build && type-coverage && npm run build --workspaces", 49 | "format": "remark --frail --output --quiet -- . && prettier --log-level warn --write -- . && xo --fix", 50 | "test-api-dev": "node --conditions development test/index.js", 51 | "test-api-prod": "node --conditions production test/index.js", 52 | "test-api": "npm run test-api-dev && npm run test-api-prod", 53 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 54 | "test": "npm run build && npm run format && npm run test-coverage" 55 | }, 56 | "typeCoverage": { 57 | "atLeast": 100, 58 | "strict": true 59 | }, 60 | "type": "module", 61 | "workspaces": [ 62 | "packages/micromark-util-events-to-acorn/", 63 | "packages/micromark-factory-mdx-expression/", 64 | "packages/micromark-extension-mdx-expression/" 65 | ], 66 | "xo": { 67 | "prettier": true, 68 | "rules": { 69 | "unicorn/no-this-assignment": "off" 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/micromark/micromark-extension-mdx-expression/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/estree": "^1.0.0", 9 | "@types/unist": "^3.0.0", 10 | "devlop": "^1.0.0", 11 | "estree-util-visit": "^2.0.0", 12 | "micromark-util-symbol": "^2.0.0", 13 | "micromark-util-types": "^2.0.0", 14 | "vfile-message": "^4.0.0" 15 | }, 16 | "description": "micromark utility to try and parse events w/ acorn", 17 | "exports": { 18 | "development": "./dev/index.js", 19 | "default": "./index.js" 20 | }, 21 | "files": [ 22 | "dev/", 23 | "index.d.ts", 24 | "index.js", 25 | "lib/" 26 | ], 27 | "funding": [ 28 | { 29 | "type": "GitHub Sponsors", 30 | "url": "https://github.com/sponsors/unifiedjs" 31 | }, 32 | { 33 | "type": "OpenCollective", 34 | "url": "https://opencollective.com/unified" 35 | } 36 | ], 37 | "keywords": [ 38 | "expression", 39 | "factory", 40 | "micromark", 41 | "mdx" 42 | ], 43 | "license": "MIT", 44 | "name": "micromark-util-events-to-acorn", 45 | "repository": "https://github.com/micromark/micromark-extension-mdx-expression/tree/main/packages/micromark-util-events-to-acorn", 46 | "scripts": { 47 | "build": "micromark-build" 48 | }, 49 | "sideEffects": false, 50 | "typeCoverage": { 51 | "atLeast": 100, 52 | "strict": true 53 | }, 54 | "type": "module", 55 | "version": "2.0.3", 56 | "xo": { 57 | "overrides": [ 58 | { 59 | "files": [ 60 | "**/*.d.ts" 61 | ], 62 | "rules": { 63 | "@typescript-eslint/array-type": [ 64 | "error", 65 | { 66 | "default": "generic" 67 | } 68 | ], 69 | "@typescript-eslint/ban-types": [ 70 | "error", 71 | { 72 | "extendDefaults": true 73 | } 74 | ], 75 | "@typescript-eslint/consistent-type-definitions": [ 76 | "error", 77 | "interface" 78 | ] 79 | } 80 | } 81 | ], 82 | "prettier": true, 83 | "rules": { 84 | "unicorn/prefer-at": "off", 85 | "unicorn/prefer-string-replace-all": "off" 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Titus Wormer (https://wooorm.com)", 3 | "bugs": "https://github.com/micromark/micromark-extension-mdx-expression/issues", 4 | "contributors": [ 5 | "Titus Wormer (https://wooorm.com)" 6 | ], 7 | "dependencies": { 8 | "@types/estree": "^1.0.0", 9 | "devlop": "^1.0.0", 10 | "micromark-factory-mdx-expression": "^2.0.0", 11 | "micromark-factory-space": "^2.0.0", 12 | "micromark-util-character": "^2.0.0", 13 | "micromark-util-events-to-acorn": "^2.0.0", 14 | "micromark-util-symbol": "^2.0.0", 15 | "micromark-util-types": "^2.0.0" 16 | }, 17 | "description": "micromark extension to support MDX or MDX JS expressions", 18 | "exports": { 19 | "development": "./dev/index.js", 20 | "default": "./index.js" 21 | }, 22 | "files": [ 23 | "dev/", 24 | "index.d.ts", 25 | "index.js", 26 | "lib/" 27 | ], 28 | "funding": [ 29 | { 30 | "type": "GitHub Sponsors", 31 | "url": "https://github.com/sponsors/unifiedjs" 32 | }, 33 | { 34 | "type": "OpenCollective", 35 | "url": "https://opencollective.com/unified" 36 | } 37 | ], 38 | "keywords": [ 39 | "ecmascript", 40 | "es", 41 | "expression", 42 | "javascript", 43 | "js", 44 | "markdown", 45 | "mdxjs", 46 | "mdx", 47 | "micromark-extension", 48 | "micromark", 49 | "unified" 50 | ], 51 | "license": "MIT", 52 | "name": "micromark-extension-mdx-expression", 53 | "repository": "https://github.com/micromark/micromark-extension-mdx-expression/tree/main/packages/micromark-extension-mdx-expression", 54 | "scripts": { 55 | "build": "micromark-build" 56 | }, 57 | "sideEffects": false, 58 | "typeCoverage": { 59 | "atLeast": 100, 60 | "strict": true 61 | }, 62 | "type": "module", 63 | "version": "3.0.1", 64 | "xo": { 65 | "overrides": [ 66 | { 67 | "files": [ 68 | "**/*.d.ts" 69 | ], 70 | "rules": { 71 | "@typescript-eslint/array-type": [ 72 | "error", 73 | { 74 | "default": "generic" 75 | } 76 | ], 77 | "@typescript-eslint/ban-types": [ 78 | "error", 79 | { 80 | "extendDefaults": true 81 | } 82 | ], 83 | "@typescript-eslint/consistent-type-definitions": [ 84 | "error", 85 | "interface" 86 | ] 87 | } 88 | } 89 | ], 90 | "prettier": true, 91 | "rules": { 92 | "logical-assignment-operators": "off", 93 | "unicorn/no-this-assignment": "off" 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # micromark-extension-mdx-expression 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Sponsors][sponsors-badge]][collective] 6 | [![Backers][backers-badge]][collective] 7 | [![Chat][chat-badge]][chat] 8 | 9 | Monorepo with a [`micromark`][micromark] extension, 10 | [`micromark-extension-mdx-expression`][micromark-extension-mdx-expression], 11 | and the underlying tools to handle JavaScript in markdown. 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When to use this](#when-to-use-this) 17 | * [Contribute](#contribute) 18 | * [License](#license) 19 | 20 | ## What is this? 21 | 22 | This project contains three packages: 23 | 24 | * [`micromark-extension-mdx-expression`][micromark-extension-mdx-expression] 25 | — extension to support MDX expressions in [`micromark`][micromark] 26 | * [`micromark-factory-mdx-expression`][micromark-factory-mdx-expression] 27 | — subroutine to parse the expressions 28 | * [`micromark-util-events-to-acorn`][micromark-util-events-to-acorn] 29 | — subroutine to parse micromark events with acorn 30 | 31 | ## When to use this 32 | 33 | You might want to use 34 | [`micromark-extension-mdx-expression`][micromark-extension-mdx-expression]. 35 | 36 | The rest is published separately to let 37 | [`micromark-extension-mdx-jsx`][micromark-extension-mdx-jsx] parse expressions 38 | in JSX and to let 39 | [`micromark-extension-mdxjs-esm`][micromark-extension-mdxjs-esm] parse ESM. 40 | You would only need `micromark-factory-mdx-expression` and 41 | `micromark-util-events-to-acorn` if you want to build alternatives to these. 42 | 43 | ## Contribute 44 | 45 | See [`contributing.md` in `micromark/.github`][contributing] for ways to get 46 | started. 47 | See [`support.md`][support] for ways to get help. 48 | 49 | This project has a [code of conduct][coc]. 50 | By interacting with this repository, organization, or community you agree to 51 | abide by its terms. 52 | 53 | ## License 54 | 55 | [MIT][license] © [Titus Wormer][author] 56 | 57 | 58 | 59 | [author]: https://wooorm.com 60 | 61 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 62 | 63 | [build]: https://github.com/micromark/micromark-extension-mdx-expression/actions 64 | 65 | [build-badge]: https://github.com/micromark/micromark-extension-mdx-expression/workflows/main/badge.svg 66 | 67 | [chat]: https://github.com/micromark/micromark/discussions 68 | 69 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 70 | 71 | [coc]: https://github.com/micromark/.github/blob/main/code-of-conduct.md 72 | 73 | [collective]: https://opencollective.com/unified 74 | 75 | [contributing]: https://github.com/micromark/.github/blob/main/contributing.md 76 | 77 | [coverage]: https://codecov.io/github/micromark/micromark-extension-mdx-expression 78 | 79 | [coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-mdx-expression.svg 80 | 81 | [license]: https://github.com/micromark/micromark-extension-mdx-expression/blob/main/license 82 | 83 | [micromark]: https://github.com/micromark/micromark 84 | 85 | [micromark-extension-mdx-expression]: packages/micromark-extension-mdx-expression/ 86 | 87 | [micromark-extension-mdx-jsx]: https://github.com/micromark/micromark-extension-mdx-jsx 88 | 89 | [micromark-extension-mdxjs-esm]: https://github.com/micromark/micromark-extension-mdxjs-esm 90 | 91 | [micromark-factory-mdx-expression]: packages/micromark-factory-mdx-expression/ 92 | 93 | [micromark-util-events-to-acorn]: packages/micromark-util-events-to-acorn/ 94 | 95 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 96 | 97 | [support]: https://github.com/micromark/.github/blob/main/support.md 98 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/readme.md: -------------------------------------------------------------------------------- 1 | # micromark-factory-mdx-expression 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]][opencollective] 8 | [![Backers][backers-badge]][opencollective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [micromark][] factory to parse MDX expressions (found in JSX attributes, flow, 12 | text). 13 | 14 | ## Contents 15 | 16 | * [Install](#install) 17 | * [Use](#use) 18 | * [API](#api) 19 | * [`factoryMdxExpression(…)`](#factorymdxexpression) 20 | * [Types](#types) 21 | * [Compatibility](#compatibility) 22 | * [Security](#security) 23 | * [Contribute](#contribute) 24 | * [License](#license) 25 | 26 | ## Install 27 | 28 | This package is [ESM only][esm]. 29 | In Node.js (version 16+), install with [npm][]: 30 | 31 | ```sh 32 | npm install micromark-factory-mdx-expression 33 | ``` 34 | 35 | In Deno with [`esm.sh`][esmsh]: 36 | 37 | ```js 38 | import {factoryMdxExpression} from 'https://esm.sh/micromark-factory-mdx-expression@2' 39 | ``` 40 | 41 | In browsers with [`esm.sh`][esmsh]: 42 | 43 | ```html 44 | 47 | ``` 48 | 49 | ## Use 50 | 51 | ```js 52 | import {ok as assert} from 'devlop' 53 | import {factoryMdxExpression} from 'micromark-factory-mdx-expression' 54 | import {codes} from 'micromark-util-symbol' 55 | 56 | // A micromark tokenizer that uses the factory: 57 | /** @type {Tokenizer} */ 58 | function tokenizeFlowExpression(effects, ok, nok) { 59 | return start 60 | 61 | // … 62 | 63 | /** @type {State} */ 64 | function start(code) { 65 | assert(code === codes.leftCurlyBrace, 'expected `{`') 66 | return factoryMdxExpression.call( 67 | self, 68 | effects, 69 | factorySpace(effects, after, types.whitespace), 70 | 'mdxFlowExpression', 71 | 'mdxFlowExpressionMarker', 72 | 'mdxFlowExpressionChunk', 73 | acorn, 74 | acornOptions, 75 | addResult, 76 | spread, 77 | allowEmpty 78 | )(code) 79 | } 80 | 81 | // … 82 | } 83 | ``` 84 | 85 | ## API 86 | 87 | This module exports the identifier 88 | [`factoryMdxExpression`][api-factory-mdx-expression]. 89 | There is no default export. 90 | 91 | The export map supports the [`development` condition][development]. 92 | Run `node --conditions development module.js` to get instrumented dev code. 93 | Without this condition, production code is loaded. 94 | 95 | ### `factoryMdxExpression(…)` 96 | 97 | ###### Parameters 98 | 99 | * `effects` (`Effects`) 100 | — context 101 | * `ok` (`State`) 102 | — state switched to when successful 103 | * `type` (`string`) 104 | — token type for whole (`{}`) 105 | * `markerType` (`string`) 106 | — token type for the markers (`{`, `}`) 107 | * `chunkType` (`string`) 108 | — token type for the value (`1`) 109 | * `acorn` (`Acorn`) 110 | — object with `acorn.parse` and `acorn.parseExpressionAt` 111 | * `acornOptions` ([`AcornOptions`][acorn-options]) 112 | — configuration for acorn 113 | * `boolean` (`addResult`, default: `false`) 114 | — add `estree` to token 115 | * `boolean` (`spread`, default: `false`) 116 | — support a spread (`{...a}`) only 117 | * `boolean` (`allowEmpty`, default: `false`) 118 | — support an empty expression 119 | * `boolean` (`allowLazy`, default: `false`) 120 | — support lazy continuation of an expression 121 | 122 | ###### Returns 123 | 124 | `State`. 125 | 126 | ## Types 127 | 128 | This package is fully typed with [TypeScript][]. 129 | It exports the additional types [`Acorn`][acorn] and 130 | [`AcornOptions`][acorn-options]. 131 | 132 | ## Compatibility 133 | 134 | Projects maintained by the unified collective are compatible with maintained 135 | versions of Node.js. 136 | 137 | When we cut a new major release, we drop support for unmaintained versions of 138 | Node. 139 | This means we try to keep the current release line, 140 | `micromark-factory-mdx-expression@^2`, compatible with Node.js 16. 141 | 142 | This package works with `micromark` version `3` and later. 143 | 144 | ## Security 145 | 146 | This package is safe. 147 | 148 | ## Contribute 149 | 150 | See [`contributing.md`][contributing] in [`micromark/.github`][health] for ways 151 | to get started. 152 | See [`support.md`][support] for ways to get help. 153 | 154 | This project has a [code of conduct][coc]. 155 | By interacting with this repository, organisation, or community you agree to 156 | abide by its terms. 157 | 158 | ## License 159 | 160 | [MIT][license] © [Titus Wormer][author] 161 | 162 | 163 | 164 | [acorn]: https://github.com/acornjs/acorn 165 | 166 | [acorn-options]: https://github.com/acornjs/acorn/blob/96c721dbf89d0ccc3a8c7f39e69ef2a6a3c04dfa/acorn/dist/acorn.d.ts#L16 167 | 168 | [api-factory-mdx-expression]: #micromark-factory-mdx-expression 169 | 170 | [author]: https://wooorm.com 171 | 172 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 173 | 174 | [build]: https://github.com/micromark/micromark-extension-mdx-expression/actions 175 | 176 | [build-badge]: https://github.com/micromark/micromark-extension-mdx-expression/workflows/main/badge.svg 177 | 178 | [chat]: https://github.com/micromark/micromark/discussions 179 | 180 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 181 | 182 | [coc]: https://github.com/micromark/.github/blob/main/code-of-conduct.md 183 | 184 | [contributing]: https://github.com/micromark/.github/blob/main/contributing.md 185 | 186 | [coverage]: https://codecov.io/github/micromark/micromark-extension-mdx-expression 187 | 188 | [coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-mdx-expression.svg 189 | 190 | [development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions 191 | 192 | [downloads]: https://www.npmjs.com/package/micromark-factory-mdx-expression 193 | 194 | [downloads-badge]: https://img.shields.io/npm/dm/micromark-factory-mdx-expression.svg 195 | 196 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 197 | 198 | [esmsh]: https://esm.sh 199 | 200 | [health]: https://github.com/micromark/.github 201 | 202 | [license]: https://github.com/micromark/micromark-extension-mdx-expression/blob/main/license 203 | 204 | [micromark]: https://github.com/micromark/micromark 205 | 206 | [npm]: https://docs.npmjs.com/cli/install 207 | 208 | [opencollective]: https://opencollective.com/unified 209 | 210 | [size]: https://bundlejs.com/?q=micromark-factory-mdx-expression 211 | 212 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=micromark-factory-mdx-expression 213 | 214 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 215 | 216 | [support]: https://github.com/micromark/.github/blob/main/support.md 217 | 218 | [typescript]: https://www.typescriptlang.org 219 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/dev/lib/syntax.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Options} from 'micromark-extension-mdx-expression' 3 | * @import {AcornOptions} from 'micromark-util-events-to-acorn' 4 | * @import {Extension, State, TokenizeContext, Tokenizer} from 'micromark-util-types' 5 | */ 6 | 7 | import {ok as assert} from 'devlop' 8 | import {factoryMdxExpression} from 'micromark-factory-mdx-expression' 9 | import {factorySpace} from 'micromark-factory-space' 10 | import {markdownLineEnding, markdownSpace} from 'micromark-util-character' 11 | import {codes, types} from 'micromark-util-symbol' 12 | 13 | /** 14 | * Create an extension for `micromark` to enable MDX expression syntax. 15 | * 16 | * @param {Options | null | undefined} [options] 17 | * Configuration (optional). 18 | * @returns {Extension} 19 | * Extension for `micromark` that can be passed in `extensions` to enable MDX 20 | * expression syntax. 21 | */ 22 | export function mdxExpression(options) { 23 | const options_ = options || {} 24 | const addResult = options_.addResult 25 | const acorn = options_.acorn 26 | // Hidden: `micromark-extension-mdx-jsx` supports expressions in tags, 27 | // and one of them is only “spread” elements. 28 | // It also has expressions that are not allowed to be empty (``). 29 | // Instead of duplicating code there, this are two small hidden feature here 30 | // to test that behavior. 31 | const spread = options_.spread 32 | let allowEmpty = options_.allowEmpty 33 | /** @type {AcornOptions} */ 34 | let acornOptions 35 | 36 | if (allowEmpty === null || allowEmpty === undefined) { 37 | allowEmpty = true 38 | } 39 | 40 | if (acorn) { 41 | if (!acorn.parseExpressionAt) { 42 | throw new Error( 43 | 'Expected a proper `acorn` instance passed in as `options.acorn`' 44 | ) 45 | } 46 | 47 | acornOptions = Object.assign( 48 | {ecmaVersion: 2024, sourceType: 'module'}, 49 | options_.acornOptions 50 | ) 51 | } else if (options_.acornOptions || options_.addResult) { 52 | throw new Error('Expected an `acorn` instance passed in as `options.acorn`') 53 | } 54 | 55 | return { 56 | flow: { 57 | [codes.leftCurlyBrace]: { 58 | name: 'mdxFlowExpression', 59 | tokenize: tokenizeFlowExpression, 60 | concrete: true 61 | } 62 | }, 63 | text: { 64 | [codes.leftCurlyBrace]: { 65 | name: 'mdxTextExpression', 66 | tokenize: tokenizeTextExpression 67 | } 68 | } 69 | } 70 | 71 | /** 72 | * MDX expression (flow). 73 | * 74 | * ```markdown 75 | * > | {Math.PI} 76 | * ^^^^^^^^^ 77 | * ``` 78 | * 79 | * @this {TokenizeContext} 80 | * @type {Tokenizer} 81 | */ 82 | function tokenizeFlowExpression(effects, ok, nok) { 83 | const self = this 84 | 85 | return start 86 | 87 | /** 88 | * Start of an MDX expression (flow). 89 | * 90 | * ```markdown 91 | * > | {Math.PI} 92 | * ^ 93 | * ``` 94 | * 95 | * @type {State} 96 | */ 97 | function start(code) { 98 | // To do: in `markdown-rs`, constructs need to parse the indent themselves. 99 | // This should also be introduced in `micromark-js`. 100 | assert(code === codes.leftCurlyBrace, 'expected `{`') 101 | return before(code) 102 | } 103 | 104 | /** 105 | * After optional whitespace, before expression. 106 | * 107 | * ```markdown 108 | * > | {Math.PI} 109 | * ^ 110 | * ``` 111 | * 112 | * @type {State} 113 | */ 114 | function before(code) { 115 | return factoryMdxExpression.call( 116 | self, 117 | effects, 118 | after, 119 | 'mdxFlowExpression', 120 | 'mdxFlowExpressionMarker', 121 | 'mdxFlowExpressionChunk', 122 | acorn, 123 | acornOptions, 124 | addResult, 125 | spread, 126 | allowEmpty 127 | )(code) 128 | } 129 | 130 | /** 131 | * After expression. 132 | * 133 | * ```markdown 134 | * > | {Math.PI} 135 | * ^ 136 | * ``` 137 | * 138 | * @type {State} 139 | */ 140 | function after(code) { 141 | return markdownSpace(code) 142 | ? factorySpace(effects, end, types.whitespace)(code) 143 | : end(code) 144 | } 145 | 146 | /** 147 | * After expression, after optional whitespace. 148 | * 149 | * ```markdown 150 | * > | {Math.PI}␠␊ 151 | * ^ 152 | * ``` 153 | * 154 | * @type {State} 155 | */ 156 | function end(code) { 157 | // We want to allow tags directly after expressions. 158 | // 159 | // This case is useful: 160 | // 161 | // ```mdx 162 | // {b} 163 | // ``` 164 | // 165 | // This case is not (very?) useful: 166 | // 167 | // ```mdx 168 | // {a} 169 | // ``` 170 | // 171 | // …but it would be tougher than needed to disallow. 172 | // 173 | // To allow that, here we call the flow construct of 174 | // `micromark-extension-mdx-jsx`, and there we call this one. 175 | // 176 | // It would introduce a cyclical interdependency if we test JSX and 177 | // expressions here. 178 | // Because the JSX extension already uses parts of this monorepo, we 179 | // instead test it there. 180 | const lessThanValue = self.parser.constructs.flow[codes.lessThan] 181 | const constructs = Array.isArray(lessThanValue) 182 | ? lessThanValue 183 | : /* c8 ignore next 3 -- always a list when normalized. */ 184 | lessThanValue 185 | ? [lessThanValue] 186 | : [] 187 | const jsxTag = constructs.find(function (d) { 188 | return d.name === 'mdxJsxFlowTag' 189 | }) 190 | 191 | /* c8 ignore next 3 -- this is tested in `micromark-extension-mdx-jsx` */ 192 | if (code === codes.lessThan && jsxTag) { 193 | return effects.attempt(jsxTag, end, nok)(code) 194 | } 195 | 196 | return code === codes.eof || markdownLineEnding(code) 197 | ? ok(code) 198 | : nok(code) 199 | } 200 | } 201 | 202 | /** 203 | * MDX expression (text). 204 | * 205 | * ```markdown 206 | * > | a {Math.PI} c. 207 | * ^^^^^^^^^ 208 | * ``` 209 | * 210 | * @this {TokenizeContext} 211 | * @type {Tokenizer} 212 | */ 213 | function tokenizeTextExpression(effects, ok) { 214 | const self = this 215 | 216 | return start 217 | 218 | /** 219 | * Start of an MDX expression (text). 220 | * 221 | * ```markdown 222 | * > | a {Math.PI} c. 223 | * ^ 224 | * ``` 225 | * 226 | * 227 | * @type {State} 228 | */ 229 | function start(code) { 230 | assert(code === codes.leftCurlyBrace, 'expected `{`') 231 | return factoryMdxExpression.call( 232 | self, 233 | effects, 234 | ok, 235 | 'mdxTextExpression', 236 | 'mdxTextExpressionMarker', 237 | 'mdxTextExpressionChunk', 238 | acorn, 239 | acornOptions, 240 | addResult, 241 | spread, 242 | allowEmpty, 243 | true 244 | )(code) 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/readme.md: -------------------------------------------------------------------------------- 1 | # micromark-util-events-to-acorn 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]][opencollective] 8 | [![Backers][backers-badge]][opencollective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [micromark][] utility to try and parse events with acorn. 12 | 13 | ## Contents 14 | 15 | * [Install](#install) 16 | * [Use](#use) 17 | * [API](#api) 18 | * [`eventsToAcorn(events, options)`](#eventstoacornevents-options) 19 | * [`Options`](#options) 20 | * [`Result`](#result) 21 | * [Types](#types) 22 | * [Compatibility](#compatibility) 23 | * [Security](#security) 24 | * [Contribute](#contribute) 25 | * [License](#license) 26 | 27 | ## Install 28 | 29 | This package is [ESM only][esm]. 30 | In Node.js (version 16+), install with [npm][]: 31 | 32 | ```sh 33 | npm install micromark-util-events-to-acorn 34 | ``` 35 | 36 | In Deno with [`esm.sh`][esmsh]: 37 | 38 | ```js 39 | import {eventsToAcorn} from 'https://esm.sh/micromark-util-events-to-acorn@2' 40 | ``` 41 | 42 | In browsers with [`esm.sh`][esmsh]: 43 | 44 | ```html 45 | 48 | ``` 49 | 50 | ## Use 51 | 52 | ```js 53 | import {eventsToAcorn} from 'micromark-util-events-to-acorn' 54 | 55 | // A factory that uses the utility: 56 | /** @type {Tokenizer} */ 57 | function factoryMdxExpression(effects, ok, nok) { 58 | return start 59 | 60 | // … 61 | 62 | // … 63 | 64 | // Gnostic mode: parse w/ acorn. 65 | const result = eventsToAcorn(this.events.slice(eventStart), { 66 | acorn, 67 | acornOptions, 68 | start: pointStart, 69 | expression: true, 70 | allowEmpty, 71 | prefix: spread ? '({' : '', 72 | suffix: spread ? '})' : '' 73 | }) 74 | 75 | // … 76 | 77 | // … 78 | } 79 | ``` 80 | 81 | ## API 82 | 83 | This module exports the identifier [`eventsToAcorn`][api-events-to-acorn]. 84 | There is no default export. 85 | 86 | The export map supports the [`development` condition][development]. 87 | Run `node --conditions development module.js` to get instrumented dev code. 88 | Without this condition, production code is loaded. 89 | 90 | ### `eventsToAcorn(events, options)` 91 | 92 | ###### Parameters 93 | 94 | * `events` (`Array`) 95 | — events 96 | * `options` ([`Options`][api-options]) 97 | — configuration (required) 98 | 99 | ###### Returns 100 | 101 | Result ([`Result`][api-result]). 102 | 103 | ### `Options` 104 | 105 | Configuration (TypeScript type). 106 | 107 | ###### Fields 108 | 109 | * `acorn` ([`Acorn`][acorn], required) 110 | — typically `acorn`, object with `parse` and `parseExpressionAt` fields 111 | * `tokenTypes` (`Array`], required) 112 | — names of (void) tokens to consider as data; `'lineEnding'` is always 113 | included 114 | * `acornOptions` ([`AcornOptions`][acorn-options], optional) 115 | — configuration for `acorn` 116 | * `start` (`Point`, optional, required if `allowEmpty`) 117 | — place where events start 118 | * `prefix` (`string`, default: `''`) 119 | — text to place before events 120 | * `suffix` (`string`, default: `''`) 121 | — text to place after events 122 | * `expression` (`boolean`, default: `false`) 123 | — whether this is a program or expression 124 | * `allowEmpty` (`boolean`, default: `false`) 125 | — whether an empty expression is allowed (programs are always allowed to be 126 | empty) 127 | 128 | ### `Result` 129 | 130 | Result (TypeScript type). 131 | 132 | ###### Fields 133 | 134 | * `estree` ([`Program`][program] or `undefined`) 135 | — Program 136 | * `error` (`Error` or `undefined`) 137 | — error if unparseable 138 | * `swallow` (`boolean`) 139 | — whether the error, if there is one, can be swallowed and more JavaScript 140 | could be valid 141 | 142 | ## Types 143 | 144 | This package is fully typed with [TypeScript][]. 145 | It exports the additional types [`Acorn`][acorn], 146 | [`AcornOptions`][acorn-options], [`Options`][api-options], and 147 | [`Result`][api-result]. 148 | 149 | ## Compatibility 150 | 151 | Projects maintained by the unified collective are compatible with maintained 152 | versions of Node.js. 153 | 154 | When we cut a new major release, we drop support for unmaintained versions of 155 | Node. 156 | This means we try to keep the current release line, 157 | `micromark-util-events-to-acorn@^2`, compatible with Node.js 16. 158 | 159 | This package works with `micromark` version `3` and later. 160 | 161 | ## Security 162 | 163 | This package is safe. 164 | 165 | ## Contribute 166 | 167 | See [`contributing.md`][contributing] in [`micromark/.github`][health] for ways 168 | to get started. 169 | See [`support.md`][support] for ways to get help. 170 | 171 | This project has a [code of conduct][coc]. 172 | By interacting with this repository, organisation, or community you agree to 173 | abide by its terms. 174 | 175 | ## License 176 | 177 | [MIT][license] © [Titus Wormer][author] 178 | 179 | 180 | 181 | [acorn]: https://github.com/acornjs/acorn 182 | 183 | [acorn-options]: https://github.com/acornjs/acorn/blob/96c721dbf89d0ccc3a8c7f39e69ef2a6a3c04dfa/acorn/dist/acorn.d.ts#L16 184 | 185 | [api-events-to-acorn]: #eventstoacornevents-options 186 | 187 | [api-options]: #options 188 | 189 | [api-result]: #result 190 | 191 | [author]: https://wooorm.com 192 | 193 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 194 | 195 | [build]: https://github.com/micromark/micromark-extension-mdx-expression/actions 196 | 197 | [build-badge]: https://github.com/micromark/micromark-extension-mdx-expression/workflows/main/badge.svg 198 | 199 | [chat]: https://github.com/micromark/micromark/discussions 200 | 201 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 202 | 203 | [coc]: https://github.com/micromark/.github/blob/main/code-of-conduct.md 204 | 205 | [contributing]: https://github.com/micromark/.github/blob/main/contributing.md 206 | 207 | [coverage]: https://codecov.io/github/micromark/micromark-extension-mdx-expression 208 | 209 | [coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-mdx-expression.svg 210 | 211 | [development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions 212 | 213 | [downloads]: https://www.npmjs.com/package/micromark-util-events-to-acorn 214 | 215 | [downloads-badge]: https://img.shields.io/npm/dm/micromark-util-events-to-acorn.svg 216 | 217 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 218 | 219 | [esmsh]: https://esm.sh 220 | 221 | [health]: https://github.com/micromark/.github 222 | 223 | [license]: https://github.com/micromark/micromark-extension-mdx-expression/blob/main/license 224 | 225 | [micromark]: https://github.com/micromark/micromark 226 | 227 | [npm]: https://docs.npmjs.com/cli/install 228 | 229 | [opencollective]: https://opencollective.com/unified 230 | 231 | [program]: https://github.com/estree/estree/blob/master/es2015.md#programs 232 | 233 | [size]: https://bundlejs.com/?q=micromark-util-events-to-acorn 234 | 235 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=micromark-util-events-to-acorn 236 | 237 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 238 | 239 | [support]: https://github.com/micromark/.github/blob/main/support.md 240 | 241 | [typescript]: https://www.typescriptlang.org 242 | -------------------------------------------------------------------------------- /packages/micromark-factory-mdx-expression/dev/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Program} from 'estree' 3 | * @import {Acorn, AcornOptions} from 'micromark-util-events-to-acorn' 4 | * @import {Effects, Point, State, TokenType, TokenizeContext} from 'micromark-util-types' 5 | */ 6 | 7 | /** 8 | * @typedef MdxSignalOk 9 | * Good result. 10 | * @property {'ok'} type 11 | * Type. 12 | * @property {Program | undefined} estree 13 | * Value. 14 | * 15 | * @typedef MdxSignalNok 16 | * Bad result. 17 | * @property {'nok'} type 18 | * Type. 19 | * @property {VFileMessage} message 20 | * Value. 21 | * 22 | * @typedef {MdxSignalNok | MdxSignalOk} MdxSignal 23 | */ 24 | 25 | import {ok as assert} from 'devlop' 26 | import {factorySpace} from 'micromark-factory-space' 27 | import {markdownLineEnding, markdownSpace} from 'micromark-util-character' 28 | import {eventsToAcorn} from 'micromark-util-events-to-acorn' 29 | import {codes, types} from 'micromark-util-symbol' 30 | import {positionFromEstree} from 'unist-util-position-from-estree' 31 | import {VFileMessage} from 'vfile-message' 32 | 33 | // Tab-size to eat has to be the same as what we serialize as. 34 | // While in some places in markdown that’s 4, in JS it’s more common as 2. 35 | // Which is what’s also in `mdast-util-mdx-jsx`: 36 | // 37 | const indentSize = 2 38 | 39 | const trouble = 40 | 'https://github.com/micromark/micromark-extension-mdx-expression/tree/main/packages/micromark-extension-mdx-expression' 41 | 42 | const unexpectedEndOfFileHash = 43 | '#unexpected-end-of-file-in-expression-expected-a-corresponding-closing-brace-for-' 44 | const unexpectedLazyHash = 45 | '#unexpected-lazy-line-in-expression-in-container-expected-line-to-be-prefixed' 46 | const nonSpreadHash = 47 | '#unexpected-type-in-code-expected-an-object-spread-spread' 48 | const spreadExtraHash = 49 | '#unexpected-extra-content-in-spread-only-a-single-spread-is-supported' 50 | const acornHash = '#could-not-parse-expression-with-acorn' 51 | 52 | /** 53 | * @this {TokenizeContext} 54 | * Context. 55 | * @param {Effects} effects 56 | * Context. 57 | * @param {State} ok 58 | * State switched to when successful 59 | * @param {TokenType} type 60 | * Token type for whole (`{}`). 61 | * @param {TokenType} markerType 62 | * Token type for the markers (`{`, `}`). 63 | * @param {TokenType} chunkType 64 | * Token type for the value (`1`). 65 | * @param {Acorn | null | undefined} [acorn] 66 | * Object with `acorn.parse` and `acorn.parseExpressionAt`. 67 | * @param {AcornOptions | null | undefined} [acornOptions] 68 | * Configuration for acorn. 69 | * @param {boolean | null | undefined} [addResult=false] 70 | * Add `estree` to token (default: `false`). 71 | * @param {boolean | null | undefined} [spread=false] 72 | * Support a spread (`{...a}`) only (default: `false`). 73 | * @param {boolean | null | undefined} [allowEmpty=false] 74 | * Support an empty expression (default: `false`). 75 | * @param {boolean | null | undefined} [allowLazy=false] 76 | * Support lazy continuation of an expression (default: `false`). 77 | * @returns {State} 78 | */ 79 | // eslint-disable-next-line max-params 80 | export function factoryMdxExpression( 81 | effects, 82 | ok, 83 | type, 84 | markerType, 85 | chunkType, 86 | acorn, 87 | acornOptions, 88 | addResult, 89 | spread, 90 | allowEmpty, 91 | allowLazy 92 | ) { 93 | const self = this 94 | const eventStart = this.events.length + 3 // Add main and marker token 95 | let size = 0 96 | /** @type {Point} */ 97 | let pointStart 98 | /** @type {Error} */ 99 | let lastCrash 100 | 101 | return start 102 | 103 | /** 104 | * Start of an MDX expression. 105 | * 106 | * ```markdown 107 | * > | a {Math.PI} c 108 | * ^ 109 | * ``` 110 | * 111 | * @type {State} 112 | */ 113 | function start(code) { 114 | assert(code === codes.leftCurlyBrace, 'expected `{`') 115 | effects.enter(type) 116 | effects.enter(markerType) 117 | effects.consume(code) 118 | effects.exit(markerType) 119 | pointStart = self.now() 120 | return before 121 | } 122 | 123 | /** 124 | * Before data. 125 | * 126 | * ```markdown 127 | * > | a {Math.PI} c 128 | * ^ 129 | * ``` 130 | * 131 | * @type {State} 132 | */ 133 | function before(code) { 134 | if (code === codes.eof) { 135 | if (lastCrash) throw lastCrash 136 | 137 | const error = new VFileMessage( 138 | 'Unexpected end of file in expression, expected a corresponding closing brace for `{`', 139 | { 140 | place: self.now(), 141 | ruleId: 'unexpected-eof', 142 | source: 'micromark-extension-mdx-expression' 143 | } 144 | ) 145 | error.url = trouble + unexpectedEndOfFileHash 146 | throw error 147 | } 148 | 149 | if (markdownLineEnding(code)) { 150 | effects.enter(types.lineEnding) 151 | effects.consume(code) 152 | effects.exit(types.lineEnding) 153 | return eolAfter 154 | } 155 | 156 | if (code === codes.rightCurlyBrace && size === 0) { 157 | /** @type {MdxSignal} */ 158 | const next = acorn 159 | ? mdxExpressionParse.call( 160 | self, 161 | acorn, 162 | acornOptions, 163 | chunkType, 164 | eventStart, 165 | pointStart, 166 | allowEmpty || false, 167 | spread || false 168 | ) 169 | : {type: 'ok', estree: undefined} 170 | 171 | if (next.type === 'ok') { 172 | effects.enter(markerType) 173 | effects.consume(code) 174 | effects.exit(markerType) 175 | const token = effects.exit(type) 176 | 177 | if (addResult && next.estree) { 178 | Object.assign(token, {estree: next.estree}) 179 | } 180 | 181 | return ok 182 | } 183 | 184 | lastCrash = next.message 185 | effects.enter(chunkType) 186 | effects.consume(code) 187 | return inside 188 | } 189 | 190 | effects.enter(chunkType) 191 | return inside(code) 192 | } 193 | 194 | /** 195 | * In data. 196 | * 197 | * ```markdown 198 | * > | a {Math.PI} c 199 | * ^ 200 | * ``` 201 | * 202 | * @type {State} 203 | */ 204 | function inside(code) { 205 | if ( 206 | (code === codes.rightCurlyBrace && size === 0) || 207 | code === codes.eof || 208 | markdownLineEnding(code) 209 | ) { 210 | effects.exit(chunkType) 211 | return before(code) 212 | } 213 | 214 | // Don’t count if gnostic. 215 | if (code === codes.leftCurlyBrace && !acorn) { 216 | size += 1 217 | } else if (code === codes.rightCurlyBrace) { 218 | size -= 1 219 | } 220 | 221 | effects.consume(code) 222 | return inside 223 | } 224 | 225 | /** 226 | * After eol. 227 | * 228 | * ```markdown 229 | * | a {b + 230 | * > | c} d 231 | * ^ 232 | * ``` 233 | * 234 | * @type {State} 235 | */ 236 | function eolAfter(code) { 237 | const now = self.now() 238 | 239 | // Lazy continuation in a flow expression (or flow tag) is a syntax error. 240 | if ( 241 | now.line !== pointStart.line && 242 | !allowLazy && 243 | self.parser.lazy[now.line] 244 | ) { 245 | const error = new VFileMessage( 246 | 'Unexpected lazy line in expression in container, expected line to be prefixed with `>` when in a block quote, whitespace when in a list, etc', 247 | { 248 | place: self.now(), 249 | ruleId: 'unexpected-lazy', 250 | source: 'micromark-extension-mdx-expression' 251 | } 252 | ) 253 | error.url = trouble + unexpectedLazyHash 254 | throw error 255 | } 256 | 257 | // Note: `markdown-rs` uses `4`, but we use `2`. 258 | // 259 | // Idea: investigate if we’d need to use more complex stripping. 260 | // Take this example: 261 | // 262 | // ```markdown 263 | // > aaa d 265 | // > `} /> eee 266 | // ``` 267 | // 268 | // Currently, the “paragraph” starts at `> | aaa`, so for the next line 269 | // here we split it into `>␠|␠␠|␠␠␠d` (prefix, this indent here, 270 | // expression data). 271 | if (markdownSpace(code)) { 272 | return factorySpace( 273 | effects, 274 | before, 275 | types.linePrefix, 276 | indentSize + 1 277 | )(code) 278 | } 279 | 280 | return before(code) 281 | } 282 | } 283 | 284 | /** 285 | * Mix of `markdown-rs`’s `parse_expression` and `MdxExpressionParse` 286 | * functionality, to wrap our `eventsToAcorn`. 287 | * 288 | * In the future, the plan is to realise the rust way, which allows arbitrary 289 | * parsers. 290 | * 291 | * @this {TokenizeContext} 292 | * @param {Acorn} acorn 293 | * @param {AcornOptions | null | undefined} acornOptions 294 | * @param {TokenType} chunkType 295 | * @param {number} eventStart 296 | * @param {Point} pointStart 297 | * @param {boolean} allowEmpty 298 | * @param {boolean} spread 299 | * @returns {MdxSignal} 300 | */ 301 | // eslint-disable-next-line max-params 302 | function mdxExpressionParse( 303 | acorn, 304 | acornOptions, 305 | chunkType, 306 | eventStart, 307 | pointStart, 308 | allowEmpty, 309 | spread 310 | ) { 311 | // Gnostic mode: parse w/ acorn. 312 | const result = eventsToAcorn(this.events.slice(eventStart), { 313 | acorn, 314 | tokenTypes: [chunkType], 315 | acornOptions, 316 | start: pointStart, 317 | expression: true, 318 | allowEmpty, 319 | prefix: spread ? '({' : '', 320 | suffix: spread ? '})' : '' 321 | }) 322 | const estree = result.estree 323 | 324 | // Get the spread value. 325 | if (spread && estree) { 326 | // Should always be the case as we wrap in `d={}` 327 | assert(estree.type === 'Program', 'expected program') 328 | const head = estree.body[0] 329 | assert(head, 'expected body') 330 | 331 | if ( 332 | head.type !== 'ExpressionStatement' || 333 | head.expression.type !== 'ObjectExpression' 334 | ) { 335 | const place = positionFromEstree(head) 336 | assert(place, 'expected position') 337 | const error = new VFileMessage( 338 | 'Unexpected `' + 339 | head.type + 340 | '` in code: expected an object spread (`{...spread}`)', 341 | { 342 | place: place.start, 343 | ruleId: 'non-spread', 344 | source: 'micromark-extension-mdx-expression' 345 | } 346 | ) 347 | error.url = trouble + nonSpreadHash 348 | throw error 349 | } 350 | 351 | if (head.expression.properties[1]) { 352 | const place = positionFromEstree(head.expression.properties[1]) 353 | assert(place, 'expected position') 354 | const error = new VFileMessage( 355 | 'Unexpected extra content in spread: only a single spread is supported', 356 | { 357 | place: place.start, 358 | ruleId: 'spread-extra', 359 | source: 'micromark-extension-mdx-expression' 360 | } 361 | ) 362 | error.url = trouble + spreadExtraHash 363 | throw error 364 | } 365 | 366 | if ( 367 | head.expression.properties[0] && 368 | head.expression.properties[0].type !== 'SpreadElement' 369 | ) { 370 | const place = positionFromEstree(head.expression.properties[0]) 371 | assert(place, 'expected position') 372 | const error = new VFileMessage( 373 | 'Unexpected `' + 374 | head.expression.properties[0].type + 375 | '` in code: only spread elements are supported', 376 | { 377 | place: place.start, 378 | ruleId: 'non-spread', 379 | source: 'micromark-extension-mdx-expression' 380 | } 381 | ) 382 | error.url = trouble + nonSpreadHash 383 | throw error 384 | } 385 | } 386 | 387 | if (result.error) { 388 | const error = new VFileMessage('Could not parse expression with acorn', { 389 | cause: result.error, 390 | place: { 391 | line: result.error.loc.line, 392 | column: result.error.loc.column + 1, 393 | offset: result.error.pos 394 | }, 395 | ruleId: 'acorn', 396 | source: 'micromark-extension-mdx-expression' 397 | }) 398 | error.url = trouble + acornHash 399 | 400 | return {type: 'nok', message: error} 401 | } 402 | 403 | return {type: 'ok', estree} 404 | } 405 | -------------------------------------------------------------------------------- /packages/micromark-util-events-to-acorn/dev/lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Comment, Node as AcornNode, Token} from 'acorn' 3 | * @import {Node as EstreeNode, Program} from 'estree' 4 | * @import {Chunk, Event, Point as MicromarkPoint, TokenType} from 'micromark-util-types' 5 | * @import {Point as UnistPoint} from 'unist' 6 | * 7 | * @import {AcornOptions, Options} from 'micromark-util-events-to-acorn' 8 | * @import {AcornError, Collection, Result, Stop} from './types.js' 9 | */ 10 | 11 | import {ok as assert} from 'devlop' 12 | import {visit} from 'estree-util-visit' 13 | import {codes, types, values} from 'micromark-util-symbol' 14 | import {VFileMessage} from 'vfile-message' 15 | 16 | /** 17 | * Parse a list of micromark events with acorn. 18 | * 19 | * @param {Array} events 20 | * Events. 21 | * @param {Options} options 22 | * Configuration (required). 23 | * @returns {Result} 24 | * Result. 25 | */ 26 | // eslint-disable-next-line complexity 27 | export function eventsToAcorn(events, options) { 28 | const prefix = options.prefix || '' 29 | const suffix = options.suffix || '' 30 | const acornOptions = Object.assign({}, options.acornOptions) 31 | /** @type {Array} */ 32 | const comments = [] 33 | /** @type {Array} */ 34 | const tokens = [] 35 | const onComment = acornOptions.onComment 36 | const onToken = acornOptions.onToken 37 | let swallow = false 38 | /** @type {AcornNode | undefined} */ 39 | let estree 40 | /** @type {AcornError | undefined} */ 41 | let exception 42 | /** @type {AcornOptions} */ 43 | const acornConfig = Object.assign({}, acornOptions, { 44 | onComment: comments, 45 | preserveParens: true 46 | }) 47 | 48 | if (onToken) { 49 | acornConfig.onToken = tokens 50 | } 51 | 52 | const collection = collect(events, options.tokenTypes) 53 | 54 | const source = collection.value 55 | 56 | const value = prefix + source + suffix 57 | const isEmptyExpression = options.expression && empty(source) 58 | 59 | if (isEmptyExpression && !options.allowEmpty) { 60 | throw new VFileMessage('Unexpected empty expression', { 61 | place: parseOffsetToUnistPoint(0), 62 | ruleId: 'unexpected-empty-expression', 63 | source: 'micromark-extension-mdx-expression' 64 | }) 65 | } 66 | 67 | try { 68 | estree = 69 | options.expression && !isEmptyExpression 70 | ? options.acorn.parseExpressionAt(value, 0, acornConfig) 71 | : options.acorn.parse(value, acornConfig) 72 | } catch (error_) { 73 | const error = /** @type {AcornError} */ (error_) 74 | const point = parseOffsetToUnistPoint(error.pos) 75 | error.message = String(error.message).replace(/ \(\d+:\d+\)$/, '') 76 | // Always defined in our unist points that come from micromark. 77 | assert(point.offset !== undefined, 'expected `offset`') 78 | error.pos = point.offset 79 | error.loc = {line: point.line, column: point.column - 1} 80 | exception = error 81 | swallow = 82 | error.raisedAt >= prefix.length + source.length || 83 | // Broken comments are raised at their start, not their end. 84 | error.message === 'Unterminated comment' 85 | } 86 | 87 | if (estree && options.expression && !isEmptyExpression) { 88 | if (empty(value.slice(estree.end, value.length - suffix.length))) { 89 | estree = { 90 | type: 'Program', 91 | start: 0, 92 | end: prefix.length + source.length, 93 | // @ts-expect-error: It’s good. 94 | body: [ 95 | { 96 | type: 'ExpressionStatement', 97 | expression: estree, 98 | start: 0, 99 | end: prefix.length + source.length 100 | } 101 | ], 102 | sourceType: 'module', 103 | comments: [] 104 | } 105 | } else { 106 | const point = parseOffsetToUnistPoint(estree.end) 107 | const error = /** @type {AcornError} */ ( 108 | new Error('Unexpected content after expression') 109 | ) 110 | // Always defined in our unist points that come from micromark. 111 | assert(point.offset !== undefined, 'expected `offset`') 112 | error.pos = point.offset 113 | error.loc = {line: point.line, column: point.column - 1} 114 | exception = error 115 | estree = undefined 116 | } 117 | } 118 | 119 | if (estree) { 120 | // @ts-expect-error: acorn *does* allow comments 121 | estree.comments = comments 122 | 123 | // @ts-expect-error: acorn looks enough like estree. 124 | visit(estree, function (esnode, field, index, parents) { 125 | let context = /** @type {AcornNode | Array} */ ( 126 | parents[parents.length - 1] 127 | ) 128 | /** @type {number | string | undefined} */ 129 | let property = field 130 | 131 | // Remove non-standard `ParenthesizedExpression`. 132 | // @ts-expect-error: included in acorn. 133 | if (esnode.type === 'ParenthesizedExpression' && context && property) { 134 | /* c8 ignore next 5 */ 135 | if (typeof index === 'number') { 136 | // @ts-expect-error: indexable. 137 | context = context[property] 138 | property = index 139 | } 140 | 141 | // @ts-expect-error: indexable. 142 | context[property] = esnode.expression 143 | } 144 | 145 | fixPosition(esnode) 146 | }) 147 | 148 | // Comment positions are fixed by `visit` because they’re in the tree. 149 | if (Array.isArray(onComment)) { 150 | onComment.push(...comments) 151 | } else if (typeof onComment === 'function') { 152 | for (const comment of comments) { 153 | assert(comment.loc, 'expected `loc` on comment') 154 | onComment( 155 | comment.type === 'Block', 156 | comment.value, 157 | comment.start, 158 | comment.end, 159 | comment.loc.start, 160 | comment.loc.end 161 | ) 162 | } 163 | } 164 | 165 | for (const token of tokens) { 166 | // Ignore tokens that ends in prefix or start in suffix: 167 | if ( 168 | token.end <= prefix.length || 169 | token.start - prefix.length >= source.length 170 | ) { 171 | continue 172 | } 173 | 174 | fixPosition(token) 175 | 176 | if (Array.isArray(onToken)) { 177 | onToken.push(token) 178 | } else { 179 | // `tokens` are not added if `onToken` is not defined, so it must be a 180 | // function. 181 | assert(typeof onToken === 'function', 'expected function') 182 | onToken(token) 183 | } 184 | } 185 | } 186 | 187 | // @ts-expect-error: It’s a program now. 188 | return {estree, error: exception, swallow} 189 | 190 | /** 191 | * Update the position of a node. 192 | * 193 | * @param {AcornNode | EstreeNode | Token} nodeOrToken 194 | * @returns {undefined} 195 | */ 196 | function fixPosition(nodeOrToken) { 197 | assert( 198 | 'start' in nodeOrToken, 199 | 'expected `start` in node or token from acorn' 200 | ) 201 | assert('end' in nodeOrToken, 'expected `end` in node or token from acorn') 202 | const pointStart = parseOffsetToUnistPoint(nodeOrToken.start) 203 | const pointEnd = parseOffsetToUnistPoint(nodeOrToken.end) 204 | // Always defined in our unist points that come from micromark. 205 | assert(pointStart.offset !== undefined, 'expected `offset`') 206 | assert(pointEnd.offset !== undefined, 'expected `offset`') 207 | nodeOrToken.start = pointStart.offset 208 | nodeOrToken.end = pointEnd.offset 209 | nodeOrToken.loc = { 210 | start: { 211 | line: pointStart.line, 212 | column: pointStart.column - 1, 213 | // @ts-expect-error: not allowed by acorn types. 214 | offset: pointStart.offset 215 | }, 216 | end: { 217 | line: pointEnd.line, 218 | column: pointEnd.column - 1, 219 | // @ts-expect-error: not allowed by acorn types. 220 | offset: pointEnd.offset 221 | } 222 | } 223 | nodeOrToken.range = [nodeOrToken.start, nodeOrToken.end] 224 | } 225 | 226 | /** 227 | * Turn an arbitrary offset into the parsed value, into a point in the source 228 | * value. 229 | * 230 | * @param {number} acornOffset 231 | * @returns {UnistPoint} 232 | */ 233 | function parseOffsetToUnistPoint(acornOffset) { 234 | let sourceOffset = acornOffset - prefix.length 235 | 236 | if (sourceOffset < 0) { 237 | sourceOffset = 0 238 | } else if (sourceOffset > source.length) { 239 | sourceOffset = source.length 240 | } 241 | 242 | let point = relativeToPoint(collection.stops, sourceOffset) 243 | 244 | if (!point) { 245 | assert( 246 | options.start, 247 | 'empty expressions are need `options.start` being passed' 248 | ) 249 | point = { 250 | line: options.start.line, 251 | column: options.start.column, 252 | offset: options.start.offset 253 | } 254 | } 255 | 256 | return point 257 | } 258 | } 259 | 260 | /** 261 | * @param {string} value 262 | * @returns {boolean} 263 | */ 264 | function empty(value) { 265 | return /^\s*$/.test( 266 | value 267 | // Multiline comments. 268 | .replace(/\/\*[\s\S]*?\*\//g, '') 269 | // Line comments. 270 | // EOF instead of EOL is specifically not allowed, because that would 271 | // mean the closing brace is on the commented-out line 272 | .replace(/\/\/[^\r\n]*(\r\n|\n|\r)/g, '') 273 | ) 274 | } 275 | 276 | // Port from . 277 | /** 278 | * @param {Array} events 279 | * @param {Array} tokenTypes 280 | * @returns {Collection} 281 | */ 282 | function collect(events, tokenTypes) { 283 | /** @type {Collection} */ 284 | const result = {value: '', stops: []} 285 | let index = -1 286 | 287 | while (++index < events.length) { 288 | const event = events[index] 289 | 290 | // Assume void. 291 | if (event[0] === 'enter') { 292 | const type = event[1].type 293 | 294 | if (type === types.lineEnding || tokenTypes.includes(type)) { 295 | const chunks = event[2].sliceStream(event[1]) 296 | 297 | // Drop virtual spaces. 298 | while (chunks.length > 0 && chunks[0] === codes.virtualSpace) { 299 | chunks.shift() 300 | } 301 | 302 | const value = serializeChunks(chunks) 303 | result.stops.push([result.value.length, event[1].start]) 304 | result.value += value 305 | result.stops.push([result.value.length, event[1].end]) 306 | } 307 | } 308 | } 309 | 310 | return result 311 | } 312 | 313 | // Port from . 314 | /** 315 | * Turn a relative offset into an absolute offset. 316 | * 317 | * @param {Array} stops 318 | * @param {number} relative 319 | * @returns {UnistPoint | undefined} 320 | */ 321 | function relativeToPoint(stops, relative) { 322 | let index = 0 323 | 324 | while (index < stops.length && stops[index][0] <= relative) { 325 | index += 1 326 | } 327 | 328 | // There are no points: that only occurs if there was an empty string. 329 | if (index === 0) { 330 | return undefined 331 | } 332 | 333 | const [stopRelative, stopAbsolute] = stops[index - 1] 334 | const rest = relative - stopRelative 335 | return { 336 | line: stopAbsolute.line, 337 | column: stopAbsolute.column + rest, 338 | offset: stopAbsolute.offset + rest 339 | } 340 | } 341 | 342 | // Copy from 343 | // To do: expose that? 344 | /** 345 | * Get the string value of a slice of chunks. 346 | * 347 | * @param {Array} chunks 348 | * @returns {string} 349 | */ 350 | function serializeChunks(chunks) { 351 | let index = -1 352 | /** @type {Array} */ 353 | const result = [] 354 | /** @type {boolean | undefined} */ 355 | let atTab 356 | 357 | while (++index < chunks.length) { 358 | const chunk = chunks[index] 359 | /** @type {string} */ 360 | let value 361 | 362 | if (typeof chunk === 'string') { 363 | value = chunk 364 | } else 365 | switch (chunk) { 366 | case codes.carriageReturn: { 367 | value = values.cr 368 | 369 | break 370 | } 371 | 372 | case codes.lineFeed: { 373 | value = values.lf 374 | 375 | break 376 | } 377 | 378 | case codes.carriageReturnLineFeed: { 379 | value = values.cr + values.lf 380 | 381 | break 382 | } 383 | 384 | case codes.horizontalTab: { 385 | value = values.ht 386 | 387 | break 388 | } 389 | 390 | /* c8 ignore next 6 */ 391 | case codes.virtualSpace: { 392 | if (atTab) continue 393 | value = values.space 394 | 395 | break 396 | } 397 | 398 | default: { 399 | assert(typeof chunk === 'number', 'expected number') 400 | // Currently only replacement character. 401 | // eslint-disable-next-line unicorn/prefer-code-point 402 | value = String.fromCharCode(chunk) 403 | } 404 | } 405 | 406 | atTab = chunk === codes.horizontalTab 407 | result.push(value) 408 | } 409 | 410 | return result.join('') 411 | } 412 | -------------------------------------------------------------------------------- /packages/micromark-extension-mdx-expression/readme.md: -------------------------------------------------------------------------------- 1 | # micromark-extension-mdx-expression 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 | [micromark][] extension to support [MDX][mdxjs] expressions (`{Math.PI}`). 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 | * [`mdxExpression(options?)`](#mdxexpressionoptions) 21 | * [Options](#options) 22 | * [Authoring](#authoring) 23 | * [Syntax](#syntax) 24 | * [Errors](#errors) 25 | * [Unexpected end of file in expression, expected a corresponding closing brace for `{`](#unexpected-end-of-file-in-expression-expected-a-corresponding-closing-brace-for-) 26 | * [Unexpected lazy line in expression in container, expected line to be prefixed…](#unexpected-lazy-line-in-expression-in-container-expected-line-to-be-prefixed) 27 | * [Unexpected `$type` in code: expected an object spread (`{...spread}`)](#unexpected-type-in-code-expected-an-object-spread-spread) 28 | * [Unexpected extra content in spread: only a single spread is supported](#unexpected-extra-content-in-spread-only-a-single-spread-is-supported) 29 | * [Could not parse expression with acorn](#could-not-parse-expression-with-acorn) 30 | * [Tokens](#tokens) 31 | * [Types](#types) 32 | * [Compatibility](#compatibility) 33 | * [Security](#security) 34 | * [Related](#related) 35 | * [Contribute](#contribute) 36 | * [License](#license) 37 | 38 | ## What is this? 39 | 40 | This package contains an extension that adds support for the expression syntax 41 | enabled by [MDX][mdxjs] to [`micromark`][micromark]. 42 | These extensions are used inside MDX. 43 | 44 | This package can be made aware or unaware of JavaScript syntax. 45 | When unaware, expressions could include Rust or variables or whatnot. 46 | 47 | ## When to use this 48 | 49 | This project is useful when you want to support expressions in markdown. 50 | 51 | You can use this extension when you are working with [`micromark`][micromark]. 52 | To support all MDX features, use 53 | [`micromark-extension-mdxjs`][micromark-extension-mdxjs] instead. 54 | 55 | When you need a syntax tree, combine this package with 56 | [`mdast-util-mdx-expression`][mdast-util-mdx-expression]. 57 | 58 | All these packages are used in [`remark-mdx`][remark-mdx], which focusses on 59 | making it easier to transform content by abstracting these internals away. 60 | 61 | When you are using [`mdx-js/mdx`][mdxjs], all of this is already included. 62 | 63 | ## Install 64 | 65 | This package is [ESM only][esm]. 66 | In Node.js (version 16+), install with [npm][]: 67 | 68 | ```sh 69 | npm install micromark-extension-mdx-expression 70 | ``` 71 | 72 | In Deno with [`esm.sh`][esmsh]: 73 | 74 | ```js 75 | import {mdxExpression} from 'https://esm.sh/micromark-extension-mdx-expression@2' 76 | ``` 77 | 78 | In browsers with [`esm.sh`][esmsh]: 79 | 80 | ```html 81 | 84 | ``` 85 | 86 | ## Use 87 | 88 | ```js 89 | import {Parser} from 'acorn' 90 | import acornJsx from 'acorn-jsx' 91 | import {micromark} from 'micromark' 92 | import {mdxExpression} from 'micromark-extension-mdx-expression' 93 | 94 | // Unaware of JavaScript (“agnostic”) (balanced braces): 95 | const output = micromark('a {1 + 1} b', {extensions: [mdxExpression()]}) 96 | 97 | console.log(output) 98 | 99 | // Aware of JavaScript: 100 | micromark('a {!} b', {extensions: [mdxExpression({acorn: Parser.extend(acornJsx())})]}) 101 | ``` 102 | 103 | Yields: 104 | 105 | ```html 106 |

a b

107 | ``` 108 | 109 | ```text 110 | [1:5: Could not parse expression with acorn] { 111 | ancestors: undefined, 112 | cause: SyntaxError: Unexpected token 113 | at pp$4.raise (file:///Users/tilde/Projects/oss/micromark-extension-mdx-expression/node_modules/acorn/dist/acorn.mjs:3547:13) 114 | at pp$9.unexpected (file:///Users/tilde/Projects/oss/micromark-extension-mdx-expression/node_modules/acorn/dist/acorn.mjs:758:8) 115 | … 116 | pos: 4, 117 | loc: { line: 1, column: 4 }, 118 | raisedAt: 1 119 | }, 120 | column: 5, 121 | fatal: undefined, 122 | line: 1, 123 | place: { line: 1, column: 5, offset: 4 }, 124 | reason: 'Could not parse expression with acorn', 125 | ruleId: 'acorn', 126 | source: 'micromark-extension-mdx-expression', 127 | url: 'https://github.com/micromark/micromark-extension-mdx-expression/tree/main/packages/micromark-extension-mdx-expression#could-not-parse-expression-with-acorn' 128 | } 129 | ``` 130 | 131 | …which is useless: go to a syntax tree with 132 | [`mdast-util-from-markdown`][mdast-util-from-markdown] and 133 | [`mdast-util-mdx-expression`][mdast-util-mdx-expression] instead. 134 | 135 | ## API 136 | 137 | This package exports the identifier [`mdxExpression`][api-mdx-expression]. 138 | There is no default export. 139 | 140 | The export map supports the [`development` condition][development]. 141 | Run `node --conditions development module.js` to get instrumented dev code. 142 | Without this condition, production code is loaded. 143 | 144 | ### `mdxExpression(options?)` 145 | 146 | Create an extension for `micromark` to enable MDX expression syntax. 147 | 148 | ###### Parameters 149 | 150 | * `options` ([`Options`][api-options], optional) 151 | — configuration 152 | 153 | ###### Returns 154 | 155 | Extension for `micromark` that can be passed in `extensions` to enable MDX 156 | expression syntax ([`Extension`][micromark-extension]). 157 | 158 | ### Options 159 | 160 | Configuration (TypeScript type). 161 | 162 | ###### Fields 163 | 164 | * `acorn` ([`Acorn`][acorn], optional) 165 | — acorn parser to use 166 | * `acornOptions` ([`AcornOptions`][acorn-options], default: 167 | `{ecmaVersion: 2024, locations: true, sourceType: 'module'}`) 168 | — configuration for acorn; all fields except `locations` can be set 169 | * `addResult` (`boolean`, default: `false`) 170 | — whether to add `estree` fields to tokens with results from acorn 171 | 172 | 173 | 174 | ## Authoring 175 | 176 | When authoring markdown with JavaScript, keep in mind that MDX is a whitespace 177 | sensitive and line-based language, while JavaScript is insensitive to 178 | whitespace. 179 | This affects how markdown and JavaScript interleave with eachother in MDX. 180 | For more info on how it works, see [§ Interleaving][mdxjs-interleaving] on the 181 | MDX site. 182 | 183 | ## Syntax 184 | 185 | This extension supports MDX both aware and unaware to JavaScript (respectively 186 | gnostic and agnostic). 187 | Depending on whether acorn is passed, either valid JavaScript must be used in 188 | expressions, or arbitrary text (such as Rust code or so) can be used. 189 | 190 | There are two types of expressions: in text (inline, span) or in flow (block). 191 | They start with `{`. 192 | 193 | Depending on whether `acorn` is passed, expressions are either parsed in several 194 | tries until whole JavaScript is found (as in, nested curly braces depend on JS 195 | expression nesting), or they are counted and must be balanced. 196 | 197 | Expressions end with `}`. 198 | 199 | For flow (block) expressions, optionally markdown spaces (` ` or `\t`) can occur 200 | after the closing brace, and finally a markdown line ending (`\r`, `\n`) or the 201 | end of the file must follow. 202 | 203 | While markdown typically knows no errors, for MDX it is decided to instead 204 | throw on invalid syntax. 205 | 206 | ```mdx 207 | Here is an expression in a heading: 208 | 209 | ## Hello, {1 + 1}! 210 | 211 | In agnostic mode, balanced braces can occur: {a + {b} + c}. 212 | 213 | In gnostic mode, the value of the expression must be JavaScript, so 214 | this would fail: {!}. 215 | But, in gnostic mode, braces can be in comments, strings, or in other 216 | places: {1 /* { */ + 2}. 217 | 218 | The previous examples were text (inline, span) expressions, they can 219 | also be flow (block): 220 | 221 | { 222 | 1 + 1 223 | } 224 | 225 | This is incorrect, because there are further characters: 226 | 227 | { 228 | 1 + 1 229 | }! 230 | ``` 231 | 232 | ```mdx-invalid 233 | Blank lines cannot occur in text, because markdown has already split them in 234 | separate constructs, so this is incorrect: {1 + 235 | 236 | 1} 237 | ``` 238 | 239 | ```mdx 240 | In flow, you can have blank lines: 241 | 242 | { 243 | 1 + 244 | 245 | 2 246 | } 247 | ``` 248 | 249 | ## Errors 250 | 251 | ### Unexpected end of file in expression, expected a corresponding closing brace for `{` 252 | 253 | This error occurs if a `{` was seen without a `}` (source: 254 | `micromark-extension-mdx-expression`, rule id: `unexpected-eof`). 255 | For example: 256 | 257 | ```mdx-invalid 258 | a { b 259 | ``` 260 | 261 | ### Unexpected lazy line in expression in container, expected line to be prefixed… 262 | 263 | This error occurs if a `{` was seen in a container which then has lazy content 264 | (source: `micromark-extension-mdx-expression`, rule id: `unexpected-lazy`). 265 | For example: 266 | 267 | ```mdx-invalid 268 | > {a 269 | b} 270 | ``` 271 | 272 | ### Unexpected `$type` in code: expected an object spread (`{...spread}`) 273 | 274 | This error occurs if a spread was expected but something else was found 275 | (source: `micromark-extension-mdx-expression`, rule id: `non-spread`). 276 | For example: 277 | 278 | ```mdx-invalid 279 | 280 | ``` 281 | 282 | ### Unexpected extra content in spread: only a single spread is supported 283 | 284 | This error occurs if a spread was expected but more was found after it 285 | (source: `micromark-extension-mdx-expression`, rule id: `spread-extra`). 286 | For example: 287 | 288 | ```mdx-invalid 289 | 290 | ``` 291 | 292 | ### Could not parse expression with acorn 293 | 294 | This error occurs if acorn crashes or when there is more content after a JS 295 | expression (source: `micromark-extension-mdx-expression`, rule id: `acorn`). 296 | For example: 297 | 298 | ```mdx-invalid 299 | a {"b" "c"} d 300 | ``` 301 | 302 | ```mdx-invalid 303 | a {var b = "c"} d 304 | ``` 305 | 306 | ## Tokens 307 | 308 | Two tokens are used, `mdxFlowExpression` and `mdxTextExpression`, to reflect 309 | flow and text expressions. 310 | 311 | They include: 312 | 313 | * `lineEnding` for the markdown line endings `\r`, `\n`, and `\r\n` 314 | * `mdxFlowExpressionMarker` and `mdxTextExpressionMarker` for the braces 315 | * `whitespace` for markdown spaces and tabs in blank lines 316 | * `mdxFlowExpressionChunk` and `mdxTextExpressionChunk` for chunks of 317 | expression content 318 | 319 | ## Types 320 | 321 | This package is fully typed with [TypeScript][]. 322 | It exports the additional type [`Options`][api-options]. 323 | 324 | ## Compatibility 325 | 326 | Projects maintained by the unified collective are compatible with maintained 327 | versions of Node.js. 328 | 329 | When we cut a new major release, we drop support for unmaintained versions of 330 | Node. 331 | This means we try to keep the current release line, 332 | `micromark-extension-mdx-expression@^2`, compatible with Node.js 16. 333 | 334 | This package works with `micromark` version `3` and later. 335 | 336 | ## Security 337 | 338 | This package is safe. 339 | 340 | ## Related 341 | 342 | * [`micromark-extension-mdxjs`][micromark-extension-mdxjs] 343 | — support all MDX syntax 344 | * [`mdast-util-mdx-expression`][mdast-util-mdx-expression] 345 | — support MDX expressions in mdast 346 | * [`remark-mdx`][remark-mdx] 347 | — support all MDX syntax in remark 348 | 349 | ## Contribute 350 | 351 | See [`contributing.md` in `micromark/.github`][contributing] for ways to get 352 | started. 353 | See [`support.md`][support] for ways to get help. 354 | 355 | This project has a [code of conduct][coc]. 356 | By interacting with this repository, organization, or community you agree to 357 | abide by its terms. 358 | 359 | ## License 360 | 361 | [MIT][license] © [Titus Wormer][author] 362 | 363 | 364 | 365 | [acorn]: https://github.com/acornjs/acorn 366 | 367 | [acorn-options]: https://github.com/acornjs/acorn/blob/96c721dbf89d0ccc3a8c7f39e69ef2a6a3c04dfa/acorn/dist/acorn.d.ts#L16 368 | 369 | [api-mdx-expression]: #mdxexpressionoptions 370 | 371 | [api-options]: #options 372 | 373 | [author]: https://wooorm.com 374 | 375 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 376 | 377 | [build]: https://github.com/micromark/micromark-extension-mdx-expression/actions 378 | 379 | [build-badge]: https://github.com/micromark/micromark-extension-mdx-expression/workflows/main/badge.svg 380 | 381 | [chat]: https://github.com/micromark/micromark/discussions 382 | 383 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 384 | 385 | [coc]: https://github.com/micromark/.github/blob/main/code-of-conduct.md 386 | 387 | [collective]: https://opencollective.com/unified 388 | 389 | [contributing]: https://github.com/micromark/.github/blob/main/contributing.md 390 | 391 | [coverage]: https://codecov.io/github/micromark/micromark-extension-mdx-expression 392 | 393 | [coverage-badge]: https://img.shields.io/codecov/c/github/micromark/micromark-extension-mdx-expression.svg 394 | 395 | [development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions 396 | 397 | [downloads]: https://www.npmjs.com/package/micromark-extension-mdx-expression 398 | 399 | [downloads-badge]: https://img.shields.io/npm/dm/micromark-extension-mdx-expression.svg 400 | 401 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 402 | 403 | [esmsh]: https://esm.sh 404 | 405 | [license]: https://github.com/micromark/micromark-extension-mdx-expression/blob/main/license 406 | 407 | [mdast-util-from-markdown]: https://github.com/syntax-tree/mdast-util-from-markdown 408 | 409 | [mdast-util-mdx-expression]: https://github.com/syntax-tree/mdast-util-mdx-expression 410 | 411 | [mdxjs]: https://mdxjs.com 412 | 413 | [mdxjs-interleaving]: https://mdxjs.com/docs/what-is-mdx/#interleaving 414 | 415 | [micromark]: https://github.com/micromark/micromark 416 | 417 | [micromark-extension]: https://github.com/micromark/micromark#syntaxextension 418 | 419 | [micromark-extension-mdxjs]: https://github.com/micromark/micromark-extension-mdxjs 420 | 421 | [npm]: https://docs.npmjs.com/cli/install 422 | 423 | [remark-mdx]: https://mdxjs.com/packages/remark-mdx/ 424 | 425 | [size]: https://bundlejs.com/?q=micromark-extension-mdx-expression 426 | 427 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=micromark-extension-mdx-expression 428 | 429 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 430 | 431 | [support]: https://github.com/micromark/.github/blob/main/support.md 432 | 433 | [typescript]: https://www.typescriptlang.org 434 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Comment, Token} from 'acorn' 3 | * @import {Node, Program} from 'estree' 4 | * @import {CompileContext, Extension, Handle, HtmlExtension, State, TokenizeContext, Tokenizer} from 'micromark-util-types' 5 | * @import {Acorn, AcornOptions} from 'micromark-util-events-to-acorn' 6 | * @import {} from './types.js' 7 | */ 8 | 9 | /* eslint-disable unicorn/prefer-structured-clone -- `JSON` used to turn instances into plain objects. */ 10 | 11 | import assert from 'node:assert/strict' 12 | import test from 'node:test' 13 | import {Parser} from 'acorn' 14 | import acornJsx from 'acorn-jsx' 15 | import {visit} from 'estree-util-visit' 16 | import {micromark} from 'micromark' 17 | import {mdxExpression} from 'micromark-extension-mdx-expression' 18 | import {factoryMdxExpression} from 'micromark-factory-mdx-expression' 19 | import {markdownLineEnding} from 'micromark-util-character' 20 | import {codes} from 'micromark-util-symbol' 21 | 22 | const acorn = Parser.extend(acornJsx()) 23 | 24 | /** @type {HtmlExtension} */ 25 | const html = { 26 | enter: {mdxFlowExpression: start, mdxTextExpression: start}, 27 | exit: {mdxFlowExpression: end, mdxTextExpression: end} 28 | } 29 | 30 | test('api', async function (t) { 31 | await t.test('should expose the public api', async function () { 32 | assert.deepEqual( 33 | Object.keys(await import('micromark-extension-mdx-expression')).sort(), 34 | ['mdxExpression'] 35 | ) 36 | }) 37 | 38 | await t.test('should expose the public api', async function () { 39 | assert.deepEqual( 40 | Object.keys(await import('micromark-factory-mdx-expression')).sort(), 41 | ['factoryMdxExpression'] 42 | ) 43 | }) 44 | 45 | await t.test('should expose the public api', async function () { 46 | assert.deepEqual( 47 | Object.keys(await import('micromark-util-events-to-acorn')).sort(), 48 | ['eventsToAcorn'] 49 | ) 50 | }) 51 | 52 | await t.test( 53 | 'should throw if `acorn` is passed but it has no `parse`', 54 | async function () { 55 | assert.throws(function () { 56 | // @ts-expect-error: check for a runtime error when `acorn` is incorrect. 57 | mdxExpression({acorn: true}) 58 | }, /Expected a proper `acorn` instance passed in as `options\.acorn`/) 59 | } 60 | ) 61 | 62 | await t.test( 63 | 'should throw if `addResult` is passed w/o `acorn`', 64 | async function () { 65 | assert.throws(function () { 66 | mdxExpression({addResult: true}) 67 | }, /Expected an `acorn` instance passed in as `options\.acorn`/) 68 | } 69 | ) 70 | 71 | await t.test( 72 | 'should throw if `acornOptions` is passed w/o `acorn`', 73 | async function () { 74 | assert.throws(function () { 75 | // @ts-expect-error: check for a runtime error when `acorn` is missing. 76 | mdxExpression({acornOptions: {}}) 77 | }, /Expected an `acorn` instance passed in as `options\.acorn`/) 78 | } 79 | ) 80 | 81 | await t.test('should not support JSX by default', async function () { 82 | assert.throws(function () { 83 | micromark('a {} c', {extensions: [mdxExpression({acorn: Parser})]}) 84 | }, /Could not parse expression with acorn/) 85 | }) 86 | 87 | await t.test( 88 | 'should support JSX if an `acorn` instance supporting it is passed in', 89 | async function () { 90 | assert.equal( 91 | micromark('a {} c', { 92 | extensions: [mdxExpression({acorn: Parser.extend(acornJsx())})], 93 | htmlExtensions: [html] 94 | }), 95 | '

a c

' 96 | ) 97 | } 98 | ) 99 | 100 | await t.test('should support `acornOptions` (1)', async function () { 101 | assert.throws(function () { 102 | micromark('a {(() => {})()} c', { 103 | extensions: [mdxExpression({acorn, acornOptions: {ecmaVersion: 5}})] 104 | }) 105 | }, /Could not parse expression with acorn/) 106 | }) 107 | 108 | await t.test('should support `acornOptions` (2)', async function () { 109 | assert.equal( 110 | micromark('a {(function () {})()} c', { 111 | extensions: [mdxExpression({acorn, acornOptions: {ecmaVersion: 6}})], 112 | htmlExtensions: [html] 113 | }), 114 | '

a c

' 115 | ) 116 | }) 117 | 118 | await t.test('should support `addResult`', async function () { 119 | assert.equal( 120 | micromark('a {b} c', { 121 | extensions: [mdxExpression({acorn, addResult: true})], 122 | htmlExtensions: [ 123 | { 124 | enter: { 125 | mdxFlowExpression: checkResultExpression, 126 | mdxTextExpression: checkResultExpression 127 | }, 128 | exit: {mdxFlowExpression: end, mdxTextExpression: end} 129 | } 130 | ] 131 | }), 132 | '

a c

' 133 | ) 134 | 135 | /** 136 | * @this {CompileContext} 137 | * @type {Handle} 138 | */ 139 | function checkResultExpression(token) { 140 | assert.ok( 141 | 'estree' in token, 142 | '`addResult` should add `estree` to expression tokens' 143 | ) 144 | assert.deepEqual( 145 | JSON.parse(JSON.stringify(token.estree)), 146 | { 147 | type: 'Program', 148 | start: 3, 149 | end: 4, 150 | body: [ 151 | { 152 | type: 'ExpressionStatement', 153 | expression: { 154 | type: 'Identifier', 155 | start: 3, 156 | end: 4, 157 | name: 'b', 158 | loc: { 159 | start: { 160 | line: 1, 161 | column: 3, 162 | offset: 3 163 | }, 164 | end: { 165 | line: 1, 166 | column: 4, 167 | offset: 4 168 | } 169 | }, 170 | range: [3, 4] 171 | }, 172 | start: 3, 173 | end: 4, 174 | loc: { 175 | start: { 176 | line: 1, 177 | column: 3, 178 | offset: 3 179 | }, 180 | end: { 181 | line: 1, 182 | column: 4, 183 | offset: 4 184 | } 185 | }, 186 | range: [3, 4] 187 | } 188 | ], 189 | sourceType: 'module', 190 | comments: [], 191 | loc: { 192 | start: { 193 | line: 1, 194 | column: 3, 195 | offset: 3 196 | }, 197 | end: { 198 | line: 1, 199 | column: 4, 200 | offset: 4 201 | } 202 | }, 203 | range: [3, 4] 204 | }, 205 | '`addResult` should add an expression' 206 | ) 207 | return start.call(this, token) 208 | } 209 | }) 210 | 211 | await t.test( 212 | 'should support `addResult` for an empty expression', 213 | async function () { 214 | assert.equal( 215 | micromark('a {} c', { 216 | extensions: [mdxExpression({acorn, addResult: true})], 217 | htmlExtensions: [ 218 | { 219 | enter: { 220 | mdxFlowExpression: checkResultEmpty, 221 | mdxTextExpression: checkResultEmpty 222 | }, 223 | exit: {mdxFlowExpression: end, mdxTextExpression: end} 224 | } 225 | ] 226 | }), 227 | '

a c

' 228 | ) 229 | 230 | /** 231 | * @this {CompileContext} 232 | * @type {Handle} 233 | */ 234 | function checkResultEmpty(token) { 235 | assert.ok( 236 | 'estree' in token, 237 | '`addResult` should add `estree` to expression tokens' 238 | ) 239 | 240 | assert.deepEqual( 241 | JSON.parse(JSON.stringify(token.estree)), 242 | { 243 | type: 'Program', 244 | start: 3, 245 | end: 3, 246 | body: [], 247 | sourceType: 'module', 248 | comments: [], 249 | loc: { 250 | start: { 251 | line: 1, 252 | column: 3, 253 | offset: 3 254 | }, 255 | end: { 256 | line: 1, 257 | column: 3, 258 | offset: 3 259 | } 260 | }, 261 | range: [3, 3] 262 | }, 263 | '`estree` should be an empty program for an empty expression' 264 | ) 265 | return start.call(this, token) 266 | } 267 | } 268 | ) 269 | 270 | await t.test('should support `onComment` as a function', async function () { 271 | /** @type {Array>} */ 272 | const listOfArguments = [] 273 | 274 | assert.equal( 275 | micromark('a {/*b*/ // c\n} d', { 276 | extensions: [ 277 | mdxExpression({ 278 | acorn, 279 | acornOptions: { 280 | ecmaVersion: 6, 281 | onComment() { 282 | listOfArguments.push([...arguments]) 283 | } 284 | } 285 | }) 286 | ], 287 | htmlExtensions: [html] 288 | }), 289 | '

a d

' 290 | ) 291 | 292 | assert.deepEqual(listOfArguments, [ 293 | [ 294 | true, 295 | 'b', 296 | 3, 297 | 8, 298 | {line: 1, column: 3, offset: 3}, 299 | {line: 1, column: 8, offset: 8} 300 | ], 301 | [ 302 | false, 303 | ' c', 304 | 9, 305 | 13, 306 | {line: 1, column: 9, offset: 9}, 307 | {line: 1, column: 13, offset: 13} 308 | ] 309 | ]) 310 | }) 311 | 312 | await t.test('should support `onToken` (1, array)', async function () { 313 | /** @type {Array} */ 314 | const tokens = [] 315 | 316 | assert.equal( 317 | micromark('a {b.c} d', { 318 | extensions: [ 319 | mdxExpression({ 320 | acorn, 321 | acornOptions: {ecmaVersion: 6, onToken: tokens} 322 | }) 323 | ], 324 | htmlExtensions: [html] 325 | }), 326 | '

a d

' 327 | ) 328 | 329 | assert.equal( 330 | JSON.stringify(tokens), 331 | JSON.stringify([ 332 | { 333 | type: { 334 | label: 'name', 335 | beforeExpr: false, 336 | startsExpr: true, 337 | isLoop: false, 338 | isAssign: false, 339 | prefix: false, 340 | postfix: false, 341 | binop: null 342 | }, 343 | value: 'b', 344 | start: 3, 345 | end: 4, 346 | loc: { 347 | start: {line: 1, column: 3, offset: 3}, 348 | end: {line: 1, column: 4, offset: 4} 349 | }, 350 | range: [3, 4] 351 | }, 352 | { 353 | type: { 354 | label: '.', 355 | beforeExpr: false, 356 | startsExpr: false, 357 | isLoop: false, 358 | isAssign: false, 359 | prefix: false, 360 | postfix: false, 361 | binop: null, 362 | updateContext: null 363 | }, 364 | start: 4, 365 | end: 5, 366 | loc: { 367 | start: {line: 1, column: 4, offset: 4}, 368 | end: {line: 1, column: 5, offset: 5} 369 | }, 370 | range: [4, 5] 371 | }, 372 | { 373 | type: { 374 | label: 'name', 375 | beforeExpr: false, 376 | startsExpr: true, 377 | isLoop: false, 378 | isAssign: false, 379 | prefix: false, 380 | postfix: false, 381 | binop: null 382 | }, 383 | value: 'c', 384 | start: 5, 385 | end: 6, 386 | loc: { 387 | start: {line: 1, column: 5, offset: 5}, 388 | end: {line: 1, column: 6, offset: 6} 389 | }, 390 | range: [5, 6] 391 | } 392 | ]) 393 | ) 394 | }) 395 | 396 | await t.test('should support `onToken` (2, function)', async function () { 397 | /** @type {Array} */ 398 | const tokens = [] 399 | 400 | assert.equal( 401 | micromark('a {b.c} d', { 402 | extensions: [ 403 | mdxExpression({ 404 | acorn, 405 | acornOptions: { 406 | ecmaVersion: 6, 407 | onToken(token) { 408 | tokens.push(token) 409 | } 410 | } 411 | }) 412 | ], 413 | htmlExtensions: [html] 414 | }), 415 | '

a d

' 416 | ) 417 | 418 | assert.equal(tokens.length, 3) 419 | }) 420 | 421 | await t.test( 422 | 'should add correct positional info on acorn tokens', 423 | function () { 424 | const micromarkExample = 'a {b} c' 425 | const acornExample = ' b ' 426 | /** @type {Array} */ 427 | const micromarkTokens = [] 428 | /** @type {Array} */ 429 | const acornTokens = [] 430 | 431 | acorn.parseExpressionAt(acornExample, 0, { 432 | ecmaVersion: 'latest', 433 | onToken: acornTokens, 434 | locations: true, 435 | ranges: true 436 | }) 437 | 438 | micromark(micromarkExample, { 439 | extensions: [ 440 | createExtensionFromFactoryOptions( 441 | acorn, 442 | {ecmaVersion: 'latest', onToken: micromarkTokens}, 443 | false, 444 | false, 445 | false, 446 | true 447 | ) 448 | ] 449 | }) 450 | 451 | removeOffsetsFromTokens(micromarkTokens) 452 | 453 | assert.deepEqual( 454 | JSON.parse(JSON.stringify(micromarkTokens)), 455 | JSON.parse(JSON.stringify(acornTokens)) 456 | ) 457 | } 458 | ) 459 | 460 | await t.test( 461 | 'should add correct positional info on acorn tokens with spread', 462 | function () { 463 | const micromarkExample = 'alp {...b}' 464 | const acornExample = 'a = {...b}' 465 | /** @type {Array} */ 466 | const micromarkTokens = [] 467 | /** @type {Array} */ 468 | const acornTokens = [] 469 | 470 | acorn.parseExpressionAt(acornExample, 0, { 471 | ecmaVersion: 'latest', 472 | onToken: acornTokens, 473 | locations: true, 474 | ranges: true 475 | }) 476 | 477 | micromark(micromarkExample, { 478 | extensions: [ 479 | createExtensionFromFactoryOptions( 480 | acorn, 481 | {ecmaVersion: 'latest', onToken: micromarkTokens}, 482 | false, 483 | true, 484 | false, 485 | true 486 | ) 487 | ] 488 | }) 489 | 490 | removeOffsetsFromTokens(micromarkTokens) 491 | 492 | // Remove `a`, `=`, `{` 493 | acornTokens.splice(0, 3) 494 | // Remove `}`. 495 | acornTokens.pop() 496 | 497 | assert.deepEqual( 498 | JSON.parse(JSON.stringify(micromarkTokens)), 499 | JSON.parse(JSON.stringify(acornTokens)) 500 | ) 501 | } 502 | ) 503 | }) 504 | 505 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_text.rs` 506 | // as `fn mdx_expression()`. 507 | test('mdxExpression', async function (t) { 508 | await t.test('should support an empty expression (1)', async function () { 509 | assert.equal( 510 | micromark('a {} b', { 511 | extensions: [mdxExpression({acorn})], 512 | htmlExtensions: [html] 513 | }), 514 | '

a b

' 515 | ) 516 | }) 517 | 518 | await t.test('should support an empty expression (2)', async function () { 519 | assert.equal( 520 | micromark('a { \t\r\n} b', { 521 | extensions: [mdxExpression({acorn})], 522 | htmlExtensions: [html] 523 | }), 524 | '

a b

' 525 | ) 526 | }) 527 | 528 | await t.test('should support a multiline comment (1)', async function () { 529 | assert.equal( 530 | micromark('a {/**/} b', { 531 | extensions: [mdxExpression({acorn})], 532 | htmlExtensions: [html] 533 | }), 534 | '

a b

' 535 | ) 536 | }) 537 | 538 | await t.test('should support a multiline comment (2)', async function () { 539 | assert.equal( 540 | micromark('a { /*\n*/\t} b', { 541 | extensions: [mdxExpression({acorn})], 542 | htmlExtensions: [html] 543 | }), 544 | '

a b

' 545 | ) 546 | }) 547 | 548 | await t.test('should support a multiline comment (3)', async function () { 549 | assert.equal( 550 | micromark('a {/*b*//*c*/} d', { 551 | extensions: [mdxExpression({acorn})], 552 | htmlExtensions: [html] 553 | }), 554 | '

a d

' 555 | ) 556 | }) 557 | 558 | await t.test('should support a multiline comment (4)', async function () { 559 | assert.equal( 560 | micromark('a {b/*c*/} d', { 561 | extensions: [mdxExpression({acorn})], 562 | htmlExtensions: [html] 563 | }), 564 | '

a d

' 565 | ) 566 | }) 567 | 568 | await t.test('should support a multiline comment (5)', async function () { 569 | assert.equal( 570 | micromark('a {/*b*/c} d', { 571 | extensions: [mdxExpression({acorn})], 572 | htmlExtensions: [html] 573 | }), 574 | '

a d

' 575 | ) 576 | }) 577 | 578 | await t.test( 579 | 'should crash on an incorrect line comment (1)', 580 | async function () { 581 | assert.throws(function () { 582 | micromark('a {//} b', {extensions: [mdxExpression({acorn})]}) 583 | }, /Could not parse expression with acorn/) 584 | } 585 | ) 586 | 587 | await t.test( 588 | 'should crash on an incorrect line comment (2)', 589 | async function () { 590 | assert.throws(function () { 591 | micromark('a { // b } c', {extensions: [mdxExpression({acorn})]}) 592 | }, /Could not parse expression with acorn/) 593 | } 594 | ) 595 | 596 | await t.test( 597 | 'should support a line comment followed by a line ending', 598 | async function () { 599 | assert.equal( 600 | micromark('a {//\n} b', { 601 | extensions: [mdxExpression({acorn})], 602 | htmlExtensions: [html] 603 | }), 604 | '

a b

' 605 | ) 606 | } 607 | ) 608 | 609 | await t.test( 610 | 'should support a line comment followed by a line ending and an expression', 611 | async function () { 612 | assert.equal( 613 | micromark('a {// b\nc} d', { 614 | extensions: [mdxExpression({acorn})], 615 | htmlExtensions: [html] 616 | }), 617 | '

a d

' 618 | ) 619 | } 620 | ) 621 | 622 | await t.test( 623 | 'should support an expression followed by a line comment and a line ending', 624 | async function () { 625 | assert.equal( 626 | micromark('a {b// c\n} d', { 627 | extensions: [mdxExpression({acorn})], 628 | htmlExtensions: [html] 629 | }), 630 | '

a d

' 631 | ) 632 | } 633 | ) 634 | 635 | await t.test('should support comments', async function () { 636 | /** @type {Array} */ 637 | const comments = [] 638 | 639 | assert.equal( 640 | micromark('a {/*b*/ // c\n} d', { 641 | extensions: [ 642 | mdxExpression({ 643 | acorn, 644 | acornOptions: {ecmaVersion: 6, onComment: comments}, 645 | addResult: true 646 | }) 647 | ], 648 | htmlExtensions: [ 649 | { 650 | enter: { 651 | mdxFlowExpression: checkResultComments, 652 | mdxTextExpression: checkResultComments 653 | }, 654 | exit: {mdxFlowExpression: end, mdxTextExpression: end} 655 | } 656 | ] 657 | }), 658 | '

a d

' 659 | ) 660 | 661 | assert.deepEqual(comments, [ 662 | { 663 | type: 'Block', 664 | value: 'b', 665 | start: 3, 666 | end: 8, 667 | loc: { 668 | start: {line: 1, column: 3, offset: 3}, 669 | end: {line: 1, column: 8, offset: 8} 670 | }, 671 | range: [3, 8] 672 | }, 673 | { 674 | type: 'Line', 675 | value: ' c', 676 | start: 9, 677 | end: 13, 678 | loc: { 679 | start: {line: 1, column: 9, offset: 9}, 680 | end: {line: 1, column: 13, offset: 13} 681 | }, 682 | range: [9, 13] 683 | } 684 | ]) 685 | 686 | /** 687 | * @this {CompileContext} 688 | * @type {Handle} 689 | */ 690 | function checkResultComments(token) { 691 | assert.deepEqual(JSON.parse(JSON.stringify(token.estree)), { 692 | type: 'Program', 693 | start: 3, 694 | end: 14, 695 | body: [], 696 | sourceType: 'module', 697 | comments: [ 698 | { 699 | type: 'Block', 700 | value: 'b', 701 | start: 3, 702 | end: 8, 703 | loc: { 704 | start: {line: 1, column: 3, offset: 3}, 705 | end: {line: 1, column: 8, offset: 8} 706 | }, 707 | range: [3, 8] 708 | }, 709 | { 710 | type: 'Line', 711 | value: ' c', 712 | start: 9, 713 | end: 13, 714 | loc: { 715 | start: {line: 1, column: 9, offset: 9}, 716 | end: {line: 1, column: 13, offset: 13} 717 | }, 718 | range: [9, 13] 719 | } 720 | ], 721 | loc: { 722 | start: {line: 1, column: 3, offset: 3}, 723 | end: {line: 2, column: 0, offset: 14} 724 | }, 725 | range: [3, 14] 726 | }) 727 | 728 | return start.call(this, token) 729 | } 730 | }) 731 | 732 | await t.test('should support expression statements (1)', async function () { 733 | assert.equal( 734 | micromark('a {b.c} d', { 735 | extensions: [mdxExpression({acorn})], 736 | htmlExtensions: [html] 737 | }), 738 | '

a d

' 739 | ) 740 | }) 741 | 742 | await t.test('should support expression statements (2)', async function () { 743 | assert.equal( 744 | micromark('a {1 + 1} b', { 745 | extensions: [mdxExpression({acorn})], 746 | htmlExtensions: [html] 747 | }), 748 | '

a b

' 749 | ) 750 | }) 751 | 752 | await t.test('should support expression statements (3)', async function () { 753 | assert.equal( 754 | micromark('a {function () {}} b', { 755 | extensions: [mdxExpression({acorn})], 756 | htmlExtensions: [html] 757 | }), 758 | '

a b

' 759 | ) 760 | }) 761 | 762 | await t.test('should crash on non-expressions', async function () { 763 | assert.throws(function () { 764 | micromark('a {var b = "c"} d', {extensions: [mdxExpression({acorn})]}) 765 | }, /Could not parse expression with acorn/) 766 | }) 767 | 768 | await t.test('should support expressions in containers', async function () { 769 | assert.equal( 770 | micromark('> a {\n> b} c', { 771 | extensions: [mdxExpression({acorn})], 772 | htmlExtensions: [html] 773 | }), 774 | '
\n

a c

\n
' 775 | ) 776 | }) 777 | 778 | await t.test( 779 | 'should crash on incorrect expressions in containers (1)', 780 | async function () { 781 | assert.throws(function () { 782 | micromark('> a {\n> b<} c', {extensions: [mdxExpression({acorn})]}) 783 | }, /Could not parse expression with acorn/) 784 | } 785 | ) 786 | 787 | await t.test( 788 | 'should crash on incorrect expressions in containers (2)', 789 | async function () { 790 | assert.throws(function () { 791 | micromark('> a {\n> b\n> c} d', {extensions: [mdxExpression({acorn})]}) 792 | }, /Could not parse expression with acorn/) 793 | } 794 | ) 795 | }) 796 | 797 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_text.rs` 798 | // as `fn mdx_expression_text_agnostic()`. 799 | test('text (agnostic)', async function (t) { 800 | await t.test('should support an expression', async function () { 801 | assert.equal( 802 | micromark('a {b} c', { 803 | extensions: [mdxExpression()], 804 | htmlExtensions: [html] 805 | }), 806 | '

a c

' 807 | ) 808 | }) 809 | 810 | await t.test('should support an empty expression', async function () { 811 | assert.equal( 812 | micromark('a {} b', { 813 | extensions: [mdxExpression()], 814 | htmlExtensions: [html] 815 | }), 816 | '

a b

' 817 | ) 818 | }) 819 | 820 | await t.test( 821 | 'should crash if no closing brace is found (1)', 822 | async function () { 823 | assert.throws(function () { 824 | micromark('a {b c', {extensions: [mdxExpression()]}) 825 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 826 | } 827 | ) 828 | 829 | await t.test( 830 | 'should crash if no closing brace is found (2)', 831 | async function () { 832 | assert.throws(function () { 833 | micromark('a {b { c } d', {extensions: [mdxExpression()]}) 834 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 835 | } 836 | ) 837 | 838 | await t.test( 839 | 'should support a line ending in an expression', 840 | async function () { 841 | assert.equal( 842 | micromark('a {\n} b', { 843 | extensions: [mdxExpression()], 844 | htmlExtensions: [html] 845 | }), 846 | '

a b

' 847 | ) 848 | } 849 | ) 850 | 851 | await t.test('should support just a closing brace', async function () { 852 | assert.equal( 853 | micromark('a } b', { 854 | extensions: [mdxExpression()], 855 | htmlExtensions: [html] 856 | }), 857 | '

a } b

' 858 | ) 859 | }) 860 | 861 | await t.test( 862 | 'should support expressions as the first thing when following by other things', 863 | async function () { 864 | assert.equal( 865 | micromark('{ a } b', { 866 | extensions: [mdxExpression()], 867 | htmlExtensions: [html] 868 | }), 869 | '

b

' 870 | ) 871 | } 872 | ) 873 | 874 | // Note: `markdown-rs` tests for mdast are in `mdast-util-mdx-expression`. 875 | }) 876 | 877 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_text.rs` 878 | // as `fn mdx_expression_text_gnostic()`. 879 | test('text (gnostic)', async function (t) { 880 | await t.test('should support an expression', async function () { 881 | assert.equal( 882 | micromark('a {b} c', { 883 | extensions: [mdxExpression({acorn})], 884 | htmlExtensions: [html] 885 | }), 886 | '

a c

' 887 | ) 888 | }) 889 | 890 | await t.test('should crash on an incorrect expression', async function () { 891 | assert.throws(function () { 892 | micromark('a {??} b', {extensions: [mdxExpression({acorn})]}) 893 | }, /Could not parse expression with acorn/) 894 | }) 895 | 896 | await t.test('should support an empty expression', async function () { 897 | assert.equal( 898 | micromark('a {} b', { 899 | extensions: [mdxExpression({acorn})], 900 | htmlExtensions: [html] 901 | }), 902 | '

a b

' 903 | ) 904 | }) 905 | 906 | await t.test( 907 | 'should crash if no closing brace is found (1)', 908 | async function () { 909 | assert.throws(function () { 910 | micromark('a {b c', {extensions: [mdxExpression({acorn})]}) 911 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 912 | } 913 | ) 914 | 915 | await t.test( 916 | 'should crash if no closing brace is found (2)', 917 | async function () { 918 | assert.throws(function () { 919 | micromark('a {b { c } d', {extensions: [mdxExpression({acorn})]}) 920 | }, /Could not parse expression with acorn/) 921 | } 922 | ) 923 | 924 | await t.test( 925 | 'should support a line ending in an expression', 926 | async function () { 927 | assert.equal( 928 | micromark('a {\n} b', { 929 | extensions: [mdxExpression({acorn})], 930 | htmlExtensions: [html] 931 | }), 932 | '

a b

' 933 | ) 934 | } 935 | ) 936 | 937 | await t.test('should support just a closing brace', async function () { 938 | assert.equal( 939 | micromark('a } b', { 940 | extensions: [mdxExpression({acorn})], 941 | htmlExtensions: [html] 942 | }), 943 | '

a } b

' 944 | ) 945 | }) 946 | 947 | await t.test( 948 | 'should support expressions as the first thing when following by other things', 949 | async function () { 950 | assert.equal( 951 | micromark('{ a } b', { 952 | extensions: [mdxExpression({acorn})], 953 | htmlExtensions: [html] 954 | }), 955 | '

b

' 956 | ) 957 | } 958 | ) 959 | 960 | await t.test( 961 | 'should support an unbalanced opening brace (if JS permits)', 962 | async function () { 963 | assert.equal( 964 | micromark('a { /* { */ } b', { 965 | extensions: [mdxExpression({acorn})], 966 | htmlExtensions: [html] 967 | }), 968 | '

a b

' 969 | ) 970 | } 971 | ) 972 | 973 | await t.test( 974 | 'should support an unbalanced closing brace (if JS permits)', 975 | async function () { 976 | assert.equal( 977 | micromark('a { /* } */ } b', { 978 | extensions: [mdxExpression({acorn})], 979 | htmlExtensions: [html] 980 | }), 981 | '

a b

' 982 | ) 983 | } 984 | ) 985 | 986 | await t.test( 987 | 'should keep the correct number of spaces in a blockquote (text)', 988 | function () { 989 | /** @type {Program | undefined} */ 990 | let program 991 | 992 | micromark( 993 | '> alpha {`\n> bravo\n> charlie\n> delta\n> echo\n> `} foxtrot.', 994 | { 995 | extensions: [ 996 | createExtensionFromFactoryOptions( 997 | acorn, 998 | {ecmaVersion: 'latest'}, 999 | true 1000 | ) 1001 | ], 1002 | htmlExtensions: [{enter: {expression}}] 1003 | } 1004 | ) 1005 | 1006 | assert(program) 1007 | const statement = program.body[0] 1008 | assert(statement.type === 'ExpressionStatement') 1009 | assert(statement.expression.type === 'TemplateLiteral') 1010 | const quasi = statement.expression.quasis[0] 1011 | assert(quasi) 1012 | const value = quasi.value.cooked 1013 | assert.equal(value, '\nbravo\ncharlie\ndelta\n echo\n') 1014 | 1015 | /** 1016 | * @this {CompileContext} 1017 | * @type {Handle} 1018 | */ 1019 | function expression(token) { 1020 | program = token.estree 1021 | } 1022 | } 1023 | ) 1024 | }) 1025 | 1026 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_flow.rs`. 1027 | test('flow (agnostic)', async function (t) { 1028 | await t.test('should support an expression', async function () { 1029 | assert.equal( 1030 | micromark('{a}', {extensions: [mdxExpression()], htmlExtensions: [html]}), 1031 | '' 1032 | ) 1033 | }) 1034 | 1035 | await t.test('should support an empty expression', async function () { 1036 | assert.equal( 1037 | micromark('{}', {extensions: [mdxExpression()], htmlExtensions: [html]}), 1038 | '' 1039 | ) 1040 | }) 1041 | 1042 | // Note: in MDX, indented code is turned off: 1043 | await t.test( 1044 | 'should prefer indented code over expressions if it’s enabled', 1045 | async function () { 1046 | assert.equal( 1047 | micromark(' {}', { 1048 | extensions: [mdxExpression()], 1049 | htmlExtensions: [html] 1050 | }), 1051 | '
{}\n
' 1052 | ) 1053 | } 1054 | ) 1055 | 1056 | await t.test( 1057 | 'should support indented expressions if indented code is enabled', 1058 | async function () { 1059 | assert.equal( 1060 | micromark(' {}', { 1061 | extensions: [mdxExpression()], 1062 | htmlExtensions: [html] 1063 | }), 1064 | '' 1065 | ) 1066 | } 1067 | ) 1068 | 1069 | await t.test( 1070 | 'should crash if no closing brace is found (1)', 1071 | async function () { 1072 | assert.throws(function () { 1073 | micromark('{a', {extensions: [mdxExpression()]}) 1074 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 1075 | } 1076 | ) 1077 | 1078 | await t.test( 1079 | 'should crash if no closing brace is found (2)', 1080 | async function () { 1081 | assert.throws(function () { 1082 | micromark('{b { c }', {extensions: [mdxExpression()]}) 1083 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 1084 | } 1085 | ) 1086 | 1087 | await t.test( 1088 | 'should support a line ending in an expression', 1089 | async function () { 1090 | assert.equal( 1091 | micromark('{\n}\na', { 1092 | extensions: [mdxExpression()], 1093 | htmlExtensions: [html] 1094 | }), 1095 | '

a

' 1096 | ) 1097 | } 1098 | ) 1099 | 1100 | await t.test( 1101 | 'should support expressions followed by spaces', 1102 | async function () { 1103 | assert.equal( 1104 | micromark('{ a } \t\nb', { 1105 | extensions: [mdxExpression()], 1106 | htmlExtensions: [html] 1107 | }), 1108 | '

b

' 1109 | ) 1110 | } 1111 | ) 1112 | 1113 | await t.test( 1114 | 'should support expressions preceded by spaces', 1115 | async function () { 1116 | assert.equal( 1117 | micromark(' { a }\nb', { 1118 | extensions: [mdxExpression()], 1119 | htmlExtensions: [html] 1120 | }), 1121 | '

b

' 1122 | ) 1123 | } 1124 | ) 1125 | 1126 | await t.test( 1127 | 'should support lists after non-expressions (wooorm/markdown-rs#11)', 1128 | async function () { 1129 | assert.equal( 1130 | micromark('a\n\n* b', { 1131 | extensions: [mdxExpression()], 1132 | htmlExtensions: [html] 1133 | }), 1134 | '

a

\n
    \n
  • b
  • \n
' 1135 | ) 1136 | } 1137 | ) 1138 | 1139 | await t.test('should not support lazyness (1)', async function () { 1140 | assert.throws(function () { 1141 | micromark('> {a\nb}', {extensions: [mdxExpression()]}) 1142 | }, /Unexpected lazy line in expression in container/) 1143 | }) 1144 | 1145 | await t.test('should not support lazyness (2)', async function () { 1146 | assert.equal( 1147 | micromark('> a\n{b}', { 1148 | extensions: [mdxExpression()], 1149 | htmlExtensions: [html] 1150 | }), 1151 | '
\n

a

\n
\n' 1152 | ) 1153 | }) 1154 | 1155 | await t.test('should not support lazyness (3)', async function () { 1156 | assert.equal( 1157 | micromark('> {a}\nb', { 1158 | extensions: [mdxExpression()], 1159 | htmlExtensions: [html] 1160 | }), 1161 | '
\n
\n

b

' 1162 | ) 1163 | }) 1164 | 1165 | await t.test('should not support lazyness (4)', async function () { 1166 | assert.throws(function () { 1167 | micromark('> {\n> a\nb}', {extensions: [mdxExpression()]}) 1168 | }, /Unexpected lazy line in expression in container/) 1169 | }) 1170 | 1171 | // Note: `markdown-rs` tests for mdast are in `mdast-util-mdx-expression`. 1172 | }) 1173 | 1174 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_flow.rs`. 1175 | test('flow (gnostic)', async function (t) { 1176 | await t.test('should support an expression', async function () { 1177 | assert.equal( 1178 | micromark('{a}', { 1179 | extensions: [mdxExpression({acorn})], 1180 | htmlExtensions: [html] 1181 | }), 1182 | '' 1183 | ) 1184 | }) 1185 | 1186 | await t.test('should support an empty expression', async function () { 1187 | assert.equal( 1188 | micromark('{}', { 1189 | extensions: [mdxExpression({acorn})], 1190 | htmlExtensions: [html] 1191 | }), 1192 | '' 1193 | ) 1194 | }) 1195 | 1196 | await t.test( 1197 | 'should crash if no closing brace is found (1)', 1198 | async function () { 1199 | assert.throws(function () { 1200 | micromark('{a', {extensions: [mdxExpression({acorn})]}) 1201 | }, /Unexpected end of file in expression, expected a corresponding closing brace for `{`/) 1202 | } 1203 | ) 1204 | 1205 | await t.test( 1206 | 'should crash if no closing brace is found (2)', 1207 | async function () { 1208 | assert.throws(function () { 1209 | micromark('{b { c }', {extensions: [mdxExpression({acorn})]}) 1210 | }, /Could not parse expression with acorn/) 1211 | } 1212 | ) 1213 | 1214 | await t.test( 1215 | 'should support a line ending in an expression', 1216 | async function () { 1217 | assert.equal( 1218 | micromark('{\n}\na', { 1219 | extensions: [mdxExpression({acorn})], 1220 | htmlExtensions: [html] 1221 | }), 1222 | '

a

' 1223 | ) 1224 | } 1225 | ) 1226 | 1227 | await t.test( 1228 | 'should support expressions followed by spaces', 1229 | async function () { 1230 | assert.equal( 1231 | micromark('{ a } \t\nb', { 1232 | extensions: [mdxExpression({acorn})], 1233 | htmlExtensions: [html] 1234 | }), 1235 | '

b

' 1236 | ) 1237 | } 1238 | ) 1239 | 1240 | await t.test( 1241 | 'should support expressions preceded by spaces', 1242 | async function () { 1243 | assert.equal( 1244 | micromark(' { a }\nb', { 1245 | extensions: [mdxExpression({acorn})], 1246 | htmlExtensions: [html] 1247 | }), 1248 | '

b

' 1249 | ) 1250 | } 1251 | ) 1252 | 1253 | await t.test('should support indented expressions', async function () { 1254 | assert.equal( 1255 | micromark(' {`\n a\n `}', { 1256 | extensions: [mdxExpression({acorn})], 1257 | htmlExtensions: [html] 1258 | }), 1259 | '' 1260 | ) 1261 | }) 1262 | 1263 | await t.test( 1264 | 'should support expressions padded w/ parens', 1265 | async function () { 1266 | assert.equal( 1267 | micromark('a{(b)}c', { 1268 | extensions: [mdxExpression({acorn})], 1269 | htmlExtensions: [html] 1270 | }), 1271 | '

ac

' 1272 | ) 1273 | } 1274 | ) 1275 | 1276 | await t.test( 1277 | 'should support expressions padded w/ parens and comments', 1278 | async function () { 1279 | assert.equal( 1280 | micromark('a{/* b */ ( (c) /* d */ + (e) )}f', { 1281 | extensions: [mdxExpression({acorn})], 1282 | htmlExtensions: [html] 1283 | }), 1284 | '

af

' 1285 | ) 1286 | } 1287 | ) 1288 | 1289 | await t.test( 1290 | 'should use correct positional info when tabs are used (1, indent)', 1291 | function () { 1292 | const micromarkExample = 'ab {`\n\t`}' 1293 | const acornExample = 'a = `\n` ' 1294 | /** @type {Array} */ 1295 | const micromarkTokens = [] 1296 | /** @type {Array} */ 1297 | const acornTokens = [] 1298 | /** @type {Program | undefined} */ 1299 | let program 1300 | 1301 | acorn.parseExpressionAt(acornExample, 0, { 1302 | ecmaVersion: 'latest', 1303 | onToken: acornTokens, 1304 | locations: true, 1305 | ranges: true 1306 | }) 1307 | 1308 | micromark(micromarkExample, { 1309 | extensions: [ 1310 | createExtensionFromFactoryOptions( 1311 | acorn, 1312 | {ecmaVersion: 'latest', onToken: micromarkTokens}, 1313 | true, 1314 | false, 1315 | false, 1316 | true 1317 | ) 1318 | ], 1319 | htmlExtensions: [{enter: {expression}}] 1320 | }) 1321 | 1322 | if (program) removeOffsets(program) 1323 | removeOffsetsFromTokens(micromarkTokens) 1324 | 1325 | // Remove: `a`, `=` 1326 | acornTokens.splice(0, 2) 1327 | 1328 | assert.deepEqual(JSON.parse(JSON.stringify(micromarkTokens)), [ 1329 | { 1330 | type: { 1331 | label: '`', 1332 | beforeExpr: false, 1333 | startsExpr: true, 1334 | isLoop: false, 1335 | isAssign: false, 1336 | prefix: false, 1337 | postfix: false, 1338 | binop: null 1339 | }, 1340 | start: 4, 1341 | end: 5, 1342 | loc: {start: {line: 1, column: 4}, end: {line: 1, column: 5}}, 1343 | range: [4, 5] 1344 | }, 1345 | { 1346 | type: { 1347 | label: 'template', 1348 | beforeExpr: false, 1349 | startsExpr: false, 1350 | isLoop: false, 1351 | isAssign: false, 1352 | prefix: false, 1353 | postfix: false, 1354 | binop: null, 1355 | updateContext: null 1356 | }, 1357 | value: '\n', 1358 | start: 5, 1359 | end: 7, 1360 | loc: {start: {line: 1, column: 5}, end: {line: 2, column: 1}}, 1361 | range: [5, 7] 1362 | }, 1363 | { 1364 | type: { 1365 | label: '`', 1366 | beforeExpr: false, 1367 | startsExpr: true, 1368 | isLoop: false, 1369 | isAssign: false, 1370 | prefix: false, 1371 | postfix: false, 1372 | binop: null 1373 | }, 1374 | start: 7, 1375 | end: 8, 1376 | loc: {start: {line: 2, column: 1}, end: {line: 2, column: 2}}, 1377 | range: [7, 8] 1378 | } 1379 | ]) 1380 | 1381 | assert.deepEqual(JSON.parse(JSON.stringify(program)), { 1382 | type: 'Program', 1383 | start: 4, 1384 | end: 8, 1385 | body: [ 1386 | { 1387 | type: 'ExpressionStatement', 1388 | expression: { 1389 | type: 'TemplateLiteral', 1390 | start: 4, 1391 | end: 8, 1392 | expressions: [], 1393 | quasis: [ 1394 | { 1395 | type: 'TemplateElement', 1396 | start: 5, 1397 | end: 7, 1398 | value: {raw: '\n', cooked: '\n'}, 1399 | tail: true, 1400 | loc: { 1401 | start: {line: 1, column: 5}, 1402 | end: {line: 2, column: 1} 1403 | }, 1404 | range: [5, 7] 1405 | } 1406 | ], 1407 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 2}}, 1408 | range: [4, 8] 1409 | }, 1410 | start: 4, 1411 | end: 8, 1412 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 2}}, 1413 | range: [4, 8] 1414 | } 1415 | ], 1416 | sourceType: 'module', 1417 | comments: [], 1418 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 2}}, 1419 | range: [4, 8] 1420 | }) 1421 | 1422 | /** 1423 | * @this {CompileContext} 1424 | * @type {Handle} 1425 | */ 1426 | function expression(token) { 1427 | program = token.estree 1428 | } 1429 | } 1430 | ) 1431 | 1432 | await t.test( 1433 | 'should use correct positional info when tabs are used (2, content)', 1434 | function () { 1435 | const micromarkExample = 'ab {`\nalpha\t`}' 1436 | const acornExample = 'a = `\nalpha\t`' 1437 | /** @type {Array} */ 1438 | const micromarkTokens = [] 1439 | /** @type {Array} */ 1440 | const acornTokens = [] 1441 | /** @type {Program | undefined} */ 1442 | let program 1443 | 1444 | acorn.parseExpressionAt(acornExample, 0, { 1445 | ecmaVersion: 'latest', 1446 | onToken: acornTokens, 1447 | locations: true, 1448 | ranges: true 1449 | }) 1450 | 1451 | micromark(micromarkExample, { 1452 | extensions: [ 1453 | createExtensionFromFactoryOptions( 1454 | acorn, 1455 | {ecmaVersion: 'latest', onToken: micromarkTokens}, 1456 | true, 1457 | false, 1458 | false, 1459 | true 1460 | ) 1461 | ], 1462 | htmlExtensions: [{enter: {expression}}] 1463 | }) 1464 | 1465 | if (program) removeOffsets(program) 1466 | removeOffsetsFromTokens(micromarkTokens) 1467 | 1468 | // Remove: `a`, `=` 1469 | acornTokens.splice(0, 2) 1470 | 1471 | assert.deepEqual( 1472 | JSON.parse(JSON.stringify(micromarkTokens)), 1473 | JSON.parse(JSON.stringify(acornTokens)) 1474 | ) 1475 | 1476 | assert.deepEqual(JSON.parse(JSON.stringify(program)), { 1477 | type: 'Program', 1478 | start: 4, 1479 | end: 13, 1480 | body: [ 1481 | { 1482 | type: 'ExpressionStatement', 1483 | expression: { 1484 | type: 'TemplateLiteral', 1485 | start: 4, 1486 | end: 13, 1487 | expressions: [], 1488 | quasis: [ 1489 | { 1490 | type: 'TemplateElement', 1491 | start: 5, 1492 | end: 12, 1493 | value: {raw: '\nalpha\t', cooked: '\nalpha\t'}, 1494 | tail: true, 1495 | loc: { 1496 | start: {line: 1, column: 5}, 1497 | end: {line: 2, column: 6} 1498 | }, 1499 | range: [5, 12] 1500 | } 1501 | ], 1502 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 7}}, 1503 | range: [4, 13] 1504 | }, 1505 | start: 4, 1506 | end: 13, 1507 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 7}}, 1508 | range: [4, 13] 1509 | } 1510 | ], 1511 | sourceType: 'module', 1512 | comments: [], 1513 | loc: {start: {line: 1, column: 4}, end: {line: 2, column: 7}}, 1514 | range: [4, 13] 1515 | }) 1516 | 1517 | /** 1518 | * @this {CompileContext} 1519 | * @type {Handle} 1520 | */ 1521 | function expression(token) { 1522 | program = token.estree 1523 | } 1524 | } 1525 | ) 1526 | 1527 | await t.test( 1528 | 'should support template strings in JSX (text) in block quotes', 1529 | function () { 1530 | /** @type {Program | undefined} */ 1531 | let program 1532 | 1533 | micromark('> aaa d\n> `} /> eee', { 1534 | extensions: [ 1535 | createExtensionFromFactoryOptions( 1536 | acorn, 1537 | {ecmaVersion: 'latest'}, 1538 | true, 1539 | false, 1540 | false, 1541 | true 1542 | ) 1543 | ], 1544 | htmlExtensions: [{enter: {expression}}] 1545 | }) 1546 | 1547 | if (program) removeOffsets(program) 1548 | 1549 | assert.deepEqual(JSON.parse(JSON.stringify(program)), { 1550 | type: 'Program', 1551 | start: 13, 1552 | end: 28, 1553 | body: [ 1554 | { 1555 | type: 'ExpressionStatement', 1556 | expression: { 1557 | type: 'TemplateLiteral', 1558 | start: 13, 1559 | end: 28, 1560 | expressions: [], 1561 | quasis: [ 1562 | { 1563 | type: 'TemplateElement', 1564 | start: 14, 1565 | end: 27, 1566 | value: {raw: '\n d\n', cooked: '\n d\n'}, 1567 | tail: true, 1568 | loc: { 1569 | start: {line: 1, column: 14}, 1570 | end: {line: 3, column: 3} 1571 | }, 1572 | range: [14, 27] 1573 | } 1574 | ], 1575 | loc: {start: {line: 1, column: 13}, end: {line: 3, column: 4}}, 1576 | range: [13, 28] 1577 | }, 1578 | start: 13, 1579 | end: 28, 1580 | loc: {start: {line: 1, column: 13}, end: {line: 3, column: 4}}, 1581 | range: [13, 28] 1582 | } 1583 | ], 1584 | sourceType: 'module', 1585 | comments: [], 1586 | loc: {start: {line: 1, column: 13}, end: {line: 3, column: 4}}, 1587 | range: [13, 28] 1588 | }) 1589 | 1590 | /** 1591 | * @this {CompileContext} 1592 | * @type {Handle} 1593 | */ 1594 | function expression(token) { 1595 | program = token.estree 1596 | } 1597 | } 1598 | ) 1599 | 1600 | await t.test( 1601 | 'should use correct positional when there are virtual spaces due to a block quote', 1602 | function () { 1603 | // Note: we drop the entire tab in this case, even though it represents 3 1604 | // spaces, where the first is eaten by the block quote. 1605 | // I believe it would be too complex for users to understand that two spaces 1606 | // are passed to acorn and present in template strings. 1607 | const micromarkExample = '> ab {`\n>\t`}' 1608 | const acornExample = '`\n`' 1609 | /** @type {Array} */ 1610 | const micromarkTokens = [] 1611 | /** @type {Array} */ 1612 | const acornTokens = [] 1613 | /** @type {Program | undefined} */ 1614 | let program 1615 | 1616 | acorn.parseExpressionAt(acornExample, 0, { 1617 | ecmaVersion: 'latest', 1618 | onToken: acornTokens, 1619 | locations: true, 1620 | ranges: true 1621 | }) 1622 | 1623 | micromark(micromarkExample, { 1624 | extensions: [ 1625 | createExtensionFromFactoryOptions( 1626 | acorn, 1627 | {ecmaVersion: 'latest', onToken: micromarkTokens}, 1628 | true, 1629 | false, 1630 | false, 1631 | true 1632 | ) 1633 | ], 1634 | htmlExtensions: [{enter: {expression}}] 1635 | }) 1636 | 1637 | if (program) removeOffsets(program) 1638 | removeOffsetsFromTokens(micromarkTokens) 1639 | 1640 | assert(acornTokens.length === 3) 1641 | // `` ` `` 1642 | acornTokens[0].start = 6 1643 | assert(acornTokens[0].loc) 1644 | acornTokens[0].loc.start.column = 6 1645 | acornTokens[0].end = 7 1646 | acornTokens[0].loc.end.column = 7 1647 | acornTokens[0].range = [6, 7] 1648 | // `template` 1649 | acornTokens[1].start = 7 1650 | assert(acornTokens[1].loc) 1651 | acornTokens[1].loc.start.column = 7 1652 | acornTokens[1].end = 10 1653 | acornTokens[1].loc.end.column = 2 1654 | acornTokens[1].range = [7, 10] 1655 | // `` ` `` 1656 | acornTokens[2].start = 10 1657 | assert(acornTokens[2].loc) 1658 | acornTokens[2].loc.start.column = 2 1659 | acornTokens[2].end = 11 1660 | acornTokens[2].loc.end.column = 3 1661 | acornTokens[2].range = [10, 11] 1662 | 1663 | assert.deepEqual( 1664 | JSON.parse(JSON.stringify(micromarkTokens)), 1665 | JSON.parse(JSON.stringify(acornTokens)) 1666 | ) 1667 | 1668 | assert.deepEqual( 1669 | JSON.parse(JSON.stringify(program)), 1670 | JSON.parse( 1671 | JSON.stringify({ 1672 | type: 'Program', 1673 | start: 6, 1674 | end: 11, 1675 | body: [ 1676 | { 1677 | type: 'ExpressionStatement', 1678 | expression: { 1679 | type: 'TemplateLiteral', 1680 | start: 6, 1681 | end: 11, 1682 | expressions: [], 1683 | quasis: [ 1684 | { 1685 | type: 'TemplateElement', 1686 | start: 7, 1687 | end: 10, 1688 | value: {raw: '\n', cooked: '\n'}, 1689 | tail: true, 1690 | loc: { 1691 | start: {line: 1, column: 7}, 1692 | end: {line: 2, column: 2} 1693 | }, 1694 | range: [7, 10] 1695 | } 1696 | ], 1697 | loc: { 1698 | start: {line: 1, column: 6}, 1699 | end: {line: 2, column: 3} 1700 | }, 1701 | range: [6, 11] 1702 | }, 1703 | start: 6, 1704 | end: 11, 1705 | loc: {start: {line: 1, column: 6}, end: {line: 2, column: 3}}, 1706 | range: [6, 11] 1707 | } 1708 | ], 1709 | sourceType: 'module', 1710 | comments: [], 1711 | loc: {start: {line: 1, column: 6}, end: {line: 2, column: 3}}, 1712 | range: [6, 11] 1713 | }) 1714 | ) 1715 | ) 1716 | 1717 | /** 1718 | * @this {CompileContext} 1719 | * @type {Handle} 1720 | */ 1721 | function expression(token) { 1722 | program = token.estree 1723 | } 1724 | } 1725 | ) 1726 | 1727 | await t.test( 1728 | 'should keep the correct number of spaces in a blockquote (flow)', 1729 | function () { 1730 | /** @type {Program | undefined} */ 1731 | let program 1732 | 1733 | micromark('> {`\n> alpha\n> bravo\n> charlie\n> delta\n> `}', { 1734 | extensions: [ 1735 | createExtensionFromFactoryOptions( 1736 | acorn, 1737 | {ecmaVersion: 'latest'}, 1738 | true 1739 | ) 1740 | ], 1741 | htmlExtensions: [{enter: {expression}}] 1742 | }) 1743 | 1744 | assert(program) 1745 | const statement = program.body[0] 1746 | assert(statement.type === 'ExpressionStatement') 1747 | assert(statement.expression.type === 'TemplateLiteral') 1748 | const quasi = statement.expression.quasis[0] 1749 | assert(quasi) 1750 | const value = quasi.value.cooked 1751 | assert.equal(value, '\nalpha\nbravo\ncharlie\n delta\n') 1752 | 1753 | /** 1754 | * @this {CompileContext} 1755 | * @type {Handle} 1756 | */ 1757 | function expression(token) { 1758 | program = token.estree 1759 | } 1760 | } 1761 | ) 1762 | 1763 | await t.test('should support `\\0` and `\\r` in expressions', function () { 1764 | /** @type {Program | undefined} */ 1765 | let program 1766 | 1767 | micromark('{`a\0b\rc\nd\r\ne`}', { 1768 | extensions: [ 1769 | createExtensionFromFactoryOptions(acorn, {ecmaVersion: 'latest'}, true) 1770 | ], 1771 | htmlExtensions: [{enter: {expression}}] 1772 | }) 1773 | 1774 | assert(program) 1775 | const statement = program.body[0] 1776 | assert(statement.type === 'ExpressionStatement') 1777 | assert(statement.expression.type === 'TemplateLiteral') 1778 | const quasi = statement.expression.quasis[0] 1779 | assert(quasi) 1780 | const value = quasi.value.cooked 1781 | assert.equal(value, 'a�b\nc\nd\ne') 1782 | 1783 | /** 1784 | * @this {CompileContext} 1785 | * @type {Handle} 1786 | */ 1787 | function expression(token) { 1788 | program = token.estree 1789 | } 1790 | }) 1791 | }) 1792 | 1793 | // Note: these tests are also in `wooorm/markdown-rs` at `tests/mdx_expression_flow.rs`. 1794 | // That project includes *all* extensions which means that it can use JSX. 1795 | // Here we test something that does not exist in actual MDX but which is used 1796 | // by the JSX extension. 1797 | test('spread (hidden)', async function (t) { 1798 | await t.test('should support a spread', async function () { 1799 | assert.equal( 1800 | micromark('a {...b} c', { 1801 | extensions: [mdxExpression({acorn, spread: true})], 1802 | htmlExtensions: [html] 1803 | }), 1804 | '

a c

' 1805 | ) 1806 | }) 1807 | 1808 | await t.test('should crash if not a spread', async function () { 1809 | assert.throws(function () { 1810 | micromark('a {b} c', { 1811 | extensions: [mdxExpression({acorn, spread: true})] 1812 | }) 1813 | }, /Unexpected `Property` in code: only spread elements are supported/) 1814 | }) 1815 | 1816 | await t.test('should crash on an incorrect spread', async function () { 1817 | assert.throws(function () { 1818 | micromark('a {...?} c', { 1819 | extensions: [mdxExpression({acorn, spread: true})] 1820 | }) 1821 | }, /Could not parse expression with acorn/) 1822 | }) 1823 | 1824 | await t.test( 1825 | 'should crash on an incorrect spread that looks like an assignment', 1826 | async function () { 1827 | assert.throws(function () { 1828 | micromark('a {b=c}={} d', { 1829 | extensions: [mdxExpression({acorn, spread: true})] 1830 | }) 1831 | }, /Unexpected `ExpressionStatement` in code: expected an object spread/) 1832 | } 1833 | ) 1834 | 1835 | await t.test('should crash if a spread and other things', async function () { 1836 | assert.throws(function () { 1837 | micromark('a {...b,c} d', { 1838 | extensions: [mdxExpression({acorn, spread: true})] 1839 | }) 1840 | }, /Unexpected extra content in spread: only a single spread is supported/) 1841 | }) 1842 | 1843 | await t.test('should crash if not an identifier', async function () { 1844 | assert.throws(function () { 1845 | micromark('a {b=c} d', { 1846 | extensions: [mdxExpression({acorn, spread: true})] 1847 | }) 1848 | }, /Could not parse expression with acorn/) 1849 | }) 1850 | 1851 | // Note: `markdown-rs` has no `allowEmpty`. 1852 | await t.test( 1853 | 'should crash on an empty spread (w/ `allowEmpty: false`)', 1854 | async function () { 1855 | assert.throws(function () { 1856 | micromark('a {} b', { 1857 | extensions: [mdxExpression({acorn, spread: true, allowEmpty: false})] 1858 | }) 1859 | }, /Unexpected empty expression/) 1860 | } 1861 | ) 1862 | 1863 | await t.test('should support an empty spread by default', async function () { 1864 | assert.equal( 1865 | micromark('a {} b', { 1866 | extensions: [mdxExpression({acorn, spread: true})], 1867 | htmlExtensions: [html] 1868 | }), 1869 | '

a b

' 1870 | ) 1871 | }) 1872 | 1873 | await t.test( 1874 | 'should crash on a comment spread (w/ `allowEmpty: false`)', 1875 | async function () { 1876 | assert.throws(function () { 1877 | micromark('a {/* b */} c', { 1878 | extensions: [mdxExpression({acorn, spread: true, allowEmpty: false})] 1879 | }) 1880 | }, /Unexpected empty expression/) 1881 | } 1882 | ) 1883 | 1884 | await t.test('should support a comment spread by default', async function () { 1885 | assert.equal( 1886 | micromark('a {/* b */} c', { 1887 | extensions: [mdxExpression({acorn, spread: true})], 1888 | htmlExtensions: [html] 1889 | }), 1890 | '

a c

' 1891 | ) 1892 | }) 1893 | 1894 | await t.test( 1895 | 'should crash if not a spread w/ `allowEmpty`', 1896 | async function () { 1897 | assert.throws(function () { 1898 | micromark('{a=b}', { 1899 | extensions: [mdxExpression({acorn, spread: true, allowEmpty: false})] 1900 | }) 1901 | }, /Could not parse expression with acorn/) 1902 | } 1903 | ) 1904 | }) 1905 | 1906 | /** 1907 | * @param {Acorn | null | undefined} [acorn] 1908 | * Object with `acorn.parse` and `acorn.parseExpressionAt`. 1909 | * @param {AcornOptions | null | undefined} [acornOptions] 1910 | * Configuration for acorn (optional). 1911 | * @param {boolean | null | undefined} [addResult=false] 1912 | * Add `estree` to token (default: `false`). 1913 | * @param {boolean | null | undefined} [spread=false] 1914 | * Support a spread (`{...a}`) only (default: `false`). 1915 | * @param {boolean | null | undefined} [allowEmpty=false] 1916 | * Support an empty expression (default: `false`). 1917 | * @param {boolean | null | undefined} [allowLazy=false] 1918 | * Support lazy continuation of an expression (default: `false`). 1919 | * @returns {Extension} 1920 | * Expression. 1921 | */ 1922 | // eslint-disable-next-line max-params 1923 | function createExtensionFromFactoryOptions( 1924 | acorn, 1925 | acornOptions, 1926 | addResult, 1927 | spread, 1928 | allowEmpty, 1929 | allowLazy 1930 | ) { 1931 | return { 1932 | flow: { 1933 | [codes.leftCurlyBrace]: {tokenize: tokenizeFlow, concrete: true} 1934 | }, 1935 | text: {[codes.leftCurlyBrace]: {tokenize: tokenizeText}} 1936 | } 1937 | 1938 | /** 1939 | * @this {TokenizeContext} 1940 | * @type {Tokenizer} 1941 | */ 1942 | function tokenizeFlow(effects, ok, nok) { 1943 | const self = this 1944 | 1945 | return start 1946 | 1947 | /** @type {State} */ 1948 | function start(code) { 1949 | return factoryMdxExpression.call( 1950 | self, 1951 | effects, 1952 | after, 1953 | 'expression', 1954 | 'expressionMarker', 1955 | 'expressionChunk', 1956 | acorn, 1957 | acornOptions, 1958 | addResult, 1959 | spread, 1960 | allowEmpty 1961 | )(code) 1962 | } 1963 | 1964 | // Note: trailing whitespace not supported. 1965 | /** @type {State} */ 1966 | function after(code) { 1967 | return code === codes.eof || markdownLineEnding(code) 1968 | ? ok(code) 1969 | : nok(code) 1970 | } 1971 | } 1972 | 1973 | /** 1974 | * @this {TokenizeContext} 1975 | * @type {Tokenizer} 1976 | */ 1977 | function tokenizeText(effects, ok) { 1978 | const self = this 1979 | 1980 | return start 1981 | 1982 | /** @type {State} */ 1983 | function start(code) { 1984 | return factoryMdxExpression.call( 1985 | self, 1986 | effects, 1987 | ok, 1988 | 'expression', 1989 | 'expressionMarker', 1990 | 'expressionChunk', 1991 | acorn, 1992 | acornOptions, 1993 | addResult, 1994 | spread, 1995 | allowEmpty, 1996 | allowLazy 1997 | )(code) 1998 | } 1999 | } 2000 | } 2001 | 2002 | /** 2003 | * @param {Node} node 2004 | * @returns {undefined} 2005 | */ 2006 | function removeOffsets(node) { 2007 | visit(node, function (d) { 2008 | assert(d.loc, 'expected `loc`') 2009 | // @ts-expect-error: we add offsets to our nodes, as we have them. 2010 | delete d.loc.start.offset 2011 | // @ts-expect-error: we add offsets. 2012 | delete d.loc.end.offset 2013 | }) 2014 | } 2015 | 2016 | /** 2017 | * @param {Array} tokens 2018 | * @returns {undefined} 2019 | */ 2020 | function removeOffsetsFromTokens(tokens) { 2021 | for (const d of tokens) { 2022 | // @ts-expect-error: we add offsets to our nodes, as we have them. 2023 | delete d.loc?.start.offset 2024 | // @ts-expect-error: we add offsets. 2025 | delete d.loc?.end.offset 2026 | } 2027 | } 2028 | 2029 | /** 2030 | * @this {CompileContext} 2031 | * @type {Handle} 2032 | */ 2033 | function start() { 2034 | this.buffer() 2035 | } 2036 | 2037 | /** 2038 | * @this {CompileContext} 2039 | * @type {Handle} 2040 | */ 2041 | function end() { 2042 | this.resume() 2043 | this.setData('slurpOneLineEnding', true) 2044 | } 2045 | --------------------------------------------------------------------------------