site/node_modules/mdast-util-from-markdown/lib/index.js

1353 lines
33 KiB
JavaScript
Raw Permalink Normal View History

2024-10-14 06:09:33 +00:00
/**
* @typedef {import('mdast').Break} Break
* @typedef {import('mdast').Blockquote} Blockquote
* @typedef {import('mdast').Code} Code
* @typedef {import('mdast').Definition} Definition
* @typedef {import('mdast').Emphasis} Emphasis
* @typedef {import('mdast').Heading} Heading
* @typedef {import('mdast').Html} Html
* @typedef {import('mdast').Image} Image
* @typedef {import('mdast').InlineCode} InlineCode
* @typedef {import('mdast').Link} Link
* @typedef {import('mdast').List} List
* @typedef {import('mdast').ListItem} ListItem
* @typedef {import('mdast').Nodes} Nodes
* @typedef {import('mdast').Paragraph} Paragraph
* @typedef {import('mdast').Parent} Parent
* @typedef {import('mdast').PhrasingContent} PhrasingContent
* @typedef {import('mdast').ReferenceType} ReferenceType
* @typedef {import('mdast').Root} Root
* @typedef {import('mdast').Strong} Strong
* @typedef {import('mdast').Text} Text
* @typedef {import('mdast').ThematicBreak} ThematicBreak
*
* @typedef {import('micromark-util-types').Encoding} Encoding
* @typedef {import('micromark-util-types').Event} Event
* @typedef {import('micromark-util-types').ParseOptions} ParseOptions
* @typedef {import('micromark-util-types').Token} Token
* @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext
* @typedef {import('micromark-util-types').Value} Value
*
* @typedef {import('unist').Point} Point
*
* @typedef {import('../index.js').CompileData} CompileData
*/
/**
* @typedef {Omit<Parent, 'children' | 'type'> & {type: 'fragment', children: Array<PhrasingContent>}} Fragment
*/
/**
* @callback Transform
* Extra transform, to change the AST afterwards.
* @param {Root} tree
* Tree to transform.
* @returns {Root | null | undefined | void}
* New tree or nothing (in which case the current tree is used).
*
* @callback Handle
* Handle a token.
* @param {CompileContext} this
* Context.
* @param {Token} token
* Current token.
* @returns {undefined | void}
* Nothing.
*
* @typedef {Record<string, Handle>} Handles
* Token types mapping to handles
*
* @callback OnEnterError
* Handle the case where the `right` token is open, but it is closed (by the
* `left` token) or because we reached the end of the document.
* @param {Omit<CompileContext, 'sliceSerialize'>} this
* Context.
* @param {Token | undefined} left
* Left token.
* @param {Token} right
* Right token.
* @returns {undefined}
* Nothing.
*
* @callback OnExitError
* Handle the case where the `right` token is open but it is closed by
* exiting the `left` token.
* @param {Omit<CompileContext, 'sliceSerialize'>} this
* Context.
* @param {Token} left
* Left token.
* @param {Token} right
* Right token.
* @returns {undefined}
* Nothing.
*
* @typedef {[Token, OnEnterError | undefined]} TokenTuple
* Open token on the stack, with an optional error handler for when
* that token isnt closed properly.
*/
/**
* @typedef Config
* Configuration.
*
* We have our defaults, but extensions will add more.
* @property {Array<string>} canContainEols
* Token types where line endings are used.
* @property {Handles} enter
* Opening handles.
* @property {Handles} exit
* Closing handles.
* @property {Array<Transform>} transforms
* Tree transforms.
*
* @typedef {Partial<Config>} Extension
* Change how markdown tokens from micromark are turned into mdast.
*
* @typedef CompileContext
* mdast compiler context.
* @property {Array<Fragment | Nodes>} stack
* Stack of nodes.
* @property {Array<TokenTuple>} tokenStack
* Stack of tokens.
* @property {(this: CompileContext) => undefined} buffer
* Capture some of the output data.
* @property {(this: CompileContext) => string} resume
* Stop capturing and access the output data.
* @property {(this: CompileContext, node: Nodes, token: Token, onError?: OnEnterError) => undefined} enter
* Enter a node.
* @property {(this: CompileContext, token: Token, onError?: OnExitError) => undefined} exit
* Exit a node.
* @property {TokenizeContext['sliceSerialize']} sliceSerialize
* Get the string value of a token.
* @property {Config} config
* Configuration.
* @property {CompileData} data
* Info passed around; key/value store.
*
* @typedef FromMarkdownOptions
* Configuration for how to build mdast.
* @property {Array<Extension | Array<Extension>> | null | undefined} [mdastExtensions]
* Extensions for this utility to change how tokens are turned into a tree.
*
* @typedef {ParseOptions & FromMarkdownOptions} Options
* Configuration.
*/
import {toString} from 'mdast-util-to-string'
import {parse, postprocess, preprocess} from 'micromark'
import {decodeNumericCharacterReference} from 'micromark-util-decode-numeric-character-reference'
import {decodeString} from 'micromark-util-decode-string'
import {normalizeIdentifier} from 'micromark-util-normalize-identifier'
import {decodeNamedCharacterReference} from 'decode-named-character-reference'
import {stringifyPosition} from 'unist-util-stringify-position'
const own = {}.hasOwnProperty
/**
* Turn markdown into a syntax tree.
*
* @overload
* @param {Value} value
* @param {Encoding | null | undefined} [encoding]
* @param {Options | null | undefined} [options]
* @returns {Root}
*
* @overload
* @param {Value} value
* @param {Options | null | undefined} [options]
* @returns {Root}
*
* @param {Value} value
* Markdown to parse.
* @param {Encoding | Options | null | undefined} [encoding]
* Character encoding for when `value` is `Buffer`.
* @param {Options | null | undefined} [options]
* Configuration.
* @returns {Root}
* mdast tree.
*/
export function fromMarkdown(value, encoding, options) {
if (typeof encoding !== 'string') {
options = encoding
encoding = undefined
}
return compiler(options)(
postprocess(
parse(options).document().write(preprocess()(value, encoding, true))
)
)
}
/**
* Note this compiler only understand complete buffering, not streaming.
*
* @param {Options | null | undefined} [options]
*/
function compiler(options) {
/** @type {Config} */
const config = {
transforms: [],
canContainEols: ['emphasis', 'fragment', 'heading', 'paragraph', 'strong'],
enter: {
autolink: opener(link),
autolinkProtocol: onenterdata,
autolinkEmail: onenterdata,
atxHeading: opener(heading),
blockQuote: opener(blockQuote),
characterEscape: onenterdata,
characterReference: onenterdata,
codeFenced: opener(codeFlow),
codeFencedFenceInfo: buffer,
codeFencedFenceMeta: buffer,
codeIndented: opener(codeFlow, buffer),
codeText: opener(codeText, buffer),
codeTextData: onenterdata,
data: onenterdata,
codeFlowValue: onenterdata,
definition: opener(definition),
definitionDestinationString: buffer,
definitionLabelString: buffer,
definitionTitleString: buffer,
emphasis: opener(emphasis),
hardBreakEscape: opener(hardBreak),
hardBreakTrailing: opener(hardBreak),
htmlFlow: opener(html, buffer),
htmlFlowData: onenterdata,
htmlText: opener(html, buffer),
htmlTextData: onenterdata,
image: opener(image),
label: buffer,
link: opener(link),
listItem: opener(listItem),
listItemValue: onenterlistitemvalue,
listOrdered: opener(list, onenterlistordered),
listUnordered: opener(list),
paragraph: opener(paragraph),
reference: onenterreference,
referenceString: buffer,
resourceDestinationString: buffer,
resourceTitleString: buffer,
setextHeading: opener(heading),
strong: opener(strong),
thematicBreak: opener(thematicBreak)
},
exit: {
atxHeading: closer(),
atxHeadingSequence: onexitatxheadingsequence,
autolink: closer(),
autolinkEmail: onexitautolinkemail,
autolinkProtocol: onexitautolinkprotocol,
blockQuote: closer(),
characterEscapeValue: onexitdata,
characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker,
characterReferenceMarkerNumeric: onexitcharacterreferencemarker,
characterReferenceValue: onexitcharacterreferencevalue,
codeFenced: closer(onexitcodefenced),
codeFencedFence: onexitcodefencedfence,
codeFencedFenceInfo: onexitcodefencedfenceinfo,
codeFencedFenceMeta: onexitcodefencedfencemeta,
codeFlowValue: onexitdata,
codeIndented: closer(onexitcodeindented),
codeText: closer(onexitcodetext),
codeTextData: onexitdata,
data: onexitdata,
definition: closer(),
definitionDestinationString: onexitdefinitiondestinationstring,
definitionLabelString: onexitdefinitionlabelstring,
definitionTitleString: onexitdefinitiontitlestring,
emphasis: closer(),
hardBreakEscape: closer(onexithardbreak),
hardBreakTrailing: closer(onexithardbreak),
htmlFlow: closer(onexithtmlflow),
htmlFlowData: onexitdata,
htmlText: closer(onexithtmltext),
htmlTextData: onexitdata,
image: closer(onexitimage),
label: onexitlabel,
labelText: onexitlabeltext,
lineEnding: onexitlineending,
link: closer(onexitlink),
listItem: closer(),
listOrdered: closer(),
listUnordered: closer(),
paragraph: closer(),
referenceString: onexitreferencestring,
resourceDestinationString: onexitresourcedestinationstring,
resourceTitleString: onexitresourcetitlestring,
resource: onexitresource,
setextHeading: closer(onexitsetextheading),
setextHeadingLineSequence: onexitsetextheadinglinesequence,
setextHeadingText: onexitsetextheadingtext,
strong: closer(),
thematicBreak: closer()
}
}
configure(config, (options || {}).mdastExtensions || [])
/** @type {CompileData} */
const data = {}
return compile
/**
* Turn micromark events into an mdast tree.
*
* @param {Array<Event>} events
* Events.
* @returns {Root}
* mdast tree.
*/
function compile(events) {
/** @type {Root} */
let tree = {
type: 'root',
children: []
}
/** @type {Omit<CompileContext, 'sliceSerialize'>} */
const context = {
stack: [tree],
tokenStack: [],
config,
enter,
exit,
buffer,
resume,
data
}
/** @type {Array<number>} */
const listStack = []
let index = -1
while (++index < events.length) {
// We preprocess lists to add `listItem` tokens, and to infer whether
// items the list itself are spread out.
if (
events[index][1].type === 'listOrdered' ||
events[index][1].type === 'listUnordered'
) {
if (events[index][0] === 'enter') {
listStack.push(index)
} else {
const tail = listStack.pop()
index = prepareList(events, tail, index)
}
}
}
index = -1
while (++index < events.length) {
const handler = config[events[index][0]]
if (own.call(handler, events[index][1].type)) {
handler[events[index][1].type].call(
Object.assign(
{
sliceSerialize: events[index][2].sliceSerialize
},
context
),
events[index][1]
)
}
}
// Handle tokens still being open.
if (context.tokenStack.length > 0) {
const tail = context.tokenStack[context.tokenStack.length - 1]
const handler = tail[1] || defaultOnError
handler.call(context, undefined, tail[0])
}
// Figure out `root` position.
tree.position = {
start: point(
events.length > 0
? events[0][1].start
: {
line: 1,
column: 1,
offset: 0
}
),
end: point(
events.length > 0
? events[events.length - 2][1].end
: {
line: 1,
column: 1,
offset: 0
}
)
}
// Call transforms.
index = -1
while (++index < config.transforms.length) {
tree = config.transforms[index](tree) || tree
}
return tree
}
/**
* @param {Array<Event>} events
* @param {number} start
* @param {number} length
* @returns {number}
*/
function prepareList(events, start, length) {
let index = start - 1
let containerBalance = -1
let listSpread = false
/** @type {Token | undefined} */
let listItem
/** @type {number | undefined} */
let lineIndex
/** @type {number | undefined} */
let firstBlankLineIndex
/** @type {boolean | undefined} */
let atMarker
while (++index <= length) {
const event = events[index]
switch (event[1].type) {
case 'listUnordered':
case 'listOrdered':
case 'blockQuote': {
if (event[0] === 'enter') {
containerBalance++
} else {
containerBalance--
}
atMarker = undefined
break
}
case 'lineEndingBlank': {
if (event[0] === 'enter') {
if (
listItem &&
!atMarker &&
!containerBalance &&
!firstBlankLineIndex
) {
firstBlankLineIndex = index
}
atMarker = undefined
}
break
}
case 'linePrefix':
case 'listItemValue':
case 'listItemMarker':
case 'listItemPrefix':
case 'listItemPrefixWhitespace': {
// Empty.
break
}
default: {
atMarker = undefined
}
}
if (
(!containerBalance &&
event[0] === 'enter' &&
event[1].type === 'listItemPrefix') ||
(containerBalance === -1 &&
event[0] === 'exit' &&
(event[1].type === 'listUnordered' ||
event[1].type === 'listOrdered'))
) {
if (listItem) {
let tailIndex = index
lineIndex = undefined
while (tailIndex--) {
const tailEvent = events[tailIndex]
if (
tailEvent[1].type === 'lineEnding' ||
tailEvent[1].type === 'lineEndingBlank'
) {
if (tailEvent[0] === 'exit') continue
if (lineIndex) {
events[lineIndex][1].type = 'lineEndingBlank'
listSpread = true
}
tailEvent[1].type = 'lineEnding'
lineIndex = tailIndex
} else if (
tailEvent[1].type === 'linePrefix' ||
tailEvent[1].type === 'blockQuotePrefix' ||
tailEvent[1].type === 'blockQuotePrefixWhitespace' ||
tailEvent[1].type === 'blockQuoteMarker' ||
tailEvent[1].type === 'listItemIndent'
) {
// Empty
} else {
break
}
}
if (
firstBlankLineIndex &&
(!lineIndex || firstBlankLineIndex < lineIndex)
) {
listItem._spread = true
}
// Fix position.
listItem.end = Object.assign(
{},
lineIndex ? events[lineIndex][1].start : event[1].end
)
events.splice(lineIndex || index, 0, ['exit', listItem, event[2]])
index++
length++
}
// Create a new list item.
if (event[1].type === 'listItemPrefix') {
/** @type {Token} */
const item = {
type: 'listItem',
_spread: false,
start: Object.assign({}, event[1].start),
// @ts-expect-error: well add `end` in a second.
end: undefined
}
listItem = item
events.splice(index, 0, ['enter', item, event[2]])
index++
length++
firstBlankLineIndex = undefined
atMarker = true
}
}
}
events[start][1]._spread = listSpread
return length
}
/**
* Create an opener handle.
*
* @param {(token: Token) => Nodes} create
* Create a node.
* @param {Handle | undefined} [and]
* Optional function to also run.
* @returns {Handle}
* Handle.
*/
function opener(create, and) {
return open
/**
* @this {CompileContext}
* @param {Token} token
* @returns {undefined}
*/
function open(token) {
enter.call(this, create(token), token)
if (and) and.call(this, token)
}
}
/**
* @this {CompileContext}
* @returns {undefined}
*/
function buffer() {
this.stack.push({
type: 'fragment',
children: []
})
}
/**
* @this {CompileContext}
* Context.
* @param {Nodes} node
* Node to enter.
* @param {Token} token
* Corresponding token.
* @param {OnEnterError | undefined} [errorHandler]
* Handle the case where this token is open, but it is closed by something else.
* @returns {undefined}
* Nothing.
*/
function enter(node, token, errorHandler) {
const parent = this.stack[this.stack.length - 1]
/** @type {Array<Nodes>} */
const siblings = parent.children
siblings.push(node)
this.stack.push(node)
this.tokenStack.push([token, errorHandler])
node.position = {
start: point(token.start),
// @ts-expect-error: `end` will be patched later.
end: undefined
}
}
/**
* Create a closer handle.
*
* @param {Handle | undefined} [and]
* Optional function to also run.
* @returns {Handle}
* Handle.
*/
function closer(and) {
return close
/**
* @this {CompileContext}
* @param {Token} token
* @returns {undefined}
*/
function close(token) {
if (and) and.call(this, token)
exit.call(this, token)
}
}
/**
* @this {CompileContext}
* Context.
* @param {Token} token
* Corresponding token.
* @param {OnExitError | undefined} [onExitError]
* Handle the case where another token is open.
* @returns {undefined}
* Nothing.
*/
function exit(token, onExitError) {
const node = this.stack.pop()
const open = this.tokenStack.pop()
if (!open) {
throw new Error(
'Cannot close `' +
token.type +
'` (' +
stringifyPosition({
start: token.start,
end: token.end
}) +
'): its not open'
)
} else if (open[0].type !== token.type) {
if (onExitError) {
onExitError.call(this, token, open[0])
} else {
const handler = open[1] || defaultOnError
handler.call(this, token, open[0])
}
}
node.position.end = point(token.end)
}
/**
* @this {CompileContext}
* @returns {string}
*/
function resume() {
return toString(this.stack.pop())
}
//
// Handlers.
//
/**
* @this {CompileContext}
* @type {Handle}
*/
function onenterlistordered() {
this.data.expectingFirstListItemValue = true
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onenterlistitemvalue(token) {
if (this.data.expectingFirstListItemValue) {
const ancestor = this.stack[this.stack.length - 2]
ancestor.start = Number.parseInt(this.sliceSerialize(token), 10)
this.data.expectingFirstListItemValue = undefined
}
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodefencedfenceinfo() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.lang = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodefencedfencemeta() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.meta = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodefencedfence() {
// Exit if this is the closing fence.
if (this.data.flowCodeInside) return
this.buffer()
this.data.flowCodeInside = true
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodefenced() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.value = data.replace(/^(\r?\n|\r)|(\r?\n|\r)$/g, '')
this.data.flowCodeInside = undefined
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodeindented() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.value = data.replace(/(\r?\n|\r)$/g, '')
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitdefinitionlabelstring(token) {
const label = this.resume()
const node = this.stack[this.stack.length - 1]
node.label = label
node.identifier = normalizeIdentifier(
this.sliceSerialize(token)
).toLowerCase()
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitdefinitiontitlestring() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.title = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitdefinitiondestinationstring() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.url = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitatxheadingsequence(token) {
const node = this.stack[this.stack.length - 1]
if (!node.depth) {
const depth = this.sliceSerialize(token).length
node.depth = depth
}
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitsetextheadingtext() {
this.data.setextHeadingSlurpLineEnding = true
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitsetextheadinglinesequence(token) {
const node = this.stack[this.stack.length - 1]
node.depth = this.sliceSerialize(token).codePointAt(0) === 61 ? 1 : 2
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitsetextheading() {
this.data.setextHeadingSlurpLineEnding = undefined
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onenterdata(token) {
const node = this.stack[this.stack.length - 1]
/** @type {Array<Nodes>} */
const siblings = node.children
let tail = siblings[siblings.length - 1]
if (!tail || tail.type !== 'text') {
// Add a new text node.
tail = text()
tail.position = {
start: point(token.start),
// @ts-expect-error: well add `end` later.
end: undefined
}
siblings.push(tail)
}
this.stack.push(tail)
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitdata(token) {
const tail = this.stack.pop()
tail.value += this.sliceSerialize(token)
tail.position.end = point(token.end)
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitlineending(token) {
const context = this.stack[this.stack.length - 1]
// If were at a hard break, include the line ending in there.
if (this.data.atHardBreak) {
const tail = context.children[context.children.length - 1]
tail.position.end = point(token.end)
this.data.atHardBreak = undefined
return
}
if (
!this.data.setextHeadingSlurpLineEnding &&
config.canContainEols.includes(context.type)
) {
onenterdata.call(this, token)
onexitdata.call(this, token)
}
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexithardbreak() {
this.data.atHardBreak = true
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexithtmlflow() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.value = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexithtmltext() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.value = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcodetext() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.value = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitlink() {
const node = this.stack[this.stack.length - 1]
// Note: there are also `identifier` and `label` fields on this link node!
// These are used / cleaned here.
// To do: clean.
if (this.data.inReference) {
/** @type {ReferenceType} */
const referenceType = this.data.referenceType || 'shortcut'
node.type += 'Reference'
// @ts-expect-error: mutate.
node.referenceType = referenceType
// @ts-expect-error: mutate.
delete node.url
delete node.title
} else {
// @ts-expect-error: mutate.
delete node.identifier
// @ts-expect-error: mutate.
delete node.label
}
this.data.referenceType = undefined
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitimage() {
const node = this.stack[this.stack.length - 1]
// Note: there are also `identifier` and `label` fields on this link node!
// These are used / cleaned here.
// To do: clean.
if (this.data.inReference) {
/** @type {ReferenceType} */
const referenceType = this.data.referenceType || 'shortcut'
node.type += 'Reference'
// @ts-expect-error: mutate.
node.referenceType = referenceType
// @ts-expect-error: mutate.
delete node.url
delete node.title
} else {
// @ts-expect-error: mutate.
delete node.identifier
// @ts-expect-error: mutate.
delete node.label
}
this.data.referenceType = undefined
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitlabeltext(token) {
const string = this.sliceSerialize(token)
const ancestor = this.stack[this.stack.length - 2]
// @ts-expect-error: stash this on the node, as it might become a reference
// later.
ancestor.label = decodeString(string)
// @ts-expect-error: same as above.
ancestor.identifier = normalizeIdentifier(string).toLowerCase()
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitlabel() {
const fragment = this.stack[this.stack.length - 1]
const value = this.resume()
const node = this.stack[this.stack.length - 1]
// Assume a reference.
this.data.inReference = true
if (node.type === 'link') {
/** @type {Array<PhrasingContent>} */
const children = fragment.children
node.children = children
} else {
node.alt = value
}
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitresourcedestinationstring() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.url = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitresourcetitlestring() {
const data = this.resume()
const node = this.stack[this.stack.length - 1]
node.title = data
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitresource() {
this.data.inReference = undefined
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onenterreference() {
this.data.referenceType = 'collapsed'
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitreferencestring(token) {
const label = this.resume()
const node = this.stack[this.stack.length - 1]
// @ts-expect-error: stash this on the node, as it might become a reference
// later.
node.label = label
// @ts-expect-error: same as above.
node.identifier = normalizeIdentifier(
this.sliceSerialize(token)
).toLowerCase()
this.data.referenceType = 'full'
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcharacterreferencemarker(token) {
this.data.characterReferenceType = token.type
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitcharacterreferencevalue(token) {
const data = this.sliceSerialize(token)
const type = this.data.characterReferenceType
/** @type {string} */
let value
if (type) {
value = decodeNumericCharacterReference(
data,
type === 'characterReferenceMarkerNumeric' ? 10 : 16
)
this.data.characterReferenceType = undefined
} else {
const result = decodeNamedCharacterReference(data)
value = result
}
const tail = this.stack.pop()
tail.value += value
tail.position.end = point(token.end)
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitautolinkprotocol(token) {
onexitdata.call(this, token)
const node = this.stack[this.stack.length - 1]
node.url = this.sliceSerialize(token)
}
/**
* @this {CompileContext}
* @type {Handle}
*/
function onexitautolinkemail(token) {
onexitdata.call(this, token)
const node = this.stack[this.stack.length - 1]
node.url = 'mailto:' + this.sliceSerialize(token)
}
//
// Creaters.
//
/** @returns {Blockquote} */
function blockQuote() {
return {
type: 'blockquote',
children: []
}
}
/** @returns {Code} */
function codeFlow() {
return {
type: 'code',
lang: null,
meta: null,
value: ''
}
}
/** @returns {InlineCode} */
function codeText() {
return {
type: 'inlineCode',
value: ''
}
}
/** @returns {Definition} */
function definition() {
return {
type: 'definition',
identifier: '',
label: null,
title: null,
url: ''
}
}
/** @returns {Emphasis} */
function emphasis() {
return {
type: 'emphasis',
children: []
}
}
/** @returns {Heading} */
function heading() {
return {
type: 'heading',
// @ts-expect-error `depth` will be set later.
depth: 0,
children: []
}
}
/** @returns {Break} */
function hardBreak() {
return {
type: 'break'
}
}
/** @returns {Html} */
function html() {
return {
type: 'html',
value: ''
}
}
/** @returns {Image} */
function image() {
return {
type: 'image',
title: null,
url: '',
alt: null
}
}
/** @returns {Link} */
function link() {
return {
type: 'link',
title: null,
url: '',
children: []
}
}
/**
* @param {Token} token
* @returns {List}
*/
function list(token) {
return {
type: 'list',
ordered: token.type === 'listOrdered',
start: null,
spread: token._spread,
children: []
}
}
/**
* @param {Token} token
* @returns {ListItem}
*/
function listItem(token) {
return {
type: 'listItem',
spread: token._spread,
checked: null,
children: []
}
}
/** @returns {Paragraph} */
function paragraph() {
return {
type: 'paragraph',
children: []
}
}
/** @returns {Strong} */
function strong() {
return {
type: 'strong',
children: []
}
}
/** @returns {Text} */
function text() {
return {
type: 'text',
value: ''
}
}
/** @returns {ThematicBreak} */
function thematicBreak() {
return {
type: 'thematicBreak'
}
}
}
/**
* Copy a point-like value.
*
* @param {Point} d
* Point-like value.
* @returns {Point}
* unist point.
*/
function point(d) {
return {
line: d.line,
column: d.column,
offset: d.offset
}
}
/**
* @param {Config} combined
* @param {Array<Array<Extension> | Extension>} extensions
* @returns {undefined}
*/
function configure(combined, extensions) {
let index = -1
while (++index < extensions.length) {
const value = extensions[index]
if (Array.isArray(value)) {
configure(combined, value)
} else {
extension(combined, value)
}
}
}
/**
* @param {Config} combined
* @param {Extension} extension
* @returns {undefined}
*/
function extension(combined, extension) {
/** @type {keyof Extension} */
let key
for (key in extension) {
if (own.call(extension, key)) {
switch (key) {
case 'canContainEols': {
const right = extension[key]
if (right) {
combined[key].push(...right)
}
break
}
case 'transforms': {
const right = extension[key]
if (right) {
combined[key].push(...right)
}
break
}
case 'enter':
case 'exit': {
const right = extension[key]
if (right) {
Object.assign(combined[key], right)
}
break
}
// No default
}
}
}
}
/** @type {OnEnterError} */
function defaultOnError(left, right) {
if (left) {
throw new Error(
'Cannot close `' +
left.type +
'` (' +
stringifyPosition({
start: left.start,
end: left.end
}) +
'): a different token (`' +
right.type +
'`, ' +
stringifyPosition({
start: right.start,
end: right.end
}) +
') is open'
)
} else {
throw new Error(
'Cannot close document, a token (`' +
right.type +
'`, ' +
stringifyPosition({
start: right.start,
end: right.end
}) +
') is still open'
)
}
}