site/node_modules/mdast-util-mdx-jsx/lib/index.js

767 lines
22 KiB
JavaScript
Raw Permalink Normal View History

2024-10-14 06:09:33 +00:00
/**
* @typedef {import('mdast-util-from-markdown').CompileContext} CompileContext
* @typedef {import('mdast-util-from-markdown').Extension} FromMarkdownExtension
* @typedef {import('mdast-util-from-markdown').Handle} FromMarkdownHandle
* @typedef {import('mdast-util-from-markdown').OnEnterError} OnEnterError
* @typedef {import('mdast-util-from-markdown').OnExitError} OnExitError
* @typedef {import('mdast-util-from-markdown').Token} Token
*
* @typedef {import('mdast-util-to-markdown').Handle} ToMarkdownHandle
* @typedef {import('mdast-util-to-markdown').Options} ToMarkdownExtension
* @typedef {import('mdast-util-to-markdown').State} State
* @typedef {import('mdast-util-to-markdown').Tracker} Tracker
*
* @typedef {import('../index.js').MdxJsxAttribute} MdxJsxAttribute
* @typedef {import('../index.js').MdxJsxAttributeValueExpression} MdxJsxAttributeValueExpression
* @typedef {import('../index.js').MdxJsxExpressionAttribute} MdxJsxExpressionAttribute
* @typedef {import('../index.js').MdxJsxFlowElement} MdxJsxFlowElement
* @typedef {import('../index.js').MdxJsxTextElement} MdxJsxTextElement
*/
/**
* @typedef Tag
* Single tag.
* @property {string | undefined} name
* Name of tag, or `undefined` for fragment.
*
* > 👉 **Note**: `null` is used in the AST for fragments, as it serializes in
* > JSON.
* @property {Array<MdxJsxAttribute | MdxJsxExpressionAttribute>} attributes
* Attributes.
* @property {boolean} close
* Whether the tag is closing (`</x>`).
* @property {boolean} selfClosing
* Whether the tag is self-closing (`<x/>`).
* @property {Token['start']} start
* Start point.
* @property {Token['start']} end
* End point.
*
* @typedef ToMarkdownOptions
* Configuration.
* @property {'"' | "'" | null | undefined} [quote='"']
* Preferred quote to use around attribute values (default: `'"'`).
* @property {boolean | null | undefined} [quoteSmart=false]
* Use the other quote if that results in less bytes (default: `false`).
* @property {boolean | null | undefined} [tightSelfClosing=false]
* Do not use an extra space when closing self-closing elements: `<img/>`
* instead of `<img />` (default: `false`).
* @property {number | null | undefined} [printWidth=Infinity]
* Try and wrap syntax at this width (default: `Infinity`).
*
* When set to a finite number (say, `80`), the formatter will print
* attributes on separate lines when a tag doesnt fit on one line.
* The normal behavior is to print attributes with spaces between them
* instead of line endings.
*/
import {ccount} from 'ccount'
import {ok as assert} from 'devlop'
import {parseEntities} from 'parse-entities'
import {stringifyEntitiesLight} from 'stringify-entities'
import {stringifyPosition} from 'unist-util-stringify-position'
import {VFileMessage} from 'vfile-message'
const indent = ' '
/**
* Create an extension for `mdast-util-from-markdown` to enable MDX JSX.
*
* @returns {FromMarkdownExtension}
* Extension for `mdast-util-from-markdown` to enable MDX JSX.
*
* When using the syntax extension with `addResult`, nodes will have a
* `data.estree` field set to an ESTree `Program` node.
*/
export function mdxJsxFromMarkdown() {
return {
canContainEols: ['mdxJsxTextElement'],
enter: {
mdxJsxFlowTag: enterMdxJsxTag,
mdxJsxFlowTagClosingMarker: enterMdxJsxTagClosingMarker,
mdxJsxFlowTagAttribute: enterMdxJsxTagAttribute,
mdxJsxFlowTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
mdxJsxFlowTagAttributeValueLiteral: buffer,
mdxJsxFlowTagAttributeValueExpression: buffer,
mdxJsxFlowTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker,
mdxJsxTextTag: enterMdxJsxTag,
mdxJsxTextTagClosingMarker: enterMdxJsxTagClosingMarker,
mdxJsxTextTagAttribute: enterMdxJsxTagAttribute,
mdxJsxTextTagExpressionAttribute: enterMdxJsxTagExpressionAttribute,
mdxJsxTextTagAttributeValueLiteral: buffer,
mdxJsxTextTagAttributeValueExpression: buffer,
mdxJsxTextTagSelfClosingMarker: enterMdxJsxTagSelfClosingMarker
},
exit: {
mdxJsxFlowTagClosingMarker: exitMdxJsxTagClosingMarker,
mdxJsxFlowTagNamePrimary: exitMdxJsxTagNamePrimary,
mdxJsxFlowTagNameMember: exitMdxJsxTagNameMember,
mdxJsxFlowTagNameLocal: exitMdxJsxTagNameLocal,
mdxJsxFlowTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
mdxJsxFlowTagExpressionAttributeValue: data,
mdxJsxFlowTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
mdxJsxFlowTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
mdxJsxFlowTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
mdxJsxFlowTagAttributeValueLiteralValue: data,
mdxJsxFlowTagAttributeValueExpression:
exitMdxJsxTagAttributeValueExpression,
mdxJsxFlowTagAttributeValueExpressionValue: data,
mdxJsxFlowTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
mdxJsxFlowTag: exitMdxJsxTag,
mdxJsxTextTagClosingMarker: exitMdxJsxTagClosingMarker,
mdxJsxTextTagNamePrimary: exitMdxJsxTagNamePrimary,
mdxJsxTextTagNameMember: exitMdxJsxTagNameMember,
mdxJsxTextTagNameLocal: exitMdxJsxTagNameLocal,
mdxJsxTextTagExpressionAttribute: exitMdxJsxTagExpressionAttribute,
mdxJsxTextTagExpressionAttributeValue: data,
mdxJsxTextTagAttributeNamePrimary: exitMdxJsxTagAttributeNamePrimary,
mdxJsxTextTagAttributeNameLocal: exitMdxJsxTagAttributeNameLocal,
mdxJsxTextTagAttributeValueLiteral: exitMdxJsxTagAttributeValueLiteral,
mdxJsxTextTagAttributeValueLiteralValue: data,
mdxJsxTextTagAttributeValueExpression:
exitMdxJsxTagAttributeValueExpression,
mdxJsxTextTagAttributeValueExpressionValue: data,
mdxJsxTextTagSelfClosingMarker: exitMdxJsxTagSelfClosingMarker,
mdxJsxTextTag: exitMdxJsxTag
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function buffer() {
this.buffer()
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function data(token) {
this.config.enter.data.call(this, token)
this.config.exit.data.call(this, token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTag(token) {
/** @type {Tag} */
const tag = {
name: undefined,
attributes: [],
close: false,
selfClosing: false,
start: token.start,
end: token.end
}
if (!this.data.mdxJsxTagStack) this.data.mdxJsxTagStack = []
this.data.mdxJsxTag = tag
this.buffer()
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTagClosingMarker(token) {
const stack = this.data.mdxJsxTagStack
assert(stack, 'expected `mdxJsxTagStack`')
if (stack.length === 0) {
throw new VFileMessage(
'Unexpected closing slash `/` in tag, expected an open tag first',
{start: token.start, end: token.end},
'mdast-util-mdx-jsx:unexpected-closing-slash'
)
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTagAnyAttribute(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
if (tag.close) {
throw new VFileMessage(
'Unexpected attribute in closing tag, expected the end of the tag',
{start: token.start, end: token.end},
'mdast-util-mdx-jsx:unexpected-attribute'
)
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTagSelfClosingMarker(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
if (tag.close) {
throw new VFileMessage(
'Unexpected self-closing slash `/` in closing tag, expected the end of the tag',
{start: token.start, end: token.end},
'mdast-util-mdx-jsx:unexpected-self-closing-slash'
)
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagClosingMarker() {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.close = true
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagNamePrimary(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.name = this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagNameMember(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.name += '.' + this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagNameLocal(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.name += ':' + this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTagAttribute(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
enterMdxJsxTagAnyAttribute.call(this, token)
tag.attributes.push({type: 'mdxJsxAttribute', name: '', value: null})
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function enterMdxJsxTagExpressionAttribute(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
enterMdxJsxTagAnyAttribute.call(this, token)
tag.attributes.push({type: 'mdxJsxExpressionAttribute', value: ''})
this.buffer()
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagExpressionAttribute(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const tail = tag.attributes[tag.attributes.length - 1]
assert(tail.type === 'mdxJsxExpressionAttribute')
const estree = token.estree
tail.value = this.resume()
if (estree) {
tail.data = {estree}
}
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagAttributeNamePrimary(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const node = tag.attributes[tag.attributes.length - 1]
assert(node.type === 'mdxJsxAttribute')
node.name = this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagAttributeNameLocal(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const node = tag.attributes[tag.attributes.length - 1]
assert(node.type === 'mdxJsxAttribute')
node.name += ':' + this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagAttributeValueLiteral() {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.attributes[tag.attributes.length - 1].value = parseEntities(
this.resume(),
{nonTerminated: false}
)
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagAttributeValueExpression(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const tail = tag.attributes[tag.attributes.length - 1]
assert(tail.type === 'mdxJsxAttribute')
/** @type {MdxJsxAttributeValueExpression} */
const node = {type: 'mdxJsxAttributeValueExpression', value: this.resume()}
const estree = token.estree
if (estree) {
node.data = {estree}
}
tail.value = node
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTagSelfClosingMarker() {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
tag.selfClosing = true
}
/**
* @this {CompileContext}
* @type {FromMarkdownHandle}
*/
function exitMdxJsxTag(token) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const stack = this.data.mdxJsxTagStack
assert(stack, 'expected `mdxJsxTagStack`')
const tail = stack[stack.length - 1]
if (tag.close && tail.name !== tag.name) {
throw new VFileMessage(
'Unexpected closing tag `' +
serializeAbbreviatedTag(tag) +
'`, expected corresponding closing tag for `' +
serializeAbbreviatedTag(tail) +
'` (' +
stringifyPosition(tail) +
')',
{start: token.start, end: token.end},
'mdast-util-mdx-jsx:end-tag-mismatch'
)
}
// End of a tag, so drop the buffer.
this.resume()
if (tag.close) {
stack.pop()
} else {
this.enter(
{
type:
token.type === 'mdxJsxTextTag'
? 'mdxJsxTextElement'
: 'mdxJsxFlowElement',
name: tag.name || null,
attributes: tag.attributes,
children: []
},
token,
onErrorRightIsTag
)
}
if (tag.selfClosing || tag.close) {
this.exit(token, onErrorLeftIsTag)
} else {
stack.push(tag)
}
}
/**
* @this {CompileContext}
* @type {OnEnterError}
*/
function onErrorRightIsTag(closing, open) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
const place = closing ? ' before the end of `' + closing.type + '`' : ''
const position = closing
? {start: closing.start, end: closing.end}
: undefined
throw new VFileMessage(
'Expected a closing tag for `' +
serializeAbbreviatedTag(tag) +
'` (' +
stringifyPosition({start: open.start, end: open.end}) +
')' +
place,
position,
'mdast-util-mdx-jsx:end-tag-mismatch'
)
}
/**
* @this {CompileContext}
* @type {OnExitError}
*/
function onErrorLeftIsTag(a, b) {
const tag = this.data.mdxJsxTag
assert(tag, 'expected `mdxJsxTag`')
throw new VFileMessage(
'Expected the closing tag `' +
serializeAbbreviatedTag(tag) +
'` either after the end of `' +
b.type +
'` (' +
stringifyPosition(b.end) +
') or another opening tag after the start of `' +
b.type +
'` (' +
stringifyPosition(b.start) +
')',
{start: a.start, end: a.end},
'mdast-util-mdx-jsx:end-tag-mismatch'
)
}
/**
* Serialize a tag, excluding attributes.
* `self-closing` is not supported, because we dont need it yet.
*
* @param {Tag} tag
* @returns {string}
*/
function serializeAbbreviatedTag(tag) {
return '<' + (tag.close ? '/' : '') + (tag.name || '') + '>'
}
}
/**
* Create an extension for `mdast-util-to-markdown` to enable MDX JSX.
*
* This extension configures `mdast-util-to-markdown` with
* `options.fences: true` and `options.resourceLink: true` too, do not
* overwrite them!
*
* @param {ToMarkdownOptions | null | undefined} [options]
* Configuration (optional).
* @returns {ToMarkdownExtension}
* Extension for `mdast-util-to-markdown` to enable MDX JSX.
*/
export function mdxJsxToMarkdown(options) {
const options_ = options || {}
const quote = options_.quote || '"'
const quoteSmart = options_.quoteSmart || false
const tightSelfClosing = options_.tightSelfClosing || false
const printWidth = options_.printWidth || Number.POSITIVE_INFINITY
const alternative = quote === '"' ? "'" : '"'
if (quote !== '"' && quote !== "'") {
throw new Error(
'Cannot serialize attribute values with `' +
quote +
'` for `options.quote`, expected `"`, or `\'`'
)
}
mdxElement.peek = peekElement
return {
handlers: {
mdxJsxFlowElement: mdxElement,
mdxJsxTextElement: mdxElement
},
unsafe: [
{character: '<', inConstruct: ['phrasing']},
{atBreak: true, character: '<'}
],
// Always generate fenced code (never indented code).
fences: true,
// Always generate links with resources (never autolinks).
resourceLink: true
}
/**
* @type {ToMarkdownHandle}
* @param {MdxJsxFlowElement | MdxJsxTextElement} node
*/
// eslint-disable-next-line complexity
function mdxElement(node, _, state, info) {
const flow = node.type === 'mdxJsxFlowElement'
const selfClosing = node.name
? !node.children || node.children.length === 0
: false
const depth = inferDepth(state)
const currentIndent = createIndent(depth)
const trackerOneLine = state.createTracker(info)
const trackerMultiLine = state.createTracker(info)
/** @type {Array<string>} */
const serializedAttributes = []
const prefix = (flow ? currentIndent : '') + '<' + (node.name || '')
const exit = state.enter(node.type)
trackerOneLine.move(prefix)
trackerMultiLine.move(prefix)
// None.
if (node.attributes && node.attributes.length > 0) {
if (!node.name) {
throw new Error('Cannot serialize fragment w/ attributes')
}
let index = -1
while (++index < node.attributes.length) {
const attribute = node.attributes[index]
/** @type {string} */
let result
if (attribute.type === 'mdxJsxExpressionAttribute') {
result = '{' + (attribute.value || '') + '}'
} else {
if (!attribute.name) {
throw new Error('Cannot serialize attribute w/o name')
}
const value = attribute.value
const left = attribute.name
/** @type {string} */
let right = ''
if (value === null || value === undefined) {
// Empty.
} else if (typeof value === 'object') {
right = '{' + (value.value || '') + '}'
} else {
// If the alternative is less common than `quote`, switch.
const appliedQuote =
quoteSmart && ccount(value, quote) > ccount(value, alternative)
? alternative
: quote
right =
appliedQuote +
stringifyEntitiesLight(value, {subset: [appliedQuote]}) +
appliedQuote
}
result = left + (right ? '=' : '') + right
}
serializedAttributes.push(result)
}
}
let attributesOnTheirOwnLine = false
const attributesOnOneLine = serializedAttributes.join(' ')
if (
// Block:
flow &&
// Including a line ending (expressions).
(/\r?\n|\r/.test(attributesOnOneLine) ||
// Current position (including `<tag`).
trackerOneLine.current().now.column +
// -1 because columns, +1 for ` ` before attributes.
// Attributes joined by spaces.
attributesOnOneLine.length +
// ` />`.
(selfClosing ? (tightSelfClosing ? 2 : 3) : 1) >
printWidth)
) {
attributesOnTheirOwnLine = true
}
let tracker = trackerOneLine
let value = prefix
if (attributesOnTheirOwnLine) {
tracker = trackerMultiLine
let index = -1
while (++index < serializedAttributes.length) {
// Only indent first line of of attributes, we cant indent attribute
// values.
serializedAttributes[index] =
currentIndent + indent + serializedAttributes[index]
}
value += tracker.move(
'\n' + serializedAttributes.join('\n') + '\n' + currentIndent
)
} else if (attributesOnOneLine) {
value += tracker.move(' ' + attributesOnOneLine)
}
if (selfClosing) {
value += tracker.move(
(tightSelfClosing || attributesOnTheirOwnLine ? '' : ' ') + '/'
)
}
value += tracker.move('>')
if (node.children && node.children.length > 0) {
if (node.type === 'mdxJsxTextElement') {
value += tracker.move(
// @ts-expect-error: `containerPhrasing` is typed correctly, but TS
// generates *hardcoded* types, which means that our dynamically added
// directives are not present.
// At some point, TS should fix that, and `from-markdown` should be fine.
state.containerPhrasing(node, {
...tracker.current(),
before: '>',
after: '<'
})
)
} else {
tracker.shift(2)
value += tracker.move('\n')
value += tracker.move(containerFlow(node, state, tracker.current()))
value += tracker.move('\n')
}
}
if (!selfClosing) {
value += tracker.move(
(flow ? currentIndent : '') + '</' + (node.name || '') + '>'
)
}
exit()
return value
}
}
// Modified copy of:
// <https://github.com/syntax-tree/mdast-util-to-markdown/blob/a381cbc/lib/util/container-flow.js>.
//
// To do: add `indent` support to `mdast-util-to-markdown`.
// As indents are only used for JSX, its fine for now, but perhaps better
// there.
/**
* @param {MdxJsxFlowElement} parent
* Parent of flow nodes.
* @param {State} state
* Info passed around about the current state.
* @param {ReturnType<Tracker['current']>} info
* Info on where we are in the document we are generating.
* @returns {string}
* Serialized children, joined by (blank) lines.
*/
function containerFlow(parent, state, info) {
const indexStack = state.indexStack
const children = parent.children
const tracker = state.createTracker(info)
const currentIndent = createIndent(inferDepth(state))
/** @type {Array<string>} */
const results = []
let index = -1
indexStack.push(-1)
while (++index < children.length) {
const child = children[index]
indexStack[indexStack.length - 1] = index
const childInfo = {before: '\n', after: '\n', ...tracker.current()}
const result = state.handle(child, parent, state, childInfo)
const serializedChild =
child.type === 'mdxJsxFlowElement'
? result
: state.indentLines(result, function (line, _, blank) {
return (blank ? '' : currentIndent) + line
})
results.push(tracker.move(serializedChild))
if (child.type !== 'list') {
state.bulletLastUsed = undefined
}
if (index < children.length - 1) {
results.push(tracker.move('\n\n'))
}
}
indexStack.pop()
return results.join('')
}
/**
*
* @param {State} state
* @returns {number}
*/
function inferDepth(state) {
let depth = 0
for (const x of state.stack) {
if (x === 'mdxJsxFlowElement') {
depth++
}
}
return depth
}
/**
* @param {number} depth
* @returns {string}
*/
function createIndent(depth) {
return indent.repeat(depth)
}
/**
* @type {ToMarkdownHandle}
*/
function peekElement() {
return '<'
}