mirror of
https://github.com/mdx-js/mdx.git
synced 2024-09-19 03:17:10 +03:00
Update @mdx-js/mdx
(#1669)
* Import xdm’s core into `@mdx-js/mdx` * Tons of fixes and updates and a couiple of changes that will be tested and documented later * Use ESM * Add JSDoc based types
This commit is contained in:
parent
22c40f78b3
commit
57e637cf55
@ -22,14 +22,15 @@ extends:
|
||||
- xo
|
||||
- prettier
|
||||
|
||||
rules:
|
||||
react/prop-types: off
|
||||
react/react-in-jsx-scope: off
|
||||
|
||||
settings:
|
||||
react:
|
||||
version: detect
|
||||
|
||||
rules:
|
||||
react/prop-types: off
|
||||
react/react-in-jsx-scope: off
|
||||
prefer-destructuring: off
|
||||
|
||||
overrides:
|
||||
- files:
|
||||
- '**/test/**/*.js'
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -15,6 +15,7 @@ coverage/
|
||||
/public
|
||||
/packages/loader/lib/**/*.d.ts
|
||||
/packages/loader/test/**/*.d.ts
|
||||
/packages/mdx/**/*.d.ts
|
||||
/packages/preact/lib/**/*.d.ts
|
||||
/packages/preact/test/**/*.d.ts
|
||||
/packages/preact/index.d.ts
|
||||
|
4492
package-lock.json
generated
4492
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -44,7 +44,7 @@
|
||||
"lint": "eslint --ext .jsx --report-unused-disable-directives --cache .",
|
||||
"publish-ci": "# lerna publish -y --canary --preid ci --pre-dist-tag ci",
|
||||
"publish-next": "# lerna publish --force-publish=\"*\" --pre-dist-tag next --preid next",
|
||||
"build": "npm run build --workspaces -w packages/loader -w packages/react -w packages/preact --if-present",
|
||||
"build": "npm run build --workspaces -w packages/loader -w packages/mdx -w packages/preact -w packages/react -w packages/remark-mdx -w packages/vue --if-present",
|
||||
"test-api": "npm run test-api --workspaces --if-present",
|
||||
"test-coverage": "npm run test-coverage --workspaces --if-present",
|
||||
"test-types": "npm run test-types --workspaces --if-present",
|
||||
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"presets": [
|
||||
[
|
||||
"@babel/env",
|
||||
{
|
||||
"corejs": 3,
|
||||
"useBuiltIns": "usage"
|
||||
}
|
||||
],
|
||||
"@babel/react"
|
||||
]
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
const astring = require('astring')
|
||||
|
||||
module.exports = estreeToJs
|
||||
|
||||
const customGenerator = {
|
||||
...astring.baseGenerator,
|
||||
JSXAttribute,
|
||||
JSXClosingElement,
|
||||
JSXClosingFragment,
|
||||
JSXElement,
|
||||
JSXEmptyExpression,
|
||||
JSXExpressionContainer,
|
||||
JSXFragment,
|
||||
JSXIdentifier,
|
||||
JSXMemberExpression,
|
||||
JSXNamespacedName,
|
||||
JSXOpeningElement,
|
||||
JSXOpeningFragment,
|
||||
JSXSpreadAttribute,
|
||||
JSXText
|
||||
}
|
||||
|
||||
function estreeToJs(estree) {
|
||||
return astring.generate(estree, {generator: customGenerator})
|
||||
}
|
||||
|
||||
// `attr="something"`
|
||||
function JSXAttribute(node, state) {
|
||||
state.write(' ')
|
||||
this[node.name.type](node.name, state)
|
||||
|
||||
if (node.value !== undefined && node.value !== null) {
|
||||
state.write('=')
|
||||
|
||||
// Encode double quotes in attribute values.
|
||||
if (node.value.type === 'Literal') {
|
||||
state.write(
|
||||
'"' + encodeJsx(String(node.value.value)).replace(/"/g, '"') + '"',
|
||||
node
|
||||
)
|
||||
} else {
|
||||
this[node.value.type](node.value, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// `</div>`
|
||||
function JSXClosingElement(node, state) {
|
||||
this[node.name.type](node.name, state)
|
||||
}
|
||||
|
||||
// `</>`
|
||||
function JSXClosingFragment(node, state) {
|
||||
state.write('</>')
|
||||
}
|
||||
|
||||
// `<div></div>`
|
||||
function JSXElement(node, state) {
|
||||
state.write('<')
|
||||
this[node.openingElement.type](node.openingElement, state)
|
||||
if (node.closingElement) {
|
||||
state.write('>')
|
||||
let index = -1
|
||||
|
||||
while (++index < node.children.length) {
|
||||
this[node.children[index].type](node.children[index], state)
|
||||
}
|
||||
|
||||
state.write('</')
|
||||
this[node.closingElement.type](node.closingElement, state)
|
||||
state.write('>')
|
||||
} else {
|
||||
state.write(' />')
|
||||
}
|
||||
}
|
||||
|
||||
// `<></>`
|
||||
function JSXFragment(node, state) {
|
||||
this[node.openingFragment.type](node.openingElement, state)
|
||||
|
||||
let index = -1
|
||||
|
||||
while (++index < node.children.length) {
|
||||
this[node.children[index].type](node.children[index], state)
|
||||
}
|
||||
|
||||
// Incorrect tree.
|
||||
/* c8 ignore next 3 */
|
||||
if (!node.closingFragment) {
|
||||
throw new Error('Cannot handle fragment w/o closing tag')
|
||||
}
|
||||
|
||||
this[node.closingFragment.type](node.closingElement, state)
|
||||
}
|
||||
|
||||
// `{}`
|
||||
function JSXEmptyExpression() {}
|
||||
|
||||
// `{expression}`
|
||||
function JSXExpressionContainer(node, state) {
|
||||
state.write('{')
|
||||
this[node.expression.type](node.expression, state)
|
||||
state.write('}')
|
||||
}
|
||||
|
||||
// `<div>`
|
||||
function JSXOpeningElement(node, state) {
|
||||
let index = -1
|
||||
|
||||
this[node.name.type](node.name, state)
|
||||
|
||||
while (++index < node.attributes.length) {
|
||||
this[node.attributes[index].type](node.attributes[index], state)
|
||||
}
|
||||
}
|
||||
|
||||
// `<>`
|
||||
function JSXOpeningFragment(node, state) {
|
||||
state.write('<>')
|
||||
}
|
||||
|
||||
// `div`
|
||||
function JSXIdentifier(node, state) {
|
||||
state.write(node.name)
|
||||
}
|
||||
|
||||
// `member.expression`
|
||||
function JSXMemberExpression(node, state) {
|
||||
this[node.object.type](node.object, state)
|
||||
state.write('.')
|
||||
this[node.property.type](node.property, state)
|
||||
}
|
||||
|
||||
// `ns:attr="something"`
|
||||
// MDX (and most JSX things) don’t support them.
|
||||
// But keep it here just in case we might in the future.
|
||||
/* c8 ignore next 5 */
|
||||
function JSXNamespacedName(node, state) {
|
||||
this[node.namespace.type](node.namespace, state)
|
||||
state.write(':')
|
||||
this[node.name.type](node.name, state)
|
||||
}
|
||||
|
||||
// `{...argument}`
|
||||
function JSXSpreadAttribute(node, state) {
|
||||
state.write(' {')
|
||||
/* eslint-disable-next-line new-cap */
|
||||
this.SpreadElement(node, state)
|
||||
state.write('}')
|
||||
}
|
||||
|
||||
// `!`
|
||||
function JSXText(node, state) {
|
||||
// `raw` is currently always be set, but could be
|
||||
// missing if something injects a `JSXText` into the tree.
|
||||
// Preferring `raw` over `value` means character references are kept as-is.
|
||||
/* c8 ignore next */
|
||||
const value = node.raw || node.value
|
||||
state.write(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that character references don’t pop up.
|
||||
* For example, the text `©` should stay that way, and not turn into `©`.
|
||||
* We could encode all `&` (easy but verbose) or look for actual valid
|
||||
* references (complex but cleanest output).
|
||||
* Looking for the 2nd character gives us a middle ground.
|
||||
* The `#` is for (decimal and hexadecimal) numeric references, the letters
|
||||
* are for the named references.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeJsx(value) {
|
||||
return value.replace(/&(?=[#a-z])/gi, '&')
|
||||
}
|
@ -1,55 +1,4 @@
|
||||
const unified = require('unified')
|
||||
const remarkParse = require('remark-parse')
|
||||
const remarkMdx = require('remark-mdx')
|
||||
const squeeze = require('remark-squeeze-paragraphs')
|
||||
const minifyWhitespace = require('rehype-minify-whitespace')
|
||||
const mdxAstToMdxHast = require('./mdx-ast-to-mdx-hast')
|
||||
const mdxHastToJsx = require('./mdx-hast-to-jsx')
|
||||
|
||||
const pragma = `/* @jsxRuntime classic */
|
||||
/* @jsx mdx */
|
||||
/* @jsxFrag mdx.Fragment */`
|
||||
|
||||
function createMdxAstCompiler(options = {}) {
|
||||
return unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkMdx)
|
||||
.use(squeeze)
|
||||
.use(options.remarkPlugins)
|
||||
.use(mdxAstToMdxHast)
|
||||
}
|
||||
|
||||
function createCompiler(options = {}) {
|
||||
return createMdxAstCompiler(options)
|
||||
.use(options.rehypePlugins)
|
||||
.use(minifyWhitespace, {newlines: true})
|
||||
.use(mdxHastToJsx, options)
|
||||
}
|
||||
|
||||
function createConfig(mdx, options) {
|
||||
const config = {contents: mdx}
|
||||
|
||||
if (options.filepath) {
|
||||
config.path = options.filepath
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
function sync(mdx, options = {}) {
|
||||
const file = createCompiler(options).processSync(createConfig(mdx, options))
|
||||
return pragma + '\n' + String(file)
|
||||
}
|
||||
|
||||
async function compile(mdx, options = {}) {
|
||||
const file = await createCompiler(options).process(createConfig(mdx, options))
|
||||
// V8 bug on Node 12.
|
||||
/* c8 ignore next */
|
||||
return pragma + '\n' + String(file)
|
||||
}
|
||||
|
||||
module.exports = compile
|
||||
compile.default = compile
|
||||
compile.sync = sync
|
||||
compile.createMdxAstCompiler = createMdxAstCompiler
|
||||
compile.createCompiler = createCompiler
|
||||
export {createProcessor} from './lib/core.js'
|
||||
export {compile, compileSync} from './lib/compile.js'
|
||||
export {evaluate, evaluateSync} from './lib/evaluate.js'
|
||||
export {nodeTypes} from './lib/node-types.js'
|
||||
|
39
packages/mdx/lib/compile.js
Normal file
39
packages/mdx/lib/compile.js
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('vfile').VFileCompatible} VFileCompatible
|
||||
* @typedef {import('vfile').VFile} VFile
|
||||
* @typedef {import('./core.js').PluginOptions} PluginOptions
|
||||
* @typedef {import('./core.js').BaseProcessorOptions} BaseProcessorOptions
|
||||
* @typedef {Omit<BaseProcessorOptions, 'format'>} CoreProcessorOptions
|
||||
*
|
||||
* @typedef ExtraOptions
|
||||
* @property {'detect'|'mdx'|'md'} [format='detect'] Format of `file`
|
||||
*
|
||||
* @typedef {CoreProcessorOptions & PluginOptions & ExtraOptions} CompileOptions
|
||||
*/
|
||||
|
||||
import {createProcessor} from './core.js'
|
||||
import {resolveFileAndOptions} from './util/resolve-file-and-options.js'
|
||||
|
||||
/**
|
||||
* Compile MDX to JS.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
|
||||
* @param {CompileOptions} [compileOptions]
|
||||
* @return {Promise<VFile>}
|
||||
*/
|
||||
export function compile(vfileCompatible, compileOptions) {
|
||||
const {file, options} = resolveFileAndOptions(vfileCompatible, compileOptions)
|
||||
return createProcessor(options).process(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously compile MDX to JS.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
|
||||
* @param {CompileOptions} [compileOptions]
|
||||
* @return {VFile}
|
||||
*/
|
||||
export function compileSync(vfileCompatible, compileOptions) {
|
||||
const {file, options} = resolveFileAndOptions(vfileCompatible, compileOptions)
|
||||
return createProcessor(options).processSync(file)
|
||||
}
|
93
packages/mdx/lib/core.js
Normal file
93
packages/mdx/lib/core.js
Normal file
@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @typedef {import('unified').Processor} Processor
|
||||
* @typedef {import('unified').PluggableList} PluggableList
|
||||
* @typedef {import('./plugin/recma-document.js').RecmaDocumentOptions} RecmaDocumentOptions
|
||||
* @typedef {import('./plugin/recma-stringify.js').RecmaStringifyOptions} RecmaStringifyOptions
|
||||
* @typedef {import('./plugin/recma-jsx-rewrite.js').RecmaJsxRewriteOptions} RecmaJsxRewriteOptions
|
||||
*
|
||||
* @typedef BaseProcessorOptions
|
||||
* @property {boolean} [jsx=false] Whether to keep JSX
|
||||
* @property {'mdx'|'md'} [format='mdx'] Format of the files to be processed
|
||||
* @property {'program'|'function-body'} [outputFormat='program'] Whether to compile to a whole program or a function body.
|
||||
* @property {string[]} [mdExtensions] Extensions (with `.`) for markdown
|
||||
* @property {string[]} [mdxExtensions] Extensions (with `.`) for MDX
|
||||
* @property {PluggableList} [recmaPlugins] List of recma (esast, JavaScript) plugins
|
||||
* @property {PluggableList} [remarkPlugins] List of remark (mdast, markdown) plugins
|
||||
* @property {PluggableList} [rehypePlugins] List of rehype (hast, HTML) plugins
|
||||
*
|
||||
* @typedef {Omit<RecmaDocumentOptions & RecmaStringifyOptions & RecmaJsxRewriteOptions, 'outputFormat'>} PluginOptions
|
||||
* @typedef {BaseProcessorOptions & PluginOptions} ProcessorOptions
|
||||
*/
|
||||
|
||||
import {unified} from 'unified'
|
||||
import remarkParse from 'remark-parse'
|
||||
import remarkRehype from 'remark-rehype'
|
||||
import {recmaJsxBuild} from './plugin/recma-jsx-build.js'
|
||||
import {recmaDocument} from './plugin/recma-document.js'
|
||||
import {recmaJsxRewrite} from './plugin/recma-jsx-rewrite.js'
|
||||
import {recmaStringify} from './plugin/recma-stringify.js'
|
||||
import {rehypeRecma} from './plugin/rehype-recma.js'
|
||||
import {rehypeRemoveRaw} from './plugin/rehype-remove-raw.js'
|
||||
import {remarkMarkAndUnravel} from './plugin/remark-mark-and-unravel.js'
|
||||
import {remarkMdx} from './plugin/remark-mdx.js'
|
||||
import {nodeTypes} from './node-types.js'
|
||||
|
||||
/**
|
||||
* Pipeline to:
|
||||
*
|
||||
* 1. Parse MDX (serialized markdown with embedded JSX, ESM, and expressions)
|
||||
* 2. Transform through remark (mdast), rehype (hast), and recma (esast)
|
||||
* 3. Serialize as JavaScript
|
||||
*
|
||||
* @param {ProcessorOptions} [options]
|
||||
* @return {Processor}
|
||||
*/
|
||||
export function createProcessor(options = {}) {
|
||||
const {
|
||||
jsx,
|
||||
format,
|
||||
outputFormat,
|
||||
providerImportSource,
|
||||
recmaPlugins,
|
||||
rehypePlugins,
|
||||
remarkPlugins,
|
||||
SourceMapGenerator,
|
||||
...rest
|
||||
} = options
|
||||
|
||||
// @ts-expect-error runtime.
|
||||
if (format === 'detect') {
|
||||
throw new Error(
|
||||
"Incorrect `format: 'detect'`: `createProcessor` can support either `md` or `mdx`; it does not support detecting the format"
|
||||
)
|
||||
}
|
||||
|
||||
const pipeline = unified().use(remarkParse)
|
||||
|
||||
if (format !== 'md') {
|
||||
pipeline.use(remarkMdx)
|
||||
}
|
||||
|
||||
pipeline
|
||||
.use(remarkMarkAndUnravel)
|
||||
.use(remarkPlugins || [])
|
||||
.use(remarkRehype, {allowDangerousHtml: true, passThrough: nodeTypes})
|
||||
.use(rehypePlugins || [])
|
||||
|
||||
if (format === 'md') {
|
||||
pipeline.use(rehypeRemoveRaw)
|
||||
}
|
||||
|
||||
pipeline
|
||||
.use(rehypeRecma)
|
||||
.use(recmaDocument, {...rest, outputFormat})
|
||||
.use(recmaJsxRewrite, {providerImportSource, outputFormat})
|
||||
|
||||
if (!jsx) {
|
||||
pipeline.use(recmaJsxBuild, {outputFormat})
|
||||
}
|
||||
|
||||
pipeline.use(recmaStringify, {SourceMapGenerator}).use(recmaPlugins || [])
|
||||
|
||||
return pipeline
|
||||
}
|
39
packages/mdx/lib/evaluate.js
Normal file
39
packages/mdx/lib/evaluate.js
Normal file
@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @typedef {import('vfile').VFileCompatible} VFileCompatible
|
||||
* @typedef {import('./util/resolve-evaluate-options.js').EvaluateOptions} EvaluateOptions
|
||||
*
|
||||
* @typedef {{[name: string]: any}} ComponentMap
|
||||
* @typedef {{[props: string]: any, components?: ComponentMap}} MDXContentProps
|
||||
* @typedef {(props: MDXContentProps) => any} MDXContent
|
||||
* @typedef {{[exports: string]: unknown, default: MDXContent}} ExportMap
|
||||
*/
|
||||
|
||||
import {compile, compileSync} from './compile.js'
|
||||
import {run, runSync} from './run.js'
|
||||
import {resolveEvaluateOptions} from './util/resolve-evaluate-options.js'
|
||||
|
||||
/**
|
||||
* Evaluate MDX.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
|
||||
* @param {EvaluateOptions} evaluateOptions
|
||||
* @return {Promise<ExportMap>}
|
||||
*/
|
||||
export async function evaluate(vfileCompatible, evaluateOptions) {
|
||||
const {compiletime, runtime} = resolveEvaluateOptions(evaluateOptions)
|
||||
// V8 on Erbium.
|
||||
/* c8 ignore next 2 */
|
||||
return run(await compile(vfileCompatible, compiletime), runtime)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously evaluate MDX.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX document to parse (`string`, `Buffer`, `vfile`, anything that can be given to `vfile`)
|
||||
* @param {EvaluateOptions} evaluateOptions
|
||||
* @return {ExportMap}
|
||||
*/
|
||||
export function evaluateSync(vfileCompatible, evaluateOptions) {
|
||||
const {compiletime, runtime} = resolveEvaluateOptions(evaluateOptions)
|
||||
return runSync(compileSync(vfileCompatible, compiletime), runtime)
|
||||
}
|
9
packages/mdx/lib/node-types.js
Normal file
9
packages/mdx/lib/node-types.js
Normal file
@ -0,0 +1,9 @@
|
||||
// List of node types made by `mdast-util-mdx`, which have to be passed
|
||||
// through untouched from the mdast tree to the hast tree.
|
||||
export const nodeTypes = [
|
||||
'mdxFlowExpression',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxTextExpression',
|
||||
'mdxjsEsm'
|
||||
]
|
523
packages/mdx/lib/plugin/recma-document.js
Normal file
523
packages/mdx/lib/plugin/recma-document.js
Normal file
@ -0,0 +1,523 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Directive} Directive
|
||||
* @typedef {import('estree-jsx').ExportDefaultDeclaration} ExportDefaultDeclaration
|
||||
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
|
||||
* @typedef {import('estree-jsx').ExportNamedDeclaration} ExportNamedDeclaration
|
||||
* @typedef {import('estree-jsx').ExportAllDeclaration} ExportAllDeclaration
|
||||
* @typedef {import('estree-jsx').Expression} Expression
|
||||
* @typedef {import('estree-jsx').FunctionDeclaration} FunctionDeclaration
|
||||
* @typedef {import('estree-jsx').ImportDeclaration} ImportDeclaration
|
||||
* @typedef {import('estree-jsx').JSXElement} JSXElement
|
||||
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
|
||||
* @typedef {import('estree-jsx').Node} Node
|
||||
* @typedef {import('estree-jsx').Program} Program
|
||||
* @typedef {import('estree-jsx').SimpleLiteral} SimpleLiteral
|
||||
* @typedef {import('estree-jsx').Statement} Statement
|
||||
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
|
||||
* @typedef {import('estree-jsx').SpreadElement} SpreadElement
|
||||
* @typedef {import('estree-jsx').Property} Property
|
||||
*
|
||||
* @typedef RecmaDocumentOptions
|
||||
* @property {'program'|'function-body'} [outputFormat='program'] Whether to use either `import` and `export` statements to get the runtime (and optionally provider) and export the content, or get values from `arguments` and return things
|
||||
* @property {boolean} [useDynamicImport=false] Whether to keep `import` (and `export … from`) statements or compile them to dynamic `import()` instead
|
||||
* @property {string} [baseUrl] Resolve relative `import` (and `export … from`) relative to this URL
|
||||
* @property {string} [pragma='React.createElement'] Pragma for JSX (used in classic runtime)
|
||||
* @property {string} [pragmaFrag='React.Fragment'] Pragma for JSX fragments (used in classic runtime)
|
||||
* @property {string} [pragmaImportSource='react'] Where to import the identifier of `pragma` from (used in classic runtime)
|
||||
* @property {string} [jsxImportSource='react'] Place to import automatic JSX runtimes from (used in automatic runtime)
|
||||
* @property {'automatic'|'classic'} [jsxRuntime='automatic'] JSX runtime to use
|
||||
*/
|
||||
|
||||
import {URL} from 'url'
|
||||
import {analyze} from 'periscopic'
|
||||
import {stringifyPosition} from 'unist-util-stringify-position'
|
||||
import {positionFromEstree} from 'unist-util-position-from-estree'
|
||||
import {create} from '../util/estree-util-create.js'
|
||||
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
|
||||
import {declarationToExpression} from '../util/estree-util-declaration-to-expression.js'
|
||||
import {isDeclaration} from '../util/estree-util-is-declaration.js'
|
||||
|
||||
/**
|
||||
* A plugin to wrap the estree in `MDXContent`.
|
||||
*
|
||||
* @type {import('unified').Plugin<[RecmaDocumentOptions]|[], Program>}
|
||||
*/
|
||||
export function recmaDocument(options = {}) {
|
||||
const {
|
||||
baseUrl,
|
||||
useDynamicImport,
|
||||
outputFormat = 'program',
|
||||
pragma = 'React.createElement',
|
||||
pragmaFrag = 'React.Fragment',
|
||||
pragmaImportSource = 'react',
|
||||
jsxImportSource = 'react',
|
||||
jsxRuntime = 'automatic'
|
||||
} = options
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
return (tree, file) => {
|
||||
/** @type {Array.<string|[string, string]>} */
|
||||
const exportedIdentifiers = []
|
||||
/** @type {Array.<Directive|Statement|ModuleDeclaration>} */
|
||||
const replacement = []
|
||||
/** @type {Array.<string>} */
|
||||
const pragmas = []
|
||||
let exportAllCount = 0
|
||||
/** @type {ExportDefaultDeclaration|ExportSpecifier|undefined} */
|
||||
let layout
|
||||
/** @type {boolean|undefined} */
|
||||
let content
|
||||
/** @type {Node} */
|
||||
let child
|
||||
|
||||
// Patch missing comments, which types say could occur.
|
||||
/* c8 ignore next */
|
||||
if (!tree.comments) tree.comments = []
|
||||
|
||||
if (jsxRuntime) {
|
||||
pragmas.push('@jsxRuntime ' + jsxRuntime)
|
||||
}
|
||||
|
||||
if (jsxRuntime === 'automatic' && jsxImportSource) {
|
||||
pragmas.push('@jsxImportSource ' + jsxImportSource)
|
||||
}
|
||||
|
||||
if (jsxRuntime === 'classic' && pragma) {
|
||||
pragmas.push('@jsx ' + pragma)
|
||||
}
|
||||
|
||||
if (jsxRuntime === 'classic' && pragmaFrag) {
|
||||
pragmas.push('@jsxFrag ' + pragmaFrag)
|
||||
}
|
||||
|
||||
if (pragmas.length > 0) {
|
||||
tree.comments.unshift({type: 'Block', value: pragmas.join(' ')})
|
||||
}
|
||||
|
||||
if (jsxRuntime === 'classic' && pragmaImportSource) {
|
||||
if (!pragma) {
|
||||
throw new Error(
|
||||
'Missing `pragma` in classic runtime with `pragmaImportSource`'
|
||||
)
|
||||
}
|
||||
|
||||
handleEsm({
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
{
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: {type: 'Identifier', name: pragma.split('.')[0]}
|
||||
}
|
||||
],
|
||||
source: {type: 'Literal', value: pragmaImportSource}
|
||||
})
|
||||
}
|
||||
|
||||
// Find the `export default`, the JSX expression, and leave the rest
|
||||
// (import/exports) as they are.
|
||||
for (child of tree.body) {
|
||||
// ```js
|
||||
// export default props => <>{props.children}</>
|
||||
// ```
|
||||
//
|
||||
// Treat it as an inline layout declaration.
|
||||
if (child.type === 'ExportDefaultDeclaration') {
|
||||
if (layout) {
|
||||
file.fail(
|
||||
'Cannot specify multiple layouts (previous: ' +
|
||||
stringifyPosition(positionFromEstree(layout)) +
|
||||
')',
|
||||
positionFromEstree(child),
|
||||
'recma-document:duplicate-layout'
|
||||
)
|
||||
}
|
||||
|
||||
layout = child
|
||||
replacement.push({
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {type: 'Identifier', name: 'MDXLayout'},
|
||||
init: isDeclaration(child.declaration)
|
||||
? declarationToExpression(child.declaration)
|
||||
: child.declaration
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
// ```js
|
||||
// export {a, b as c} from 'd'
|
||||
// ```
|
||||
else if (child.type === 'ExportNamedDeclaration' && child.source) {
|
||||
/** @type {SimpleLiteral} */
|
||||
// @ts-expect-error `ExportNamedDeclaration.source` can only be a string literal.
|
||||
const source = child.source
|
||||
|
||||
// Remove `default` or `as default`, but not `default as`, specifier.
|
||||
child.specifiers = child.specifiers.filter((specifier) => {
|
||||
if (specifier.exported.name === 'default') {
|
||||
if (layout) {
|
||||
file.fail(
|
||||
'Cannot specify multiple layouts (previous: ' +
|
||||
stringifyPosition(positionFromEstree(layout)) +
|
||||
')',
|
||||
positionFromEstree(child),
|
||||
'recma-document:duplicate-layout'
|
||||
)
|
||||
}
|
||||
|
||||
layout = specifier
|
||||
|
||||
// Make it just an import: `import MDXLayout from '…'`.
|
||||
handleEsm(
|
||||
create(specifier, {
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
// Default as default / something else as default.
|
||||
specifier.local.name === 'default'
|
||||
? {
|
||||
type: 'ImportDefaultSpecifier',
|
||||
local: {type: 'Identifier', name: 'MDXLayout'}
|
||||
}
|
||||
: create(specifier.local, {
|
||||
type: 'ImportSpecifier',
|
||||
imported: specifier.local,
|
||||
local: {type: 'Identifier', name: 'MDXLayout'}
|
||||
})
|
||||
],
|
||||
source: create(source, {type: 'Literal', value: source.value})
|
||||
})
|
||||
)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// If there are other things imported, keep it.
|
||||
if (child.specifiers.length > 0) {
|
||||
handleExport(child)
|
||||
}
|
||||
}
|
||||
// ```js
|
||||
// export {a, b as c}
|
||||
// export * from 'a'
|
||||
// ```
|
||||
else if (
|
||||
child.type === 'ExportNamedDeclaration' ||
|
||||
child.type === 'ExportAllDeclaration'
|
||||
) {
|
||||
handleExport(child)
|
||||
} else if (child.type === 'ImportDeclaration') {
|
||||
handleEsm(child)
|
||||
} else if (
|
||||
child.type === 'ExpressionStatement' &&
|
||||
// @ts-expect-error types are wrong: `JSXElement`/`JSXFragment` are
|
||||
// `Expression`s.
|
||||
(child.expression.type === 'JSXFragment' ||
|
||||
// @ts-expect-error "
|
||||
child.expression.type === 'JSXElement')
|
||||
) {
|
||||
content = true
|
||||
replacement.push(createMdxContent(child.expression))
|
||||
// The following catch-all branch is because plugins might’ve added
|
||||
// other things.
|
||||
// Normally, we only have import/export/jsx, but just add whatever’s
|
||||
// there.
|
||||
/* c8 ignore next 3 */
|
||||
} else {
|
||||
replacement.push(child)
|
||||
}
|
||||
}
|
||||
|
||||
// If there was no JSX content at all, add an empty function.
|
||||
if (!content) {
|
||||
replacement.push(createMdxContent())
|
||||
}
|
||||
|
||||
exportedIdentifiers.push(['MDXContent', 'default'])
|
||||
|
||||
if (outputFormat === 'function-body') {
|
||||
replacement.push({
|
||||
type: 'ReturnStatement',
|
||||
argument: {
|
||||
type: 'ObjectExpression',
|
||||
properties: [
|
||||
...Array.from({length: exportAllCount}).map(
|
||||
/**
|
||||
* @param {undefined} _
|
||||
* @param {number} index
|
||||
* @returns {SpreadElement}
|
||||
*/
|
||||
(_, index) => ({
|
||||
type: 'SpreadElement',
|
||||
argument: {type: 'Identifier', name: '_exportAll' + (index + 1)}
|
||||
})
|
||||
),
|
||||
...exportedIdentifiers.map((d) => {
|
||||
/** @type {Property} */
|
||||
const prop = {
|
||||
type: 'Property',
|
||||
kind: 'init',
|
||||
method: false,
|
||||
computed: false,
|
||||
shorthand: typeof d === 'string',
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: typeof d === 'string' ? d : d[1]
|
||||
},
|
||||
value: {
|
||||
type: 'Identifier',
|
||||
name: typeof d === 'string' ? d : d[0]
|
||||
}
|
||||
}
|
||||
|
||||
return prop
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
} else {
|
||||
replacement.push({
|
||||
type: 'ExportDefaultDeclaration',
|
||||
declaration: {type: 'Identifier', name: 'MDXContent'}
|
||||
})
|
||||
}
|
||||
|
||||
tree.body = replacement
|
||||
|
||||
/**
|
||||
* @param {ExportNamedDeclaration|ExportAllDeclaration} node
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleExport(node) {
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
// ```js
|
||||
// export function a() {}
|
||||
// export class A {}
|
||||
// export var a = 1
|
||||
// ```
|
||||
if (node.declaration) {
|
||||
exportedIdentifiers.push(
|
||||
...analyze(node.declaration).scope.declarations.keys()
|
||||
)
|
||||
}
|
||||
|
||||
// ```js
|
||||
// export {a, b as c}
|
||||
// export {a, b as c} from 'd'
|
||||
// ```
|
||||
for (child of node.specifiers) {
|
||||
exportedIdentifiers.push(child.exported.name)
|
||||
}
|
||||
}
|
||||
|
||||
handleEsm(node)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ImportDeclaration|ExportNamedDeclaration|ExportAllDeclaration} node
|
||||
* @returns {void}
|
||||
*/
|
||||
function handleEsm(node) {
|
||||
// Rewrite the source of the `import` / `export … from`.
|
||||
// See: <https://html.spec.whatwg.org/multipage/webappapis.html#resolve-a-module-specifier>
|
||||
if (baseUrl && node.source) {
|
||||
let value = String(node.source.value)
|
||||
|
||||
try {
|
||||
// A full valid URL.
|
||||
value = String(new URL(value))
|
||||
} catch {
|
||||
// Relative: `/example.js`, `./example.js`, and `../example.js`.
|
||||
if (/^\.{0,2}\//.test(value)) {
|
||||
value = String(new URL(value, baseUrl))
|
||||
}
|
||||
// Otherwise, it’s a bare specifiers.
|
||||
// For example `some-package`, `@some-package`, and
|
||||
// `some-package/path`.
|
||||
// These are supported in Node and browsers plan to support them
|
||||
// with import maps (<https://github.com/WICG/import-maps>).
|
||||
}
|
||||
|
||||
node.source = create(node.source, {type: 'Literal', value})
|
||||
}
|
||||
|
||||
/** @type {Statement|ModuleDeclaration|undefined} */
|
||||
let replace
|
||||
/** @type {Expression} */
|
||||
let init
|
||||
|
||||
if (outputFormat === 'function-body') {
|
||||
if (
|
||||
// Always have a source:
|
||||
node.type === 'ImportDeclaration' ||
|
||||
node.type === 'ExportAllDeclaration' ||
|
||||
// Source optional:
|
||||
(node.type === 'ExportNamedDeclaration' && node.source)
|
||||
) {
|
||||
if (!useDynamicImport) {
|
||||
file.fail(
|
||||
'Cannot use `import` or `export … from` in `evaluate` (outputting a function body) by default: please set `useDynamicImport: true` (and probably specify a `baseUrl`)',
|
||||
positionFromEstree(node),
|
||||
'recma-document:invalid-esm-statement'
|
||||
)
|
||||
}
|
||||
|
||||
// Just for types.
|
||||
/* c8 ignore next 3 */
|
||||
if (!node.source) {
|
||||
throw new Error('Expected `node.source` to be defined')
|
||||
}
|
||||
|
||||
// ```
|
||||
// import 'a'
|
||||
// //=> await import('a')
|
||||
// import a from 'b'
|
||||
// //=> const {default: a} = await import('b')
|
||||
// export {a, b as c} from 'd'
|
||||
// //=> const {a, c: b} = await import('d')
|
||||
// export * from 'a'
|
||||
// //=> const _exportAll0 = await import('a')
|
||||
// ```
|
||||
init = {
|
||||
type: 'AwaitExpression',
|
||||
argument: create(node, {
|
||||
type: 'ImportExpression',
|
||||
source: node.source
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
(node.type === 'ImportDeclaration' ||
|
||||
node.type === 'ExportNamedDeclaration') &&
|
||||
node.specifiers.length === 0
|
||||
) {
|
||||
replace = {type: 'ExpressionStatement', expression: init}
|
||||
} else {
|
||||
replace = {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id:
|
||||
node.type === 'ImportDeclaration' ||
|
||||
node.type === 'ExportNamedDeclaration'
|
||||
? specifiersToObjectPattern(node.specifiers)
|
||||
: {
|
||||
type: 'Identifier',
|
||||
name: '_exportAll' + ++exportAllCount
|
||||
},
|
||||
init
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
} else if (node.declaration) {
|
||||
replace = node.declaration
|
||||
} else {
|
||||
/** @type {Array.<VariableDeclarator>} */
|
||||
const declarators = node.specifiers
|
||||
.filter(
|
||||
(specifier) => specifier.local.name !== specifier.exported.name
|
||||
)
|
||||
.map((specifier) => ({
|
||||
type: 'VariableDeclarator',
|
||||
id: specifier.exported,
|
||||
init: specifier.local
|
||||
}))
|
||||
|
||||
if (declarators.length > 0) {
|
||||
replace = {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: declarators
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
replace = node
|
||||
}
|
||||
|
||||
if (replace) {
|
||||
replacement.push(replace)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Expression} [content]
|
||||
* @returns {FunctionDeclaration}
|
||||
*/
|
||||
function createMdxContent(content) {
|
||||
/** @type {JSXElement} */
|
||||
const element = {
|
||||
type: 'JSXElement',
|
||||
openingElement: {
|
||||
type: 'JSXOpeningElement',
|
||||
name: {type: 'JSXIdentifier', name: 'MDXLayout'},
|
||||
attributes: [
|
||||
{
|
||||
type: 'JSXSpreadAttribute',
|
||||
argument: {type: 'Identifier', name: 'props'}
|
||||
}
|
||||
],
|
||||
selfClosing: false
|
||||
},
|
||||
closingElement: {
|
||||
type: 'JSXClosingElement',
|
||||
name: {type: 'JSXIdentifier', name: 'MDXLayout'}
|
||||
},
|
||||
children: [
|
||||
{
|
||||
type: 'JSXExpressionContainer',
|
||||
expression: {type: 'Identifier', name: '_content'}
|
||||
}
|
||||
]
|
||||
}
|
||||
/** @type {Expression} */
|
||||
// @ts-expect-error types are wrong: `JSXElement` is an `Expression`.
|
||||
const consequent = element
|
||||
|
||||
return {
|
||||
type: 'FunctionDeclaration',
|
||||
id: {type: 'Identifier', name: 'MDXContent'},
|
||||
params: [
|
||||
{
|
||||
type: 'AssignmentPattern',
|
||||
left: {type: 'Identifier', name: 'props'},
|
||||
right: {type: 'ObjectExpression', properties: []}
|
||||
}
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
body: [
|
||||
{
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {type: 'Identifier', name: '_content'},
|
||||
init: content || {type: 'Literal', value: null}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'ReturnStatement',
|
||||
argument: {
|
||||
type: 'ConditionalExpression',
|
||||
test: {type: 'Identifier', name: 'MDXLayout'},
|
||||
consequent,
|
||||
alternate: {type: 'Identifier', name: '_content'}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
52
packages/mdx/lib/plugin/recma-jsx-build.js
Normal file
52
packages/mdx/lib/plugin/recma-jsx-build.js
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Program} Program
|
||||
*
|
||||
* @typedef RecmaJsxBuildOptions
|
||||
* @property {'program'|'function-body'} [outputFormat='program'] Whether to keep the import of the automatic runtime or get it from `arguments[0]` instead
|
||||
*/
|
||||
|
||||
import {buildJsx} from 'estree-util-build-jsx'
|
||||
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
|
||||
|
||||
/**
|
||||
* A plugin to build JSX into function calls.
|
||||
* `estree-util-build-jsx` does all the work for us!
|
||||
*
|
||||
* @type {import('unified').Plugin<[RecmaJsxBuildOptions]|[], Program>}
|
||||
*/
|
||||
export function recmaJsxBuild(options = {}) {
|
||||
const {outputFormat} = options
|
||||
|
||||
return (tree) => {
|
||||
buildJsx(tree)
|
||||
|
||||
// When compiling to a function body, replace the import that was just
|
||||
// generated, and get `jsx`, `jsxs`, and `Fragment` from `arguments[0]`
|
||||
// instead.
|
||||
if (
|
||||
outputFormat === 'function-body' &&
|
||||
tree.body[0] &&
|
||||
tree.body[0].type === 'ImportDeclaration' &&
|
||||
typeof tree.body[0].source.value === 'string' &&
|
||||
/\/jsx-runtime$/.test(tree.body[0].source.value)
|
||||
) {
|
||||
tree.body[0] = {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: specifiersToObjectPattern(tree.body[0].specifiers),
|
||||
init: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'arguments'},
|
||||
property: {type: 'Literal', value: 0},
|
||||
computed: true,
|
||||
optional: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
365
packages/mdx/lib/plugin/recma-jsx-rewrite.js
Normal file
365
packages/mdx/lib/plugin/recma-jsx-rewrite.js
Normal file
@ -0,0 +1,365 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Node} Node
|
||||
* @typedef {import('estree-jsx').Expression} Expression
|
||||
* @typedef {import('estree-jsx').Function} ESFunction
|
||||
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
|
||||
* @typedef {import('estree-jsx').JSXElement} JSXElement
|
||||
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
|
||||
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
|
||||
* @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
|
||||
* @typedef {import('estree-jsx').ModuleDeclaration} ModuleDeclaration
|
||||
* @typedef {import('estree-jsx').Program} Program
|
||||
* @typedef {import('estree-jsx').Property} Property
|
||||
* @typedef {import('estree-jsx').Statement} Statement
|
||||
* @typedef {import('estree-jsx').VariableDeclarator} VariableDeclarator
|
||||
*
|
||||
* @typedef {import('estree-walker').SyncHandler} WalkHandler
|
||||
*
|
||||
* @typedef {import('periscopic').Scope & {node: Node}} Scope
|
||||
*
|
||||
* @typedef RecmaJsxRewriteOptions
|
||||
* @property {'program'|'function-body'} [outputFormat='program'] Whether to use an import statement or `arguments[0]` to get the provider
|
||||
* @property {string} [providerImportSource] Place to import a provider from
|
||||
*
|
||||
* @typedef StackEntry
|
||||
* @property {Array.<string>} objects
|
||||
* @property {Array.<string>} components
|
||||
* @property {Array.<string>} tags
|
||||
* @property {ESFunction} node
|
||||
*/
|
||||
|
||||
import {name as isIdentifierName} from 'estree-util-is-identifier-name'
|
||||
import {walk} from 'estree-walker'
|
||||
import {analyze} from 'periscopic'
|
||||
import {specifiersToObjectPattern} from '../util/estree-util-specifiers-to-object-pattern.js'
|
||||
|
||||
/**
|
||||
* A plugin that rewrites JSX in functions to accept components as
|
||||
* `props.components` (when the function is called `MDXContent`), or from
|
||||
* a provider (if there is one).
|
||||
* It also makes sure that any undefined components are defined: either from
|
||||
* received components or as a function that throws an error.
|
||||
*
|
||||
* @type {import('unified').Plugin<[RecmaJsxRewriteOptions]|[], Program>}
|
||||
*/
|
||||
export function recmaJsxRewrite(options = {}) {
|
||||
const {providerImportSource, outputFormat} = options
|
||||
|
||||
return (tree) => {
|
||||
// Find everything that’s defined in the top-level scope.
|
||||
const scopeInfo = analyze(tree)
|
||||
/** @type {Array.<StackEntry>} */
|
||||
const fnStack = []
|
||||
/** @type {boolean|undefined} */
|
||||
let importProvider
|
||||
/** @type {Scope|null} */
|
||||
let currentScope
|
||||
|
||||
walk(tree, {
|
||||
// eslint-disable-next-line complexity
|
||||
enter(_node) {
|
||||
const node = /** @type {Node} */ (_node)
|
||||
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
fnStack.push({objects: [], components: [], tags: [], node})
|
||||
}
|
||||
|
||||
const fnScope = fnStack[0]
|
||||
|
||||
if (
|
||||
!fnScope ||
|
||||
(!isMdxContent(fnScope.node) && !providerImportSource)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const newScope = /** @type {Scope|undefined} */ (
|
||||
// @ts-expect-error: periscopic doesn’t support JSX.
|
||||
scopeInfo.map.get(node)
|
||||
)
|
||||
|
||||
if (newScope) {
|
||||
newScope.node = node
|
||||
currentScope = newScope
|
||||
}
|
||||
|
||||
if (currentScope && node.type === 'JSXElement') {
|
||||
let name = node.openingElement.name
|
||||
|
||||
// `<x.y>`, `<Foo.Bar>`, `<x.y.z>`.
|
||||
if (name.type === 'JSXMemberExpression') {
|
||||
// Find the left-most identifier.
|
||||
while (name.type === 'JSXMemberExpression') name = name.object
|
||||
|
||||
const id = name.name
|
||||
|
||||
if (!fnScope.objects.includes(id) && !inScope(currentScope, id)) {
|
||||
fnScope.objects.push(id)
|
||||
}
|
||||
}
|
||||
// `<xml:thing>`.
|
||||
else if (name.type === 'JSXNamespacedName') {
|
||||
// Ignore namespaces.
|
||||
}
|
||||
// If the name is a valid ES identifier, and it doesn’t start with a
|
||||
// lowercase letter, it’s a component.
|
||||
// For example, `$foo`, `_bar`, `Baz` are all component names.
|
||||
// But `foo` and `b-ar` are tag names.
|
||||
else if (isIdentifierName(name.name) && !/^[a-z]/.test(name.name)) {
|
||||
const id = name.name
|
||||
|
||||
if (
|
||||
!fnScope.components.includes(id) &&
|
||||
!inScope(currentScope, id)
|
||||
) {
|
||||
fnScope.components.push(id)
|
||||
}
|
||||
}
|
||||
// @ts-expect-error Allow fields passed through from mdast through hast to
|
||||
// esast.
|
||||
else if (node.data && node.data._xdmExplicitJsx) {
|
||||
// Do not turn explicit JSX into components from `_components`.
|
||||
// As in, a given `h1` component is used for `# heading` (next case),
|
||||
// but not for `<h1>heading</h1>`.
|
||||
} else {
|
||||
const id = name.name
|
||||
|
||||
if (!fnScope.tags.includes(id)) {
|
||||
fnScope.tags.push(id)
|
||||
}
|
||||
|
||||
node.openingElement.name = {
|
||||
type: 'JSXMemberExpression',
|
||||
object: {type: 'JSXIdentifier', name: '_components'},
|
||||
property: name
|
||||
}
|
||||
|
||||
if (node.closingElement) {
|
||||
node.closingElement.name = {
|
||||
type: 'JSXMemberExpression',
|
||||
object: {type: 'JSXIdentifier', name: '_components'},
|
||||
property: {type: 'JSXIdentifier', name: id}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
leave(node) {
|
||||
/** @type {Array.<Property>} */
|
||||
const defaults = []
|
||||
/** @type {Array.<string>} */
|
||||
const actual = []
|
||||
/** @type {Array.<Expression>} */
|
||||
const parameters = []
|
||||
/** @type {Array.<VariableDeclarator>} */
|
||||
const declarations = []
|
||||
|
||||
if (currentScope && currentScope.node === node) {
|
||||
// @ts-expect-error: `node`s were patched when entering.
|
||||
currentScope = currentScope.parent
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === 'FunctionDeclaration' ||
|
||||
node.type === 'FunctionExpression' ||
|
||||
node.type === 'ArrowFunctionExpression'
|
||||
) {
|
||||
const fn = /** @type {ESFunction} */ (node)
|
||||
const scope = fnStack[fnStack.length - 1]
|
||||
/** @type {string} */
|
||||
let name
|
||||
|
||||
for (name of scope.tags) {
|
||||
defaults.push({
|
||||
type: 'Property',
|
||||
kind: 'init',
|
||||
key: {type: 'Identifier', name},
|
||||
value: {type: 'Literal', value: name},
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false
|
||||
})
|
||||
}
|
||||
|
||||
actual.push(...scope.components)
|
||||
|
||||
for (name of scope.objects) {
|
||||
// In some cases, a component is used directly (`<X>`) but it’s also
|
||||
// used as an object (`<X.Y>`).
|
||||
if (!actual.includes(name)) {
|
||||
actual.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
if (defaults.length > 0 || actual.length > 0) {
|
||||
parameters.push({type: 'ObjectExpression', properties: defaults})
|
||||
|
||||
if (providerImportSource) {
|
||||
importProvider = true
|
||||
parameters.push({
|
||||
type: 'CallExpression',
|
||||
callee: {type: 'Identifier', name: '_provideComponents'},
|
||||
arguments: [],
|
||||
optional: false
|
||||
})
|
||||
}
|
||||
|
||||
// Accept `components` as a prop if this is the `MDXContent` function.
|
||||
if (isMdxContent(scope.node)) {
|
||||
parameters.push({
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'props'},
|
||||
property: {type: 'Identifier', name: 'components'},
|
||||
computed: false,
|
||||
optional: false
|
||||
})
|
||||
}
|
||||
|
||||
declarations.push({
|
||||
type: 'VariableDeclarator',
|
||||
id: {type: 'Identifier', name: '_components'},
|
||||
init: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'Object'},
|
||||
property: {type: 'Identifier', name: 'assign'},
|
||||
computed: false,
|
||||
optional: false
|
||||
},
|
||||
arguments: parameters,
|
||||
optional: false
|
||||
}
|
||||
})
|
||||
|
||||
// Add components to scope.
|
||||
// For `['MyComponent', 'MDXLayout']` this generates:
|
||||
// ```js
|
||||
// const {MyComponent, wrapper: MDXLayout} = _components
|
||||
// ```
|
||||
// Note that MDXLayout is special as it’s taken from
|
||||
// `_components.wrapper`.
|
||||
if (actual.length > 0) {
|
||||
declarations.push({
|
||||
type: 'VariableDeclarator',
|
||||
id: {
|
||||
type: 'ObjectPattern',
|
||||
properties: actual.map((name) => ({
|
||||
type: 'Property',
|
||||
kind: 'init',
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: name === 'MDXLayout' ? 'wrapper' : name
|
||||
},
|
||||
value: {type: 'Identifier', name},
|
||||
method: false,
|
||||
shorthand: name !== 'MDXLayout',
|
||||
computed: false
|
||||
}))
|
||||
},
|
||||
init: {type: 'Identifier', name: '_components'}
|
||||
})
|
||||
}
|
||||
|
||||
// Arrow functions with an implied return:
|
||||
if (fn.body.type !== 'BlockStatement') {
|
||||
fn.body = {
|
||||
type: 'BlockStatement',
|
||||
body: [{type: 'ReturnStatement', argument: fn.body}]
|
||||
}
|
||||
}
|
||||
|
||||
fn.body.body.unshift({
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations
|
||||
})
|
||||
}
|
||||
|
||||
fnStack.pop()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// If a provider is used (and can be used), import it.
|
||||
if (importProvider && providerImportSource) {
|
||||
tree.body.unshift(
|
||||
createImportProvider(providerImportSource, outputFormat)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} providerImportSource
|
||||
* @param {RecmaJsxRewriteOptions['outputFormat']} outputFormat
|
||||
* @returns {Statement|ModuleDeclaration}
|
||||
*/
|
||||
function createImportProvider(providerImportSource, outputFormat) {
|
||||
/** @type {Array<ImportSpecifier>} */
|
||||
const specifiers = [
|
||||
{
|
||||
type: 'ImportSpecifier',
|
||||
imported: {type: 'Identifier', name: 'useMDXComponents'},
|
||||
local: {type: 'Identifier', name: '_provideComponents'}
|
||||
}
|
||||
]
|
||||
|
||||
return outputFormat === 'function-body'
|
||||
? {
|
||||
type: 'VariableDeclaration',
|
||||
kind: 'const',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: specifiersToObjectPattern(specifiers),
|
||||
init: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'arguments'},
|
||||
property: {type: 'Literal', value: 0},
|
||||
computed: true,
|
||||
optional: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
: {
|
||||
type: 'ImportDeclaration',
|
||||
specifiers,
|
||||
source: {type: 'Literal', value: providerImportSource}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ESFunction} [node]
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isMdxContent(node) {
|
||||
return Boolean(
|
||||
node && 'id' in node && node.id && node.id.name === 'MDXContent'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Scope} scope
|
||||
* @param {string} id
|
||||
*/
|
||||
function inScope(scope, id) {
|
||||
/** @type {Scope|null} */
|
||||
let currentScope = scope
|
||||
|
||||
while (currentScope) {
|
||||
if (currentScope.declarations.has(id)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// @ts-expect-error: `node`s have been added when entering.
|
||||
currentScope = currentScope.parent
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
340
packages/mdx/lib/plugin/recma-stringify.js
Normal file
340
packages/mdx/lib/plugin/recma-stringify.js
Normal file
@ -0,0 +1,340 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Node} Node
|
||||
* @typedef {import('estree-jsx').Program} Program
|
||||
* @typedef {import('estree-jsx').JSXAttribute} JSXAttribute
|
||||
* @typedef {import('estree-jsx').JSXClosingElement} JSXClosingElement
|
||||
* @typedef {import('estree-jsx').JSXClosingFragment} JSXClosingFragment
|
||||
* @typedef {import('estree-jsx').JSXElement} JSXElement
|
||||
* @typedef {import('estree-jsx').JSXEmptyExpression} JSXEmptyExpression
|
||||
* @typedef {import('estree-jsx').JSXExpressionContainer} JSXExpressionContainer
|
||||
* @typedef {import('estree-jsx').JSXFragment} JSXFragment
|
||||
* @typedef {import('estree-jsx').JSXIdentifier} JSXIdentifier
|
||||
* @typedef {import('estree-jsx').JSXMemberExpression} JSXMemberExpression
|
||||
* @typedef {import('estree-jsx').JSXNamespacedName} JSXNamespacedName
|
||||
* @typedef {import('estree-jsx').JSXOpeningElement} JSXOpeningElement
|
||||
* @typedef {import('estree-jsx').JSXOpeningFragment} JSXOpeningFragment
|
||||
* @typedef {import('estree-jsx').JSXSpreadAttribute} JSXSpreadAttribute
|
||||
* @typedef {import('estree-jsx').JSXText} JSXText
|
||||
* @typedef {import('vfile').VFile} VFile
|
||||
* @typedef {typeof import('source-map').SourceMapGenerator} SourceMapGenerator
|
||||
*
|
||||
* @typedef {Omit<import('astring').State, 'write'> & {write: ((code: string, node?: Node) => void)}} State
|
||||
*
|
||||
* @typedef {{[K in Node['type']]: (node: Node, state: State) => void}} Generator
|
||||
*
|
||||
* @typedef RecmaStringifyOptions
|
||||
* @property {SourceMapGenerator} [SourceMapGenerator] Generate a source map by passing a `SourceMapGenerator` from `source-map` in
|
||||
*/
|
||||
|
||||
// @ts-expect-error baseGenerator is not yet exported by astring typings
|
||||
import {baseGenerator, generate} from 'astring'
|
||||
|
||||
/**
|
||||
* A plugin that adds an esast compiler: a small wrapper around `astring` to add
|
||||
* support for serializing JSX.
|
||||
*
|
||||
* @type {import('unified').Plugin<[RecmaStringifyOptions]|[], Program, string>}
|
||||
*/
|
||||
export function recmaStringify(options = {}) {
|
||||
const {SourceMapGenerator} = options
|
||||
|
||||
Object.assign(this, {Compiler: compiler})
|
||||
|
||||
/** @type {import('unified').CompilerFunction<Program, string>} */
|
||||
function compiler(tree, file) {
|
||||
/** @type {InstanceType<SourceMapGenerator>|undefined} */
|
||||
let sourceMap
|
||||
|
||||
if (SourceMapGenerator) {
|
||||
sourceMap = new SourceMapGenerator({file: file.path || 'unknown.mdx'})
|
||||
}
|
||||
|
||||
const result = generate(tree, {
|
||||
generator: { ...baseGenerator, JSXAttribute,
|
||||
JSXClosingElement,
|
||||
JSXClosingFragment,
|
||||
JSXElement,
|
||||
JSXEmptyExpression,
|
||||
JSXExpressionContainer,
|
||||
JSXFragment,
|
||||
JSXIdentifier,
|
||||
JSXMemberExpression,
|
||||
JSXNamespacedName,
|
||||
JSXOpeningElement,
|
||||
JSXOpeningFragment,
|
||||
JSXSpreadAttribute,
|
||||
JSXText},
|
||||
comments: true,
|
||||
sourceMap
|
||||
})
|
||||
|
||||
if (sourceMap) {
|
||||
file.map = sourceMap.toJSON()
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `attr`
|
||||
* `attr="something"`
|
||||
* `attr={1}`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXAttribute} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXAttribute(node, state) {
|
||||
this[node.name.type](node.name, state)
|
||||
|
||||
if (node.value !== undefined && node.value !== null) {
|
||||
state.write('=')
|
||||
|
||||
// Encode double quotes in attribute values.
|
||||
if (node.value.type === 'Literal') {
|
||||
state.write(
|
||||
'"' + encodeJsx(String(node.value.value)).replace(/"/g, '"') + '"',
|
||||
node
|
||||
)
|
||||
} else {
|
||||
this[node.value.type](node.value, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `</div>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXClosingElement} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXClosingElement(node, state) {
|
||||
state.write('</')
|
||||
this[node.name.type](node.name, state)
|
||||
state.write('>')
|
||||
}
|
||||
|
||||
/**
|
||||
* `</>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXClosingFragment} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXClosingFragment(node, state) {
|
||||
state.write('</>', node)
|
||||
}
|
||||
|
||||
/**
|
||||
* `<div />`
|
||||
* `<div></div>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXElement} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXElement(node, state) {
|
||||
let index = -1
|
||||
|
||||
this[node.openingElement.type](node.openingElement, state)
|
||||
|
||||
if (node.children) {
|
||||
while (++index < node.children.length) {
|
||||
const child = node.children[index]
|
||||
|
||||
// Supported in types but not by Acorn.
|
||||
/* c8 ignore next 3 */
|
||||
if (child.type === 'JSXSpreadChild') {
|
||||
throw new Error('JSX spread children are not supported')
|
||||
}
|
||||
|
||||
this[child.type](child, state)
|
||||
}
|
||||
}
|
||||
|
||||
if (node.closingElement) {
|
||||
this[node.closingElement.type](node.closingElement, state)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `{}` (always in a `JSXExpressionContainer`, which does the curlies)
|
||||
*
|
||||
* @this {Generator}
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXEmptyExpression() {}
|
||||
|
||||
/**
|
||||
* `{expression}`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXExpressionContainer} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXExpressionContainer(node, state) {
|
||||
state.write('{')
|
||||
this[node.expression.type](node.expression, state)
|
||||
state.write('}')
|
||||
}
|
||||
|
||||
/**
|
||||
* `<></>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXFragment} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXFragment(node, state) {
|
||||
let index = -1
|
||||
|
||||
this[node.openingFragment.type](node.openingFragment, state)
|
||||
|
||||
if (node.children) {
|
||||
while (++index < node.children.length) {
|
||||
const child = node.children[index]
|
||||
|
||||
// Supported in types but not by Acorn.
|
||||
/* c8 ignore next 3 */
|
||||
if (child.type === 'JSXSpreadChild') {
|
||||
throw new Error('JSX spread children are not supported')
|
||||
}
|
||||
|
||||
this[child.type](child, state)
|
||||
}
|
||||
}
|
||||
|
||||
this[node.closingFragment.type](node.closingFragment, state)
|
||||
}
|
||||
|
||||
/**
|
||||
* `div`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXIdentifier} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXIdentifier(node, state) {
|
||||
state.write(node.name, node)
|
||||
}
|
||||
|
||||
/**
|
||||
* `member.expression`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXMemberExpression} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXMemberExpression(node, state) {
|
||||
this[node.object.type](node.object, state)
|
||||
state.write('.')
|
||||
this[node.property.type](node.property, state)
|
||||
}
|
||||
|
||||
/**
|
||||
* `ns:name`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXNamespacedName} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXNamespacedName(node, state) {
|
||||
this[node.namespace.type](node.namespace, state)
|
||||
state.write(':')
|
||||
this[node.name.type](node.name, state)
|
||||
}
|
||||
|
||||
/**
|
||||
* `<div>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXOpeningElement} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXOpeningElement(node, state) {
|
||||
let index = -1
|
||||
|
||||
state.write('<')
|
||||
this[node.name.type](node.name, state)
|
||||
|
||||
if (node.attributes) {
|
||||
while (++index < node.attributes.length) {
|
||||
state.write(' ')
|
||||
this[node.attributes[index].type](node.attributes[index], state)
|
||||
}
|
||||
}
|
||||
|
||||
state.write(node.selfClosing ? ' />' : '>')
|
||||
}
|
||||
|
||||
/**
|
||||
* `<>`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXOpeningFragment} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXOpeningFragment(node, state) {
|
||||
state.write('<>', node)
|
||||
}
|
||||
|
||||
/**
|
||||
* `{...argument}`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXSpreadAttribute} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXSpreadAttribute(node, state) {
|
||||
state.write('{')
|
||||
// eslint-disable-next-line new-cap
|
||||
this.SpreadElement(node, state)
|
||||
state.write('}')
|
||||
}
|
||||
|
||||
/**
|
||||
* `!`
|
||||
*
|
||||
* @this {Generator}
|
||||
* @param {JSXText} node
|
||||
* @param {State} state
|
||||
* @returns {void}
|
||||
*/
|
||||
function JSXText(node, state) {
|
||||
state.write(
|
||||
encodeJsx(node.value).replace(/<|{/g, ($0) =>
|
||||
$0 === '<' ? '<' : '{'
|
||||
),
|
||||
node
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure that character references don’t pop up.
|
||||
* For example, the text `©` should stay that way, and not turn into `©`.
|
||||
* We could encode all `&` (easy but verbose) or look for actual valid
|
||||
* references (complex but cleanest output).
|
||||
* Looking for the 2nd character gives us a middle ground.
|
||||
* The `#` is for (decimal and hexadecimal) numeric references, the letters
|
||||
* are for the named references.
|
||||
*
|
||||
* @param {string} value
|
||||
* @returns {string}
|
||||
*/
|
||||
function encodeJsx(value) {
|
||||
return value.replace(/&(?=[#a-z])/gi, '&')
|
||||
}
|
16
packages/mdx/lib/plugin/rehype-recma.js
Normal file
16
packages/mdx/lib/plugin/rehype-recma.js
Normal file
@ -0,0 +1,16 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Program} Program
|
||||
* @typedef {import('hast').Root} Root
|
||||
*/
|
||||
|
||||
import {toEstree} from 'hast-util-to-estree'
|
||||
|
||||
/**
|
||||
* A plugin to transform an HTML (hast) tree to a JS (estree).
|
||||
* `hast-util-to-estree` does all the work for us!
|
||||
*
|
||||
* @type {import('unified').Plugin<void[], Root, Program>}
|
||||
*/
|
||||
export function rehypeRecma() {
|
||||
return (tree) => toEstree(tree)
|
||||
}
|
23
packages/mdx/lib/plugin/rehype-remove-raw.js
Normal file
23
packages/mdx/lib/plugin/rehype-remove-raw.js
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @typedef {import('hast').Root} Root
|
||||
*/
|
||||
|
||||
import {visit} from 'unist-util-visit'
|
||||
|
||||
/**
|
||||
* A tiny plugin that removes raw HTML.
|
||||
* This is needed if the format is `md` and `rehype-raw` was not used to parse
|
||||
* dangerous HTML into nodes.
|
||||
*
|
||||
* @type {import('unified').Plugin<void[], Root>}
|
||||
*/
|
||||
export function rehypeRemoveRaw() {
|
||||
return (tree) => {
|
||||
visit(tree, 'raw', (_, index, parent) => {
|
||||
if (parent && typeof index === 'number') {
|
||||
parent.children.splice(index, 1)
|
||||
return index
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
82
packages/mdx/lib/plugin/remark-mark-and-unravel.js
Normal file
82
packages/mdx/lib/plugin/remark-mark-and-unravel.js
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
* @typedef {import('mdast').Content} Content
|
||||
* @typedef {Root|Content} Node
|
||||
* @typedef {Extract<Node, import('unist').Parent>} Parent
|
||||
*
|
||||
* @typedef {import('./remark-mdx.js')} DoNotTouchAsThisImportIncludesMdxInTree
|
||||
*/
|
||||
|
||||
import {visit} from 'unist-util-visit'
|
||||
|
||||
/**
|
||||
* A tiny plugin that unravels `<p><h1>x</h1></p>` but also
|
||||
* `<p><Component /></p>` (so it has no knowledge of “HTML”).
|
||||
* It also marks JSX as being explicitly JSX, so when a user passes a `h1`
|
||||
* component, it is used for `# heading` but not for `<h1>heading</h1>`.
|
||||
*
|
||||
* @type {import('unified').Plugin<void[], Root>}
|
||||
*/
|
||||
export function remarkMarkAndUnravel() {
|
||||
return (tree) => {
|
||||
visit(tree, (node, index, parent_) => {
|
||||
const parent = /** @type {Parent} */ (parent_)
|
||||
let offset = -1
|
||||
let all = true
|
||||
/** @type {boolean|undefined} */
|
||||
let oneOrMore
|
||||
|
||||
if (parent && typeof index === 'number' && node.type === 'paragraph') {
|
||||
const children = node.children
|
||||
|
||||
while (++offset < children.length) {
|
||||
const child = children[offset]
|
||||
|
||||
if (
|
||||
child.type === 'mdxJsxTextElement' ||
|
||||
child.type === 'mdxTextExpression'
|
||||
) {
|
||||
oneOrMore = true
|
||||
} else if (
|
||||
child.type === 'text' &&
|
||||
/^[\t\r\n ]+$/.test(String(child.value))
|
||||
) {
|
||||
// Empty.
|
||||
} else {
|
||||
all = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (all && oneOrMore) {
|
||||
offset = -1
|
||||
|
||||
while (++offset < children.length) {
|
||||
const child = children[offset]
|
||||
|
||||
if (child.type === 'mdxJsxTextElement') {
|
||||
// @ts-expect-error: content model is fine.
|
||||
child.type = 'mdxJsxFlowElement'
|
||||
}
|
||||
|
||||
if (child.type === 'mdxTextExpression') {
|
||||
// @ts-expect-error: content model is fine.
|
||||
child.type = 'mdxFlowExpression'
|
||||
}
|
||||
}
|
||||
|
||||
parent.children.splice(index, 1, ...children)
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
node.type === 'mdxJsxFlowElement' ||
|
||||
node.type === 'mdxJsxTextElement'
|
||||
) {
|
||||
const data = node.data || (node.data = {})
|
||||
data._xdmExplicitJsx = true
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
36
packages/mdx/lib/plugin/remark-mdx.js
Normal file
36
packages/mdx/lib/plugin/remark-mdx.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @typedef {import('mdast').Root} Root
|
||||
* @typedef {import('micromark-extension-mdxjs').Options} Options
|
||||
*
|
||||
* @typedef {import('mdast-util-mdx')} DoNotTouchAsThisImportIncludesMdxInTree
|
||||
*/
|
||||
|
||||
import {mdxjs} from 'micromark-extension-mdxjs'
|
||||
import {mdxFromMarkdown, mdxToMarkdown} from 'mdast-util-mdx'
|
||||
|
||||
/**
|
||||
* Add the micromark and mdast extensions for MDX.js (JS aware MDX).
|
||||
*
|
||||
* @type {import('unified').Plugin<[Options?]|[], Root>}
|
||||
*/
|
||||
export function remarkMdx(options = {}) {
|
||||
const data = this.data()
|
||||
|
||||
add('micromarkExtensions', mdxjs(options))
|
||||
add('fromMarkdownExtensions', mdxFromMarkdown)
|
||||
add('toMarkdownExtensions', mdxToMarkdown)
|
||||
|
||||
/**
|
||||
* @param {string} field
|
||||
* @param {unknown} value
|
||||
*/
|
||||
function add(field, value) {
|
||||
const list = /** @type {unknown[]} */ (
|
||||
// Other extensions
|
||||
/* c8 ignore next 2 */
|
||||
data[field] ? data[field] : (data[field] = [])
|
||||
)
|
||||
|
||||
list.push(value)
|
||||
}
|
||||
}
|
31
packages/mdx/lib/run.js
Normal file
31
packages/mdx/lib/run.js
Normal file
@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @typedef {import('vfile').VFile} VFile
|
||||
*/
|
||||
|
||||
/** @type {new (code: string, ...args: unknown[]) => Function} **/
|
||||
const AsyncFunction = Object.getPrototypeOf(run).constructor
|
||||
|
||||
/**
|
||||
* Asynchronously run code.
|
||||
*
|
||||
* @param {VFile} file JS document to run
|
||||
* @param {unknown} options
|
||||
* @return {Promise<*>}
|
||||
*/
|
||||
export async function run(file, options) {
|
||||
// V8 on Erbium.
|
||||
/* c8 ignore next 2 */
|
||||
return new AsyncFunction(String(file))(options)
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronously run code.
|
||||
*
|
||||
* @param {VFile} file JS document to run
|
||||
* @param {unknown} options
|
||||
* @return {*}
|
||||
*/
|
||||
export function runSync(file, options) {
|
||||
// eslint-disable-next-line no-new-func
|
||||
return new Function(String(file))(options)
|
||||
}
|
82
packages/mdx/lib/util/create-format-aware-processors.js
Normal file
82
packages/mdx/lib/util/create-format-aware-processors.js
Normal file
@ -0,0 +1,82 @@
|
||||
/**
|
||||
* @typedef {import('vfile').VFileCompatible} VFileCompatible
|
||||
* @typedef {import('vfile').VFile} VFile
|
||||
* @typedef {import('unified').Processor} Processor
|
||||
* @typedef {import('../compile.js').CompileOptions} CompileOptions
|
||||
*/
|
||||
|
||||
import {createProcessor} from '../core.js'
|
||||
import {md, mdx} from './extnames.js'
|
||||
import {resolveFileAndOptions} from './resolve-file-and-options.js'
|
||||
|
||||
/**
|
||||
* Create smart processors to handle different formats.
|
||||
*
|
||||
* @param {CompileOptions} [compileOptions]
|
||||
* @return {{extnames: string[], process: process, processSync: processSync}}
|
||||
*/
|
||||
export function createFormatAwareProcessors(compileOptions = {}) {
|
||||
const mdExtensions = compileOptions.mdExtensions || md
|
||||
const mdxExtensions = compileOptions.mdxExtensions || mdx
|
||||
/** @type {Processor} */
|
||||
let cachedMarkdown
|
||||
/** @type {Processor} */
|
||||
let cachedMdx
|
||||
|
||||
return {
|
||||
extnames:
|
||||
compileOptions.format === 'md'
|
||||
? mdExtensions
|
||||
: compileOptions.format === 'mdx'
|
||||
? mdxExtensions
|
||||
: mdExtensions.concat(mdxExtensions),
|
||||
process,
|
||||
processSync
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart processor.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX or markdown document
|
||||
* @return {Promise<VFile>}
|
||||
*/
|
||||
function process(vfileCompatible) {
|
||||
const {file, processor} = split(vfileCompatible)
|
||||
return processor.process(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync smart processor.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX or markdown document
|
||||
* @return {VFile}
|
||||
*/
|
||||
// C8 does not cover `.cjs` files (this is only used for the require hook,
|
||||
// which has to be CJS).
|
||||
/* c8 ignore next 4 */
|
||||
function processSync(vfileCompatible) {
|
||||
const {file, processor} = split(vfileCompatible)
|
||||
return processor.processSync(file)
|
||||
}
|
||||
|
||||
/**
|
||||
* Make a full vfile from what’s given, and figure out which processor
|
||||
* should be used for it.
|
||||
* This caches processors (one for markdown and one for MDX) so that they do
|
||||
* not have to be reconstructed for each file.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible MDX or markdown document
|
||||
* @return {{file: VFile, processor: Processor}}
|
||||
*/
|
||||
function split(vfileCompatible) {
|
||||
const {file, options} = resolveFileAndOptions(
|
||||
vfileCompatible,
|
||||
compileOptions
|
||||
)
|
||||
const processor =
|
||||
options.format === 'md'
|
||||
? cachedMarkdown || (cachedMarkdown = createProcessor(options))
|
||||
: cachedMdx || (cachedMdx = createProcessor(options))
|
||||
return {file, processor}
|
||||
}
|
||||
}
|
27
packages/mdx/lib/util/estree-util-create.js
Normal file
27
packages/mdx/lib/util/estree-util-create.js
Normal file
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Node} Node
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Node} N
|
||||
* @param {Node} template
|
||||
* @param {N} node
|
||||
* @returns {N}
|
||||
*/
|
||||
export function create(template, node) {
|
||||
/** @type {Array<keyof template>} */
|
||||
// @ts-expect-error: `start`, `end`, `comments` are custom Acorn fields.
|
||||
const fields = ['start', 'end', 'loc', 'range', 'comments']
|
||||
let index = -1
|
||||
|
||||
while (++index < fields.length) {
|
||||
const field = fields[index]
|
||||
|
||||
if (field in template) {
|
||||
// @ts-expect-error: assume they’re settable.
|
||||
node[field] = template[field]
|
||||
}
|
||||
}
|
||||
|
||||
return node
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Declaration} Declaration
|
||||
* @typedef {import('estree-jsx').Expression} Expression
|
||||
*/
|
||||
|
||||
/**
|
||||
* Turn a declaration into an expression.
|
||||
* Doesn’t work for variable declarations, but that’s fine for our use case
|
||||
* because currently we’re using this utility for export default declarations,
|
||||
* which can’t contain variable declarations.
|
||||
*
|
||||
* @param {Declaration} declaration
|
||||
* @returns {Expression}
|
||||
*/
|
||||
export function declarationToExpression(declaration) {
|
||||
if (declaration.type === 'FunctionDeclaration') {
|
||||
return {...declaration, type: 'FunctionExpression'}
|
||||
}
|
||||
|
||||
if (declaration.type === 'ClassDeclaration') {
|
||||
return {...declaration, type: 'ClassExpression'}
|
||||
/* c8 ignore next 4 */
|
||||
}
|
||||
|
||||
// Probably `VariableDeclaration`.
|
||||
throw new Error('Cannot turn `' + declaration.type + '` into an expression')
|
||||
}
|
18
packages/mdx/lib/util/estree-util-is-declaration.js
Normal file
18
packages/mdx/lib/util/estree-util-is-declaration.js
Normal file
@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Declaration} Declaration
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {unknown} node
|
||||
* @returns {node is Declaration}
|
||||
*/
|
||||
export function isDeclaration(node) {
|
||||
/** @type {string} */
|
||||
// @ts-expect-error Hush typescript, looks like `type` is available.
|
||||
const type = node && typeof node === 'object' && node.type
|
||||
return Boolean(
|
||||
type === 'FunctionDeclaration' ||
|
||||
type === 'ClassDeclaration' ||
|
||||
type === 'VariableDeclaration'
|
||||
)
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/**
|
||||
* @typedef {import('estree-jsx').Identifier} Identifier
|
||||
* @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier
|
||||
* @typedef {import('estree-jsx').ImportDefaultSpecifier} ImportDefaultSpecifier
|
||||
* @typedef {import('estree-jsx').ImportNamespaceSpecifier} ImportNamespaceSpecifier
|
||||
* @typedef {import('estree-jsx').ExportSpecifier} ExportSpecifier
|
||||
* @typedef {import('estree-jsx').ObjectPattern} ObjectPattern
|
||||
*/
|
||||
|
||||
import {create} from './estree-util-create.js'
|
||||
|
||||
/**
|
||||
* @param {Array<ImportSpecifier|ImportDefaultSpecifier|ImportNamespaceSpecifier|ExportSpecifier>} specifiers
|
||||
* @returns {ObjectPattern}
|
||||
*/
|
||||
export function specifiersToObjectPattern(specifiers) {
|
||||
return {
|
||||
type: 'ObjectPattern',
|
||||
properties: specifiers.map((specifier) => {
|
||||
/** @type {Identifier} */
|
||||
let key =
|
||||
specifier.type === 'ImportSpecifier'
|
||||
? specifier.imported
|
||||
: specifier.type === 'ExportSpecifier'
|
||||
? specifier.exported
|
||||
: {type: 'Identifier', name: 'default'}
|
||||
let value = specifier.local
|
||||
|
||||
// Switch them around if we’re exporting.
|
||||
if (specifier.type === 'ExportSpecifier') {
|
||||
value = key
|
||||
key = specifier.local
|
||||
}
|
||||
|
||||
return create(specifier, {
|
||||
type: 'Property',
|
||||
kind: 'init',
|
||||
shorthand: key.name === value.name,
|
||||
method: false,
|
||||
computed: false,
|
||||
key,
|
||||
value
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
11
packages/mdx/lib/util/extnames-to-regex.js
Normal file
11
packages/mdx/lib/util/extnames-to-regex.js
Normal file
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Utility to turn a list of extnames (*with* dots) into an expression.
|
||||
*
|
||||
* @param {string[]} extnames List of extnames
|
||||
* @returns {RegExp} Regex matching them
|
||||
*/
|
||||
export function extnamesToRegex(extnames) {
|
||||
return new RegExp(
|
||||
'\\.(' + extnames.map((d) => d.slice(1)).join('|') + ')([?#]|$)'
|
||||
)
|
||||
}
|
6
packages/mdx/lib/util/extnames.js
Normal file
6
packages/mdx/lib/util/extnames.js
Normal file
@ -0,0 +1,6 @@
|
||||
// @ts-expect-error: untyped.
|
||||
import markdownExtensions from 'markdown-extensions'
|
||||
|
||||
export const mdx = ['.mdx']
|
||||
/** @type {string[]} */
|
||||
export const md = markdownExtensions.map((/** @type {string} */ d) => '.' + d)
|
36
packages/mdx/lib/util/resolve-evaluate-options.js
Normal file
36
packages/mdx/lib/util/resolve-evaluate-options.js
Normal file
@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
|
||||
*
|
||||
* @typedef RunnerOptions
|
||||
* @property {*} Fragment Symbol to use for fragments
|
||||
* @property {*} jsx Function to generate an element with static children
|
||||
* @property {*} jsxs Function to generate an element with dynamic children
|
||||
* @property {*} [useMDXComponents] Function to get `MDXComponents` from context
|
||||
*
|
||||
* @typedef {Omit<ProcessorOptions, 'jsx' | 'jsxImportSource' | 'jsxRuntime' | 'pragma' | 'pragmaFrag' | 'pragmaImportSource' | 'providerImportSource' | 'outputFormat'> } EvaluateProcessorOptions
|
||||
*
|
||||
* @typedef {EvaluateProcessorOptions & RunnerOptions} EvaluateOptions
|
||||
*/
|
||||
|
||||
/**
|
||||
* Split compiletime options from runtime options.
|
||||
*
|
||||
* @param {EvaluateOptions} options
|
||||
* @returns {{compiletime: ProcessorOptions, runtime: RunnerOptions}}
|
||||
*/
|
||||
export function resolveEvaluateOptions(options) {
|
||||
const {Fragment, jsx, jsxs, useMDXComponents, ...rest} = options || {}
|
||||
|
||||
if (!Fragment) throw new Error('Expected `Fragment` given to `evaluate`')
|
||||
if (!jsx) throw new Error('Expected `jsx` given to `evaluate`')
|
||||
if (!jsxs) throw new Error('Expected `jsxs` given to `evaluate`')
|
||||
|
||||
return {
|
||||
compiletime: {
|
||||
...rest,
|
||||
outputFormat: 'function-body',
|
||||
providerImportSource: useMDXComponents ? '#' : undefined
|
||||
},
|
||||
runtime: {Fragment, jsx, jsxs, useMDXComponents}
|
||||
}
|
||||
}
|
48
packages/mdx/lib/util/resolve-file-and-options.js
Normal file
48
packages/mdx/lib/util/resolve-file-and-options.js
Normal file
@ -0,0 +1,48 @@
|
||||
/**
|
||||
* @typedef {import('vfile').VFileCompatible} VFileCompatible
|
||||
* @typedef {import('../core.js').ProcessorOptions} ProcessorOptions
|
||||
* @typedef {import('../compile.js').CompileOptions} CompileOptions
|
||||
*/
|
||||
|
||||
import {VFile} from 'vfile'
|
||||
import {md} from './extnames.js'
|
||||
|
||||
/**
|
||||
* Create a file and options from a given `vfileCompatible` and options that
|
||||
* might contain `format: 'detect'`.
|
||||
*
|
||||
* @param {VFileCompatible} vfileCompatible
|
||||
* @param {CompileOptions} [options]
|
||||
* @returns {{file: VFile, options: ProcessorOptions}}
|
||||
*/
|
||||
export function resolveFileAndOptions(vfileCompatible, options) {
|
||||
const file = looksLikeAVFile(vfileCompatible)
|
||||
? vfileCompatible
|
||||
: new VFile(vfileCompatible)
|
||||
const {format, ...rest} = options || {}
|
||||
return {
|
||||
file,
|
||||
options: {
|
||||
format:
|
||||
format === 'md' || format === 'mdx'
|
||||
? format
|
||||
: file.extname && (rest.mdExtensions || md).includes(file.extname)
|
||||
? 'md'
|
||||
: 'mdx',
|
||||
...rest
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {VFileCompatible} [value]
|
||||
* @returns {value is VFile}
|
||||
*/
|
||||
function looksLikeAVFile(value) {
|
||||
return Boolean(
|
||||
value &&
|
||||
typeof value === 'object' &&
|
||||
'message' in value &&
|
||||
'messages' in value
|
||||
)
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
const toHast = require('mdast-util-to-hast')
|
||||
const detab = require('detab')
|
||||
const u = require('unist-builder')
|
||||
|
||||
function mdxAstToMdxHast() {
|
||||
return tree =>
|
||||
toHast(tree, {
|
||||
passThrough: [
|
||||
'mdxFlowExpression',
|
||||
'mdxJsxFlowElement',
|
||||
'mdxJsxTextElement',
|
||||
'mdxTextExpression',
|
||||
'mdxjsEsm'
|
||||
],
|
||||
handlers: {
|
||||
// Use a custom `inlineCode` element for inline code.
|
||||
inlineCode(h, node) {
|
||||
return h(node, 'inlineCode', [{type: 'text', value: node.value}])
|
||||
},
|
||||
// Add a custom `metastring` attribute to `code` elements,
|
||||
// and support it also as a key/value attribute setter.
|
||||
code(h, node) {
|
||||
const value = node.value ? detab(node.value + '\n') : ''
|
||||
const {lang} = node
|
||||
const props = {}
|
||||
|
||||
if (lang) {
|
||||
props.className = ['language-' + lang]
|
||||
}
|
||||
|
||||
props.metastring = node.meta
|
||||
|
||||
// To do: this handling seems not what users expect:
|
||||
// <https://github.com/mdx-js/mdx/issues/702>.
|
||||
const meta =
|
||||
node.meta &&
|
||||
node.meta.split(' ').reduce((acc, cur) => {
|
||||
if (cur.split('=').length > 1) {
|
||||
const t = cur.split('=')
|
||||
acc[t[0]] = t[1]
|
||||
return acc
|
||||
}
|
||||
|
||||
acc[cur] = true
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
if (meta) {
|
||||
Object.keys(meta).forEach(key => {
|
||||
const isClassKey = key === 'class' || key === 'className'
|
||||
if (props.className && isClassKey) {
|
||||
props.className.push(meta[key])
|
||||
} else {
|
||||
props[key] = meta[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return h(node.position, 'pre', [
|
||||
h(node, 'code', props, [u('text', value)])
|
||||
])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = mdxAstToMdxHast
|
@ -1,397 +0,0 @@
|
||||
const toEstree = require('hast-util-to-estree')
|
||||
const {walk} = require('estree-walker')
|
||||
const periscopic = require('periscopic')
|
||||
const estreeToJs = require('./estree-to-js')
|
||||
|
||||
function serializeEstree(estree, options) {
|
||||
const {
|
||||
// Default options
|
||||
skipExport = false,
|
||||
wrapExport
|
||||
} = options
|
||||
|
||||
let layout
|
||||
let children = []
|
||||
let mdxLayoutDefault
|
||||
|
||||
// Find the `export default`, the JSX expression, and leave the rest
|
||||
// (import/exports) as they are.
|
||||
estree.body = estree.body.filter(child => {
|
||||
// ```js
|
||||
// export default a = 1
|
||||
// ```
|
||||
if (child.type === 'ExportDefaultDeclaration') {
|
||||
layout = child.declaration
|
||||
return false
|
||||
}
|
||||
|
||||
// ```js
|
||||
// export {default} from "a"
|
||||
// export {default as a} from "b"
|
||||
// export {default as a, b} from "c"
|
||||
// export {a as default} from "b"
|
||||
// export {a as default, b} from "c"
|
||||
// ```
|
||||
if (child.type === 'ExportNamedDeclaration' && child.source) {
|
||||
// Remove `default` or `as default`, but not `default as`, specifier.
|
||||
child.specifiers = child.specifiers.filter(specifier => {
|
||||
if (specifier.exported.name === 'default') {
|
||||
mdxLayoutDefault = {local: specifier.local, source: child.source}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Keep the export if there are other specifiers, drop it if there was
|
||||
// just a default.
|
||||
return child.specifiers.length > 0
|
||||
}
|
||||
|
||||
if (
|
||||
child.type === 'ExpressionStatement' &&
|
||||
(child.expression.type === 'JSXFragment' ||
|
||||
child.expression.type === 'JSXElement')
|
||||
) {
|
||||
children =
|
||||
child.expression.type === 'JSXFragment'
|
||||
? child.expression.children
|
||||
: [child.expression]
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Find everything that’s defined in the top-level scope.
|
||||
// Do this here because `estree` currently only includes import/exports
|
||||
// and we don’t have to walk all the JSX to figure out the top scope.
|
||||
const inTopScope = [
|
||||
'MDXLayout',
|
||||
'MDXContent',
|
||||
...periscopic.analyze(estree).scope.declarations.keys()
|
||||
]
|
||||
|
||||
estree.body = [
|
||||
...estree.body,
|
||||
...createMdxLayout(layout, mdxLayoutDefault),
|
||||
...createMdxContent(children)
|
||||
]
|
||||
|
||||
// Add `mdxType`, `parentName` props to JSX elements.
|
||||
const magicShortcodes = []
|
||||
const stack = []
|
||||
|
||||
walk(estree, {
|
||||
enter(node) {
|
||||
if (
|
||||
node.type === 'JSXElement' &&
|
||||
// To do: support members (`<x.y>`).
|
||||
node.openingElement.name.type === 'JSXIdentifier'
|
||||
) {
|
||||
const {name} = node.openingElement.name
|
||||
|
||||
if (stack.length > 1) {
|
||||
const parentName = stack[stack.length - 1]
|
||||
|
||||
node.openingElement.attributes.push({
|
||||
type: 'JSXAttribute',
|
||||
name: {type: 'JSXIdentifier', name: 'parentName'},
|
||||
value: {
|
||||
type: 'Literal',
|
||||
value: parentName,
|
||||
raw: JSON.stringify(parentName)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const head = name.charAt(0)
|
||||
|
||||
// A component.
|
||||
if (head === head.toUpperCase() && name !== 'MDXLayout') {
|
||||
node.openingElement.attributes.push({
|
||||
type: 'JSXAttribute',
|
||||
name: {type: 'JSXIdentifier', name: 'mdxType'},
|
||||
value: {type: 'Literal', value: name, raw: JSON.stringify(name)}
|
||||
})
|
||||
|
||||
if (!inTopScope.includes(name) && !magicShortcodes.includes(name)) {
|
||||
magicShortcodes.push(name)
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(name)
|
||||
}
|
||||
},
|
||||
leave(node) {
|
||||
if (
|
||||
node.type === 'JSXElement' &&
|
||||
// To do: support members (`<x.y>`).
|
||||
node.openingElement.name.type === 'JSXIdentifier'
|
||||
) {
|
||||
stack.pop()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const exports = []
|
||||
|
||||
if (!skipExport) {
|
||||
let declaration = {type: 'Identifier', name: 'MDXContent'}
|
||||
|
||||
if (wrapExport) {
|
||||
declaration = {
|
||||
type: 'CallExpression',
|
||||
callee: {type: 'Identifier', name: wrapExport},
|
||||
arguments: [declaration]
|
||||
}
|
||||
}
|
||||
|
||||
exports.push({type: 'ExportDefaultDeclaration', declaration})
|
||||
}
|
||||
|
||||
estree.body = [
|
||||
...createMakeShortcodeHelper(
|
||||
magicShortcodes,
|
||||
options.mdxFragment === false
|
||||
),
|
||||
...estree.body,
|
||||
...exports
|
||||
]
|
||||
|
||||
return estreeToJs(estree)
|
||||
}
|
||||
|
||||
function compile(options = {}) {
|
||||
function compiler(tree, file) {
|
||||
return serializeEstree(toEstree(tree), {filename: file.path, ...options})
|
||||
}
|
||||
|
||||
this.Compiler = compiler
|
||||
}
|
||||
|
||||
module.exports = compile
|
||||
compile.default = compile
|
||||
|
||||
function createMdxContent(children) {
|
||||
return [
|
||||
{
|
||||
type: 'FunctionDeclaration',
|
||||
id: {type: 'Identifier', name: 'MDXContent'},
|
||||
expression: false,
|
||||
generator: false,
|
||||
async: false,
|
||||
params: [
|
||||
{
|
||||
type: 'ObjectPattern',
|
||||
properties: [
|
||||
{
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: true,
|
||||
computed: false,
|
||||
key: {type: 'Identifier', name: 'components'},
|
||||
kind: 'init',
|
||||
value: {type: 'Identifier', name: 'components'}
|
||||
},
|
||||
{type: 'RestElement', argument: {type: 'Identifier', name: 'props'}}
|
||||
]
|
||||
}
|
||||
],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
body: [
|
||||
{
|
||||
type: 'ReturnStatement',
|
||||
argument: {
|
||||
type: 'JSXElement',
|
||||
openingElement: {
|
||||
type: 'JSXOpeningElement',
|
||||
attributes: [
|
||||
{
|
||||
type: 'JSXAttribute',
|
||||
name: {type: 'JSXIdentifier', name: 'components'},
|
||||
value: {
|
||||
type: 'JSXExpressionContainer',
|
||||
expression: {type: 'Identifier', name: 'components'}
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'JSXSpreadAttribute',
|
||||
argument: {type: 'Identifier', name: 'props'}
|
||||
}
|
||||
],
|
||||
name: {type: 'JSXIdentifier', name: 'MDXLayout'},
|
||||
selfClosing: false
|
||||
},
|
||||
closingElement: {
|
||||
type: 'JSXClosingElement',
|
||||
name: {type: 'JSXIdentifier', name: 'MDXLayout'}
|
||||
},
|
||||
children
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'AssignmentExpression',
|
||||
operator: '=',
|
||||
left: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'MDXContent'},
|
||||
property: {type: 'Identifier', name: 'isMDXComponent'},
|
||||
computed: false,
|
||||
optional: false
|
||||
},
|
||||
right: {type: 'Literal', value: true, raw: 'true'}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function createMdxLayout(declaration, mdxLayoutDefault) {
|
||||
const id = {type: 'Identifier', name: 'MDXLayout'}
|
||||
const init = {type: 'Literal', value: 'wrapper', raw: '"wrapper"'}
|
||||
|
||||
return [
|
||||
mdxLayoutDefault
|
||||
? {
|
||||
type: 'ImportDeclaration',
|
||||
specifiers: [
|
||||
mdxLayoutDefault.local.name === 'default'
|
||||
? {type: 'ImportDefaultSpecifier', local: id}
|
||||
: {
|
||||
type: 'ImportSpecifier',
|
||||
imported: mdxLayoutDefault.local,
|
||||
local: id
|
||||
}
|
||||
],
|
||||
source: {
|
||||
type: 'Literal',
|
||||
value: mdxLayoutDefault.source.value,
|
||||
raw: mdxLayoutDefault.source.raw
|
||||
}
|
||||
}
|
||||
: {
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [
|
||||
{type: 'VariableDeclarator', id, init: declaration || init}
|
||||
],
|
||||
kind: 'const'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
function createMakeShortcodeHelper(names, useElement) {
|
||||
const func = {
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {type: 'Identifier', name: 'makeShortcode'},
|
||||
init: {
|
||||
type: 'ArrowFunctionExpression',
|
||||
id: null,
|
||||
expression: true,
|
||||
generator: false,
|
||||
async: false,
|
||||
params: [{type: 'Identifier', name: 'name'}],
|
||||
body: {
|
||||
type: 'ArrowFunctionExpression',
|
||||
id: null,
|
||||
expression: false,
|
||||
generator: false,
|
||||
async: false,
|
||||
params: [{type: 'Identifier', name: 'props'}],
|
||||
body: {
|
||||
type: 'BlockStatement',
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'CallExpression',
|
||||
callee: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'console'},
|
||||
property: {type: 'Identifier', name: 'warn'},
|
||||
computed: false,
|
||||
optional: false
|
||||
},
|
||||
arguments: [
|
||||
{
|
||||
type: 'Literal',
|
||||
value:
|
||||
'Component `%s` was not imported, exported, or provided by MDXProvider as global scope'
|
||||
},
|
||||
{type: 'Identifier', name: 'name'}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'ReturnStatement',
|
||||
// Vue.
|
||||
/* c8 ignore next 16 */
|
||||
argument: useElement
|
||||
? {
|
||||
type: 'JSXElement',
|
||||
openingElement: {
|
||||
type: 'JSXOpeningElement',
|
||||
attributes: [
|
||||
{
|
||||
type: 'JSXSpreadAttribute',
|
||||
argument: {type: 'Identifier', name: 'props'}
|
||||
}
|
||||
],
|
||||
name: {type: 'JSXIdentifier', name: 'div'},
|
||||
selfClosing: true
|
||||
},
|
||||
closingElement: null,
|
||||
children: []
|
||||
}
|
||||
: {
|
||||
type: 'JSXFragment',
|
||||
openingFragment: {type: 'JSXOpeningFragment'},
|
||||
closingFragment: {type: 'JSXClosingFragment'},
|
||||
children: [
|
||||
{
|
||||
type: 'JSXExpressionContainer',
|
||||
expression: {
|
||||
type: 'MemberExpression',
|
||||
object: {type: 'Identifier', name: 'props'},
|
||||
property: {type: 'Identifier', name: 'children'},
|
||||
computed: false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
kind: 'const'
|
||||
}
|
||||
|
||||
const shortcodes = names.map(name => ({
|
||||
type: 'VariableDeclaration',
|
||||
declarations: [
|
||||
{
|
||||
type: 'VariableDeclarator',
|
||||
id: {type: 'Identifier', name: String(name)},
|
||||
init: {
|
||||
type: 'CallExpression',
|
||||
callee: {type: 'Identifier', name: 'makeShortcode'},
|
||||
arguments: [{type: 'Literal', value: String(name)}]
|
||||
}
|
||||
}
|
||||
],
|
||||
kind: 'const'
|
||||
}))
|
||||
|
||||
return shortcodes.length > 0 ? [func, ...shortcodes] : []
|
||||
}
|
@ -1,13 +1,22 @@
|
||||
{
|
||||
"name": "@mdx-js/mdx",
|
||||
"version": "2.0.0-next.9",
|
||||
"description": "Parse MDX and transpile to JSX",
|
||||
"#": "to do: this prevents for a second that siblings use this",
|
||||
"version": "2.0.0-next.999",
|
||||
"description": "Compile MDX",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"mdx",
|
||||
"markdown",
|
||||
"jsx",
|
||||
"remark",
|
||||
"mdxast"
|
||||
],
|
||||
"homepage": "https://mdxjs.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/mdx-js/mdx",
|
||||
"directory": "packages/mdx"
|
||||
},
|
||||
"homepage": "https://mdxjs.com",
|
||||
"bugs": "https://github.com/mdx-js/mdx/issues",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@ -22,53 +31,61 @@
|
||||
"JounQin <admin@1stg.me> (https://www.1stg.me)",
|
||||
"Christian Murphy <christian.murphy.42@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"types": "types/index.d.ts",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
"main": "index.js",
|
||||
"types": "index.d.ts",
|
||||
"files": [
|
||||
"index.js",
|
||||
"util.js",
|
||||
"estree-to-js.js",
|
||||
"mdx-ast-to-mdx-hast.js",
|
||||
"mdx-hast-to-jsx.js",
|
||||
"types/index.d.ts"
|
||||
"lib/",
|
||||
"index.d.ts",
|
||||
"index.js"
|
||||
],
|
||||
"keywords": [
|
||||
"mdx",
|
||||
"markdown",
|
||||
"react",
|
||||
"jsx",
|
||||
"remark",
|
||||
"mdxast"
|
||||
],
|
||||
"scripts": {
|
||||
"test-api": "uvu -r esbuild-register test \"\\.jsx?$\"",
|
||||
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api",
|
||||
"test-types": "dtslint types",
|
||||
"test": "npm run test-coverage && npm run test-types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/util": "2.0.0-next.1",
|
||||
"astring": "^1.0.0",
|
||||
"detab": "^2.0.0",
|
||||
"estree-walker": "^2.0.0",
|
||||
"hast-util-to-estree": "^1.0.0",
|
||||
"mdast-util-to-hast": "^10.1.0",
|
||||
"periscopic": "^2.0.0",
|
||||
"rehype-minify-whitespace": "^4.0.0",
|
||||
"remark-mdx": "2.0.0-next.9",
|
||||
"remark-parse": "^9.0.0",
|
||||
"remark-squeeze-paragraphs": "^4.0.0",
|
||||
"unified": "^9.0.0",
|
||||
"unist-builder": "^2.0.0"
|
||||
"@types/estree-jsx": "^0.0.1",
|
||||
"astring": "^1.6.0",
|
||||
"estree-util-build-jsx": "^2.0.0",
|
||||
"estree-util-is-identifier-name": "^2.0.0",
|
||||
"estree-walker": "^3.0.0",
|
||||
"hast-util-to-estree": "^2.0.0",
|
||||
"markdown-extensions": "^1.0.0",
|
||||
"mdast-util-mdx": "^1.0.0",
|
||||
"micromark-extension-mdxjs": "^1.0.0",
|
||||
"periscopic": "^3.0.0",
|
||||
"remark-parse": "^10.0.0",
|
||||
"remark-rehype": "^9.0.0",
|
||||
"source-map": "^0.7.0",
|
||||
"unified": "^10.0.0",
|
||||
"unist-util-position-from-estree": "^1.0.0",
|
||||
"unist-util-stringify-position": "^3.0.0",
|
||||
"unist-util-visit": "^4.0.0",
|
||||
"vfile": "^5.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mdx-js/react": "2.0.0-next.9",
|
||||
"@mdx-js/react": "2.0.0-next.8",
|
||||
"react": "^17.0.0",
|
||||
"react-dom": "^17.0.0",
|
||||
"rehype-katex": "^4.0.0",
|
||||
"remark-footnotes": "^3.0.0",
|
||||
"remark-gfm": "^1.0.0",
|
||||
"remark-math": "^4.0.0"
|
||||
"rehype-katex": "^6.0.0",
|
||||
"remark-footnotes": "^4.0.0",
|
||||
"remark-gfm": "^2.0.0",
|
||||
"remark-math": "^5.0.0",
|
||||
"unist-util-remove-position": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"prepack": "npm run build",
|
||||
"build": "rimraf \"lib/**/*.d.ts\" \"test/**/*.d.ts\" \"*.d.ts\" && tsc && type-coverage",
|
||||
"test-api": "node --no-warnings --experimental-loader=../../script/jsx-loader.js ../../node_modules/uvu/bin.js test \"\\.jsx?$\"",
|
||||
"test-coverage": "node --no-warnings --experimental-loader=../../script/jsx-loader.js ../../node_modules/uvu/bin.js test \"\\.jsx?$\"",
|
||||
"#": "to do: use this if xdm tests are ported over: `c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov npm run test-api`",
|
||||
"test": "npm run build && npm run test-coverage"
|
||||
},
|
||||
"typeCoverage": {
|
||||
"atLeast": 100,
|
||||
"detail": true,
|
||||
"strict": true,
|
||||
"ignoreCatch": true,
|
||||
"ignoreFiles": [
|
||||
"lib/util/resolve-evaluate-options.{d.ts,js}"
|
||||
]
|
||||
},
|
||||
"gitHead": "bf7deab69996449cb99c2217dff75e65855eb2c1"
|
||||
}
|
||||
|
@ -1,46 +1,34 @@
|
||||
import {test} from 'uvu'
|
||||
import assert from 'uvu/assert'
|
||||
import path from 'path'
|
||||
import babel from '@babel/core'
|
||||
import unified from 'unified'
|
||||
import * as assert from 'uvu/assert'
|
||||
import {removePosition} from 'unist-util-remove-position'
|
||||
import React from 'react'
|
||||
import {
|
||||
renderToStaticMarkup
|
||||
} from 'react-dom/server'
|
||||
import {mdx as mdxReact, MDXProvider} from '@mdx-js/react'
|
||||
import mdx from '..'
|
||||
import toMdxHast from '../mdx-ast-to-mdx-hast'
|
||||
import toJsx from '../mdx-hast-to-jsx'
|
||||
import * as runtime from 'react/jsx-runtime.js'
|
||||
import {renderToStaticMarkup} from 'react-dom/server.js'
|
||||
// `eslint-plugin-import` is wrong.
|
||||
/* eslint-disable-next-line import/default */
|
||||
import mdxReact from '@mdx-js/react'
|
||||
import footnotes from 'remark-footnotes'
|
||||
import gfm from 'remark-gfm'
|
||||
import math from 'remark-math'
|
||||
import katex from 'rehype-katex'
|
||||
import {compile, compileSync, evaluate, createProcessor} from '../index.js'
|
||||
|
||||
const run = async (value, options = {}) => {
|
||||
const doc = await mdx(value, {...options, skipExport: true})
|
||||
|
||||
// …and that into serialized JS.
|
||||
const {code} = await babel.transformAsync(doc, {
|
||||
configFile: false,
|
||||
plugins: [
|
||||
'@babel/plugin-transform-react-jsx',
|
||||
path.resolve(__dirname, '../../babel-plugin-remove-export-keywords')
|
||||
]
|
||||
})
|
||||
|
||||
// …and finally run it, returning the component.
|
||||
// eslint-disable-next-line no-new-func
|
||||
return new Function('mdx', `${code}; return MDXContent`)(mdxReact)
|
||||
}
|
||||
const {MDXProvider, useMDXComponents} = mdxReact
|
||||
|
||||
test('should generate JSX', async () => {
|
||||
const result = await mdx('Hello World')
|
||||
const result = await compile('Hello World', {jsx: true})
|
||||
|
||||
assert.match(result, /<p>\{"Hello World"\}<\/p>/)
|
||||
assert.match(result, /<_components.p>\{"Hello World"\}<\/_components.p>/)
|
||||
})
|
||||
|
||||
test('should generate runnable JSX', async () => {
|
||||
const Content = await run('Hello World')
|
||||
test('should compile JSX to function calls', async () => {
|
||||
const result = await compile('Hello World')
|
||||
|
||||
assert.match(result, /_jsx\(_components\.p, {\s+children: "Hello World"\s+}\)/)
|
||||
})
|
||||
|
||||
test('should generate runnable JS', async () => {
|
||||
const {default: Content} = await evaluate('Hello World', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -51,7 +39,7 @@ test('should generate runnable JSX', async () => {
|
||||
test('should support `&`, `<`, and `>` in text', async () => {
|
||||
// Note: we don’t allow `<` in MDX files, but the character reference will
|
||||
// get decoded and is present in the AST as `<`.
|
||||
const Content = await run('a < b > c & d')
|
||||
const {default: Content} = await evaluate('a < b > c & d', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -60,7 +48,7 @@ test('should support `&`, `<`, and `>` in text', async () => {
|
||||
})
|
||||
|
||||
test('should generate JSX-compliant strings', async () => {
|
||||
const Content = await run('!["so" cute](cats.com/cat.jpeg)')
|
||||
const {default: Content} = await evaluate('!["so" cute](cats.com/cat.jpeg)', runtime)
|
||||
|
||||
// Note: Escaped quotes (\") isn't valid for JSX string syntax. So we're
|
||||
// making sure that quotes aren't escaped here (prettier doesn't like us
|
||||
@ -77,7 +65,7 @@ test('should generate JSX-compliant strings', async () => {
|
||||
})
|
||||
|
||||
test('should support `remarkPlugins` (math)', async () => {
|
||||
const Content = await run('$x$', {remarkPlugins: [math]})
|
||||
const {default: Content} = await evaluate('$x$', {remarkPlugins: [math], ...runtime})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -90,7 +78,7 @@ test('should support `remarkPlugins` (math)', async () => {
|
||||
})
|
||||
|
||||
test('should support `remarkPlugins` (footnotes)', async () => {
|
||||
const Content = await run('x [^y]\n\n[^y]: z', {remarkPlugins: [footnotes]})
|
||||
const {default: Content} = await evaluate('x [^y]\n\n[^y]: z', {remarkPlugins: [footnotes], ...runtime})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -98,32 +86,28 @@ test('should support `remarkPlugins` (footnotes)', async () => {
|
||||
<>
|
||||
<p>
|
||||
x{' '}
|
||||
<sup id="fnref-y">
|
||||
<a href="#fn-y" className="footnote-ref">
|
||||
y
|
||||
</a>
|
||||
</sup>
|
||||
</p>
|
||||
<div className="footnotes">
|
||||
<hr />
|
||||
<ol>
|
||||
<li id="fn-y">
|
||||
z
|
||||
<a href="#fnref-y" className="footnote-backref">
|
||||
↩
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<a href="#fn1" className="footnote-ref" id="fnref1" role="doc-noteref">
|
||||
<sup>
|
||||
1
|
||||
</sup>
|
||||
</a>
|
||||
</p>{'\n'}
|
||||
<section className="footnotes" role="doc-endnotes">{'\n'}
|
||||
<hr />{'\n'}
|
||||
<ol>{'\n'}
|
||||
<li id="fn1" role="doc-endnote">z<a href="#fnref1" className="footnote-back" role="doc-backlink">↩</a></li>{'\n'}
|
||||
</ol>{'\n'}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
test('should support `rehypePlugins`', async () => {
|
||||
const Content = await run('$x$', {
|
||||
const {default: Content} = await evaluate('$x$', {
|
||||
remarkPlugins: [math],
|
||||
rehypePlugins: [katex]
|
||||
rehypePlugins: [katex],
|
||||
...runtime
|
||||
})
|
||||
|
||||
assert.equal(
|
||||
@ -163,7 +147,7 @@ test('should support async plugins', async () => {
|
||||
tree.children[0].children[0].value = 'y'
|
||||
}
|
||||
|
||||
const Content = await run('x', {remarkPlugins: [plugin]})
|
||||
const {default: Content} = await evaluate('x', {remarkPlugins: [plugin], ...runtime})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -171,38 +155,30 @@ test('should support async plugins', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should support `filepath` to set the vfile’s path', async () => {
|
||||
test('should support a `VFileCompatible` to set the vfile’s path', async () => {
|
||||
const plugin = () => (_, file) => {
|
||||
assert.equal(file.path, 'y')
|
||||
}
|
||||
|
||||
await run('x', {filepath: 'y', remarkPlugins: [plugin]})
|
||||
await evaluate({value: 'x', path: 'y'}, {remarkPlugins: [plugin], ...runtime})
|
||||
})
|
||||
|
||||
test('should use an `inlineCode` “element” in mdxhast', async () => {
|
||||
let called = false
|
||||
const {default: Content} = await evaluate('`x`', runtime)
|
||||
|
||||
const plugin = () => tree => {
|
||||
assert.equal(tree.children[0].children[0], {
|
||||
type: 'element',
|
||||
tagName: 'inlineCode',
|
||||
properties: {},
|
||||
children: [{type: 'text', value: 'x'}],
|
||||
position: {
|
||||
start: {line: 1, column: 1, offset: 0},
|
||||
end: {line: 1, column: 4, offset: 3}
|
||||
}
|
||||
})
|
||||
called = true
|
||||
}
|
||||
|
||||
await run('`x`', {rehypePlugins: [plugin]})
|
||||
|
||||
assert.ok(called)
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<p>
|
||||
<code>x</code>
|
||||
</p>
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
test('should support `pre`/`code` from empty fenced code in mdxhast', async () => {
|
||||
const Content = await run('```\n```')
|
||||
|
||||
test('should support `pre`/`code` from empty fenced code', async () => {
|
||||
const {default: Content} = await evaluate('```\n```', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -214,8 +190,8 @@ test('should support `pre`/`code` from empty fenced code in mdxhast', async () =
|
||||
)
|
||||
})
|
||||
|
||||
test('should support `pre`/`code` from fenced code in mdxhast', async () => {
|
||||
const Content = await run('```\nx\n```')
|
||||
test('should support `pre`/`code` from fenced code', async () => {
|
||||
const {default: Content} = await evaluate('```\nx\n```', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -228,7 +204,7 @@ test('should support `pre`/`code` from fenced code in mdxhast', async () => {
|
||||
})
|
||||
|
||||
test('should support `pre`/`code` from fenced code w/ lang in mdxhast', async () => {
|
||||
const Content = await run('```js\nx\n```')
|
||||
const {default: Content} = await evaluate('```js\nx\n```', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -240,47 +216,24 @@ test('should support `pre`/`code` from fenced code w/ lang in mdxhast', async ()
|
||||
)
|
||||
})
|
||||
|
||||
test('should support attributes from fenced code meta string in mdxhast', async () => {
|
||||
const Content = await run('```js id class=y title=z\nx\n```')
|
||||
const {error} = console
|
||||
console.error = () => { /* Empty */ }
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<pre>
|
||||
<code
|
||||
className="language-js y"
|
||||
metastring="id class=y title=z"
|
||||
title="z"
|
||||
>
|
||||
x{'\n'}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
)
|
||||
|
||||
console.error = error
|
||||
})
|
||||
|
||||
test('should support a block quote in markdown', async () => {
|
||||
const Content = await run('> x\n> `y`')
|
||||
const {default: Content} = await evaluate('> x\n> `y`', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<blockquote>
|
||||
<blockquote>{'\n'}
|
||||
<p>
|
||||
x{'\n'}
|
||||
<code>y</code>
|
||||
</p>
|
||||
</p>{'\n'}
|
||||
</blockquote>
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
test('should support html/jsx inside code in markdown', async () => {
|
||||
const Content = await run('`<x>`')
|
||||
const {default: Content} = await evaluate('`<x>`', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -293,9 +246,7 @@ test('should support html/jsx inside code in markdown', async () => {
|
||||
})
|
||||
|
||||
test('should support tables in markdown', async () => {
|
||||
const Content = await run('| A | B |\n| :- | -: |\n| a | b |', {
|
||||
remarkPlugins: [gfm]
|
||||
})
|
||||
const {default: Content} = await evaluate('| A | B |\n| :- | -: |\n| a | b |', {remarkPlugins: [gfm], ...runtime})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -319,7 +270,7 @@ test('should support tables in markdown', async () => {
|
||||
})
|
||||
|
||||
test('should support line endings in paragraphs', async () => {
|
||||
const Content = await run('x\ny')
|
||||
const {default: Content} = await evaluate('x\ny', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -328,7 +279,7 @@ test('should support line endings in paragraphs', async () => {
|
||||
})
|
||||
|
||||
test('should support line endings between nodes paragraphs', async () => {
|
||||
const Content = await run('*x*\n[y]()')
|
||||
const {default: Content} = await evaluate('*x*\n[y]()', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -343,21 +294,21 @@ test('should support line endings between nodes paragraphs', async () => {
|
||||
})
|
||||
|
||||
test('should support an empty document', async () => {
|
||||
const Content = await run('')
|
||||
const {default: Content} = await evaluate('', runtime)
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<></>))
|
||||
})
|
||||
|
||||
test('should support an ignored node instead of a `root`', async () => {
|
||||
const plugin = () => () => ({type: 'doctype', name: 'html'})
|
||||
const Content = await run('', {rehypePlugins: [plugin]})
|
||||
const {default: Content} = await evaluate('', {rehypePlugins: [plugin], ...runtime})
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<></>))
|
||||
})
|
||||
|
||||
test('should support an element instead of a `root`', async () => {
|
||||
const plugin = () => () => ({type: 'element', tagName: 'x', children: []})
|
||||
const Content = await run('', {rehypePlugins: [plugin]})
|
||||
const {default: Content} = await evaluate('', {rehypePlugins: [plugin], ...runtime})
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<x />))
|
||||
})
|
||||
@ -383,30 +334,33 @@ test('should support imports', async () => {
|
||||
called = true
|
||||
}
|
||||
|
||||
const result = await mdx('import X from "y"', {rehypePlugins: [plugin]})
|
||||
const result = await compile('import X from "y"', {rehypePlugins: [plugin]})
|
||||
|
||||
assert.match(result, /import X from "y"/)
|
||||
assert.ok(called)
|
||||
})
|
||||
|
||||
test('should crash on incorrect imports', async () => {
|
||||
assert.throws(() => {
|
||||
mdx.sync('import a')
|
||||
}, /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
|
||||
try {
|
||||
await compile('import a')
|
||||
assert.unreachable('should not compile')
|
||||
} catch (error) {
|
||||
assert.match(String(error), /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should support import as a word when it’s not the top level', async () => {
|
||||
const Content = await run('> import a\n\n- import b')
|
||||
const {default: Content} = await evaluate('> import a\n\n- import b', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<>
|
||||
<blockquote>
|
||||
<p>import a</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>import b</li>
|
||||
<blockquote>{'\n'}
|
||||
<p>import a</p>{'\n'}
|
||||
</blockquote>{'\n'}
|
||||
<ul>{'\n'}
|
||||
<li>import b</li>{'\n'}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
@ -433,8 +387,9 @@ test('should support exports', async () => {
|
||||
called = true
|
||||
}
|
||||
|
||||
const result = await mdx('export const A = () => <b>!</b>', {
|
||||
rehypePlugins: [plugin]
|
||||
const result = await compile('export const A = () => <b>!</b>', {
|
||||
rehypePlugins: [plugin],
|
||||
jsx: true
|
||||
})
|
||||
|
||||
assert.match(result, /export const A = \(\) => <b>!<\/b>/)
|
||||
@ -442,23 +397,26 @@ test('should support exports', async () => {
|
||||
})
|
||||
|
||||
test('should crash on incorrect exports', async () => {
|
||||
assert.throws(() => {
|
||||
mdx.sync('export a')
|
||||
}, /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
|
||||
try {
|
||||
await compile('export a')
|
||||
assert.unreachable('should not compile')
|
||||
} catch (error) {
|
||||
assert.match(String(error), /Could not parse import\/exports with acorn: SyntaxError: Unexpected token/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should support export as a word when it’s not the top level', async () => {
|
||||
const Content = await run('> export a\n\n- export b')
|
||||
const {default: Content} = await evaluate('> export a\n\n- export b', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<>
|
||||
<blockquote>
|
||||
<p>export a</p>
|
||||
</blockquote>
|
||||
<ul>
|
||||
<li>export b</li>
|
||||
<blockquote>{'\n'}
|
||||
<p>export a</p>{'\n'}
|
||||
</blockquote>{'\n'}
|
||||
<ul>{'\n'}
|
||||
<li>export b</li>{'\n'}
|
||||
</ul>
|
||||
</>
|
||||
)
|
||||
@ -466,7 +424,7 @@ test('should support export as a word when it’s not the top level', async () =
|
||||
})
|
||||
|
||||
test('should support JSX (flow, block)', async () => {
|
||||
const Content = await run('<main><span /></main>')
|
||||
const {default: Content} = await evaluate('<main><span /></main>', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -479,7 +437,7 @@ test('should support JSX (flow, block)', async () => {
|
||||
})
|
||||
|
||||
test('should support JSX (text, inline)', async () => {
|
||||
const Content = await run('x <span /> y')
|
||||
const {default: Content} = await evaluate('x <span /> y', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -492,13 +450,13 @@ test('should support JSX (text, inline)', async () => {
|
||||
})
|
||||
|
||||
test('should support JSX expressions (flow, block)', async () => {
|
||||
const Content = await run('{1 + 1}')
|
||||
const {default: Content} = await evaluate('{1 + 1}', runtime)
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), '2')
|
||||
})
|
||||
|
||||
test('should support JSX expressions (text, inline)', async () => {
|
||||
const Content = await run('x {1 + 1} y')
|
||||
const {default: Content} = await evaluate('x {1 + 1} y', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -507,7 +465,7 @@ test('should support JSX expressions (text, inline)', async () => {
|
||||
})
|
||||
|
||||
test('should support a default export for a layout', async () => {
|
||||
const Content = await run('export default props => <main {...props} />\n\nx')
|
||||
const {default: Content} = await evaluate('export default props => <main {...props} />\n\nx', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -520,36 +478,34 @@ test('should support a default export for a layout', async () => {
|
||||
})
|
||||
|
||||
test('should support a default export from an import', async () => {
|
||||
let result = await mdx('import a from "b"\nexport default a')
|
||||
let result = await compile('import a from "b"\nexport default a')
|
||||
assert.match(result, /import a from "b"/)
|
||||
assert.match(result, /const MDXLayout = a/)
|
||||
|
||||
result = await mdx('export {default} from "a"')
|
||||
result = await compile('export {default} from "a"')
|
||||
assert.match(result, /import MDXLayout from "a"/)
|
||||
|
||||
// These are not export defaults: they imports default but export as
|
||||
// something else.
|
||||
result = await mdx('export {default as a} from "b"')
|
||||
result = await compile('export {default as a} from "b"')
|
||||
assert.match(result, /export {default as a} from "b"/)
|
||||
assert.match(result, /const MDXLayout = "wrapper"/)
|
||||
result = await mdx('export {default as a, b} from "c"')
|
||||
assert.match(result, /{wrapper: MDXLayout}/)
|
||||
result = await compile('export {default as a, b} from "c"')
|
||||
assert.match(result, /export {default as a, b} from "c"/)
|
||||
assert.match(result, /const MDXLayout = "wrapper"/)
|
||||
assert.match(result, /{wrapper: MDXLayout}/)
|
||||
|
||||
// These are export defaults.
|
||||
result = await mdx('export {a as default} from "b"')
|
||||
result = await compile('export {a as default} from "b"')
|
||||
assert.match(result, /import {a as MDXLayout} from "b"/)
|
||||
assert.not.match(result, /const MDXLayout/)
|
||||
result = await mdx('export {a as default, b} from "c"')
|
||||
result = await compile('export {a as default, b} from "c"')
|
||||
assert.match(result, /export {b} from "c"/)
|
||||
assert.match(result, /import {a as MDXLayout} from "c"/)
|
||||
assert.not.match(result, /const MDXLayout/)
|
||||
})
|
||||
|
||||
test('should support semicolons in the default export', async () => {
|
||||
const Content = await run(
|
||||
'export default props => <section {...props} />;\n\nx'
|
||||
)
|
||||
const {default: Content} = await evaluate('export default props => <section {...props} />;\n\nx', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -562,9 +518,7 @@ test('should support semicolons in the default export', async () => {
|
||||
})
|
||||
|
||||
test('should support a multiline default export', async () => {
|
||||
const Content = await run(
|
||||
'export default ({children}) => (\n <article>\n {children}\n </article>\n)\n\nx'
|
||||
)
|
||||
const {default: Content} = await evaluate('export default ({children}) => (\n <article>\n {children}\n </article>\n)\n\nx', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -577,13 +531,13 @@ test('should support a multiline default export', async () => {
|
||||
})
|
||||
|
||||
test('should support using a non-default export in content', async () => {
|
||||
const Content = await run('export var X = props => <b {...props} />\n\n<X />')
|
||||
const {default: Content} = await evaluate('export var X = props => <b {...props} />\n\n<X />', runtime)
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<b />))
|
||||
})
|
||||
|
||||
test('should support overwriting missing compile-time components at run-time', async () => {
|
||||
const Content = await run('x <Y /> z')
|
||||
const {default: Content} = await evaluate('x <Y /> z', {...runtime, useMDXComponents})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(
|
||||
@ -605,33 +559,30 @@ test('should support overwriting missing compile-time components at run-time', a
|
||||
)
|
||||
})
|
||||
|
||||
test('should not crash but issue a warning when an undefined component is used', async () => {
|
||||
const Content = await run('w <X>y</X> z')
|
||||
test('should throw when an undefined component is used', async () => {
|
||||
const {default: Content} = await evaluate('w <X>y</X> z', runtime)
|
||||
const calls = []
|
||||
const {warn} = console
|
||||
console.warn = (...parameters) => {
|
||||
const {error} = console
|
||||
|
||||
console.error = (...parameters) => {
|
||||
calls.push(parameters)
|
||||
}
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(<p>w y z</p>)
|
||||
)
|
||||
try {
|
||||
renderToStaticMarkup(<Content />)
|
||||
assert.unreachable('should not compile')
|
||||
} catch (error) {
|
||||
assert.match(String(error), /Error: Element type is invalid/)
|
||||
}
|
||||
|
||||
assert.equal(calls, [
|
||||
[
|
||||
'Component `%s` was not imported, exported, or provided by MDXProvider as global scope',
|
||||
'X'
|
||||
]
|
||||
])
|
||||
console.error = error
|
||||
|
||||
console.warn = warn
|
||||
assert.equal(calls.length, 1)
|
||||
assert.match(calls[0][0], /Warning: React.jsx: type is invalid/)
|
||||
})
|
||||
|
||||
test('should support `.` in component names for members', async () => {
|
||||
const Content = await run(
|
||||
'export var x = {y: props => <b {...props} />}\n\n<x.y />'
|
||||
)
|
||||
const {default: Content} = await evaluate('export var x = {y: props => <b {...props} />}\n\n<x.y />', runtime)
|
||||
|
||||
assert.equal(renderToStaticMarkup(<Content />), renderToStaticMarkup(<b />))
|
||||
})
|
||||
@ -648,9 +599,12 @@ test('should crash on unknown nodes in mdxhast', async () => {
|
||||
})
|
||||
}
|
||||
|
||||
assert.throws(() => {
|
||||
mdx.sync('x', {rehypePlugins: [plugin]})
|
||||
}, /Cannot handle unknown node `unknown`/)
|
||||
try {
|
||||
await compile('x', {rehypePlugins: [plugin]})
|
||||
assert.unreachable('should not compile')
|
||||
} catch (error) {
|
||||
assert.match(String(error), /Cannot handle unknown node `unknown`/)
|
||||
}
|
||||
})
|
||||
|
||||
test('should support `element` nodes w/o `properties` in mdxhast', async () => {
|
||||
@ -662,7 +616,7 @@ test('should support `element` nodes w/o `properties` in mdxhast', async () => {
|
||||
})
|
||||
}
|
||||
|
||||
const Content = await run('x', {rehypePlugins: [plugin]})
|
||||
const {default: Content} = await evaluate('x', {rehypePlugins: [plugin], ...runtime})
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -675,35 +629,9 @@ test('should support `element` nodes w/o `properties` in mdxhast', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('should support `skipExport` to not export anything', async () => {
|
||||
const resultDefault = await mdx('x')
|
||||
const resultTrue = await mdx('x', {skipExport: true})
|
||||
const resultFalse = await mdx('x', {skipExport: false})
|
||||
|
||||
assert.equal(resultDefault, resultFalse)
|
||||
assert.match(resultTrue, /\nfunction MDXContent/)
|
||||
assert.match(resultFalse, /export default MDXContent/)
|
||||
})
|
||||
|
||||
test('should support `wrapExport` to wrap the exported value', async () => {
|
||||
const resultDefault = await mdx('x')
|
||||
const resultValue = await mdx('x', {wrapExport: 'y'})
|
||||
const resultNull = await mdx('x', {wrapExport: null})
|
||||
|
||||
assert.equal(resultDefault, resultNull)
|
||||
assert.match(resultValue, /export default y\(MDXContent\)/)
|
||||
assert.match(resultNull, /export default MDXContent/)
|
||||
})
|
||||
|
||||
test('should expose an `isMDXComponent` field on the component', async () => {
|
||||
const Content = await run('x')
|
||||
|
||||
assert.equal(Content.isMDXComponent, true)
|
||||
})
|
||||
|
||||
test('should escape what could look like template literal placeholders in text', async () => {
|
||||
/* eslint-disable no-template-curly-in-string */
|
||||
const Content = await run('`${x}`')
|
||||
const {default: Content} = await evaluate('`${x}`', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -717,7 +645,7 @@ test('should escape what could look like template literal placeholders in text',
|
||||
})
|
||||
|
||||
test('should support a dollar in text', async () => {
|
||||
const Content = await run('$')
|
||||
const {default: Content} = await evaluate('$', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -726,7 +654,7 @@ test('should support a dollar in text', async () => {
|
||||
})
|
||||
|
||||
test('should support an escaped dollar in text', async () => {
|
||||
const Content = await run('\\$')
|
||||
const {default: Content} = await evaluate('\\$', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
@ -735,129 +663,77 @@ test('should support an escaped dollar in text', async () => {
|
||||
})
|
||||
|
||||
test('should support an empty expression in JSX', async () => {
|
||||
const Content = await run('<x>{}</x>')
|
||||
const {default: Content} = await evaluate('<x>{}</x>', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<p>
|
||||
<x />
|
||||
</p>
|
||||
)
|
||||
renderToStaticMarkup(<x />)
|
||||
)
|
||||
})
|
||||
|
||||
test('should support a more complex expression in JSX', async () => {
|
||||
const Content = await run('<x>{(() => 1 + 2)(1)}</x>')
|
||||
const {default: Content} = await evaluate('<x>{(() => 1 + 2)(1)}</x>', runtime)
|
||||
|
||||
assert.equal(
|
||||
renderToStaticMarkup(<Content />),
|
||||
renderToStaticMarkup(
|
||||
<p>
|
||||
<x>3</x>
|
||||
</p>
|
||||
)
|
||||
renderToStaticMarkup(<x>3</x>)
|
||||
)
|
||||
})
|
||||
|
||||
test('default: should be async', async () => {
|
||||
assert.match(await mdx('x'), /<p>{"x"}<\/p>/)
|
||||
assert.match(await compile('x', {jsx: true}), /<_components\.p>{"x"}<\/_components\.p>/)
|
||||
})
|
||||
|
||||
test('default: should support `remarkPlugins`', async () => {
|
||||
assert.match(
|
||||
await mdx('$x$', {remarkPlugins: [math]}),
|
||||
await compile('$x$', {jsx: true, remarkPlugins: [math]}),
|
||||
/className="math math-inline"/
|
||||
)
|
||||
})
|
||||
|
||||
test('sync: should be sync', () => {
|
||||
assert.match(mdx.sync('x'), /<p>{"x"}<\/p>/)
|
||||
assert.match(compileSync('x', {jsx: true}), /<_components\.p>{"x"}<\/_components\.p>/)
|
||||
})
|
||||
|
||||
test('sync: should support `remarkPlugins`', () => {
|
||||
assert.match(
|
||||
mdx.sync('$x$', {remarkPlugins: [math]}),
|
||||
/className="math math-inline"/
|
||||
)
|
||||
assert.match(compileSync('$x$', {remarkPlugins: [math], jsx: true}), /className="math math-inline"/)
|
||||
})
|
||||
|
||||
test('createMdxAstCompiler: should create a unified processor', () => {
|
||||
const result = mdx.createMdxAstCompiler()
|
||||
const tree = result.runSync(result.parse('x'))
|
||||
test('should create a unified processor', async () => {
|
||||
const remarkPlugin = () => (tree) => {
|
||||
const clone = removePosition(JSON.parse(JSON.stringify(tree)), true)
|
||||
|
||||
assert.equal(tree, {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'p',
|
||||
properties: {},
|
||||
children: [
|
||||
{
|
||||
type: 'text',
|
||||
value: 'x',
|
||||
position: {
|
||||
start: {line: 1, column: 1, offset: 0},
|
||||
end: {line: 1, column: 2, offset: 1}
|
||||
}
|
||||
}
|
||||
],
|
||||
position: {
|
||||
start: {line: 1, column: 1, offset: 0},
|
||||
end: {line: 1, column: 2, offset: 1}
|
||||
assert.equal(clone, {
|
||||
type: 'root',
|
||||
children: [
|
||||
{ type: 'paragraph', children: [ { type: 'text', value: 'x' } ] }
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const rehypePlugin = () => (tree) => {
|
||||
const clone = removePosition(JSON.parse(JSON.stringify(tree)), true)
|
||||
|
||||
assert.equal(clone, {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'p',
|
||||
properties: {},
|
||||
children: [ { type: 'text', value: 'x' } ]
|
||||
}
|
||||
}
|
||||
],
|
||||
position: {
|
||||
start: {line: 1, column: 1, offset: 0},
|
||||
end: {line: 1, column: 2, offset: 1}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test('createCompiler: should create a unified processor', () => {
|
||||
assert.match(String(mdx.createCompiler().processSync('x')), /<p>{"x"}<\/p>/)
|
||||
})
|
||||
|
||||
test('mdx-ast-to-mdx-hast: should be a unified plugin transforming the tree', () => {
|
||||
const mdxast = {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'paragraph',
|
||||
children: [{type: 'mdxTextExpression', value: '1 + 1'}]
|
||||
}
|
||||
]
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
const mdxhast = unified().use(toMdxHast).runSync(mdxast)
|
||||
|
||||
assert.equal(mdxhast, {
|
||||
type: 'root',
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'p',
|
||||
properties: {},
|
||||
children: [{type: 'mdxTextExpression', value: '1 + 1'}]
|
||||
}
|
||||
]
|
||||
const processor = createProcessor({
|
||||
remarkPlugins: [remarkPlugin],
|
||||
rehypePlugins: [rehypePlugin],
|
||||
jsx: true
|
||||
})
|
||||
})
|
||||
|
||||
test('mdx-hast-to-jsx: should be a unified plugin defining a compiler', () => {
|
||||
const tree = {
|
||||
type: 'root',
|
||||
children: [
|
||||
{type: 'element', tagName: 'x', children: [{type: 'text', value: 'a'}]}
|
||||
]
|
||||
}
|
||||
|
||||
const doc = unified().use(toJsx).stringify(tree)
|
||||
|
||||
assert.match(doc, /export default MDXContent/)
|
||||
assert.match(doc, /<x>\{"a"}<\/x>/)
|
||||
assert.match(await processor.process('x'), /<_components\.p>{"x"}<\/_components\.p>/)
|
||||
})
|
||||
|
||||
test.run()
|
||||
|
16
packages/mdx/tsconfig.json
Normal file
16
packages/mdx/tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"include": ["lib/**/*.js", "test/**/*.js", "*.js"],
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"lib": ["ES2020"],
|
||||
"module": "ES2020",
|
||||
"moduleResolution": "node",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"declaration": true,
|
||||
"emitDeclarationOnly": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
console.warn('@mdx-js/util is deprecated: please update the code using it')
|
||||
module.exports = require('@mdx-js/util')
|
Loading…
Reference in New Issue
Block a user