767 lines
22 KiB
JavaScript
767 lines
22 KiB
JavaScript
/**
|
||
* @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 doesn’t 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 don’t 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 can’t 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, it’s 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 '<'
|
||
}
|