site/node_modules/mdast-util-mdx-jsx/lib/index.js
2024-10-14 08:09:33 +02:00

767 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* @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 '<'
}