/** * @typedef {import('micromark-util-types').Construct} Construct * @typedef {import('micromark-util-types').Event} Event * @typedef {import('micromark-util-types').Resolver} Resolver * @typedef {import('micromark-util-types').State} State * @typedef {import('micromark-util-types').Token} Token * @typedef {import('micromark-util-types').TokenizeContext} TokenizeContext * @typedef {import('micromark-util-types').Tokenizer} Tokenizer */ import {factoryDestination} from 'micromark-factory-destination' import {factoryLabel} from 'micromark-factory-label' import {factoryTitle} from 'micromark-factory-title' import {factoryWhitespace} from 'micromark-factory-whitespace' import {markdownLineEndingOrSpace} from 'micromark-util-character' import {push, splice} from 'micromark-util-chunked' import {normalizeIdentifier} from 'micromark-util-normalize-identifier' import {resolveAll} from 'micromark-util-resolve-all' /** @type {Construct} */ export const labelEnd = { name: 'labelEnd', tokenize: tokenizeLabelEnd, resolveTo: resolveToLabelEnd, resolveAll: resolveAllLabelEnd } /** @type {Construct} */ const resourceConstruct = { tokenize: tokenizeResource } /** @type {Construct} */ const referenceFullConstruct = { tokenize: tokenizeReferenceFull } /** @type {Construct} */ const referenceCollapsedConstruct = { tokenize: tokenizeReferenceCollapsed } /** @type {Resolver} */ function resolveAllLabelEnd(events) { let index = -1 while (++index < events.length) { const token = events[index][1] if ( token.type === 'labelImage' || token.type === 'labelLink' || token.type === 'labelEnd' ) { // Remove the marker. events.splice(index + 1, token.type === 'labelImage' ? 4 : 2) token.type = 'data' index++ } } return events } /** @type {Resolver} */ function resolveToLabelEnd(events, context) { let index = events.length let offset = 0 /** @type {Token} */ let token /** @type {number | undefined} */ let open /** @type {number | undefined} */ let close /** @type {Array} */ let media // Find an opening. while (index--) { token = events[index][1] if (open) { // If we see another link, or inactive link label, we’ve been here before. if ( token.type === 'link' || (token.type === 'labelLink' && token._inactive) ) { break } // Mark other link openings as inactive, as we can’t have links in // links. if (events[index][0] === 'enter' && token.type === 'labelLink') { token._inactive = true } } else if (close) { if ( events[index][0] === 'enter' && (token.type === 'labelImage' || token.type === 'labelLink') && !token._balanced ) { open = index if (token.type !== 'labelLink') { offset = 2 break } } } else if (token.type === 'labelEnd') { close = index } } const group = { type: events[open][1].type === 'labelLink' ? 'link' : 'image', start: Object.assign({}, events[open][1].start), end: Object.assign({}, events[events.length - 1][1].end) } const label = { type: 'label', start: Object.assign({}, events[open][1].start), end: Object.assign({}, events[close][1].end) } const text = { type: 'labelText', start: Object.assign({}, events[open + offset + 2][1].end), end: Object.assign({}, events[close - 2][1].start) } media = [ ['enter', group, context], ['enter', label, context] ] // Opening marker. media = push(media, events.slice(open + 1, open + offset + 3)) // Text open. media = push(media, [['enter', text, context]]) // Always populated by defaults. // Between. media = push( media, resolveAll( context.parser.constructs.insideSpan.null, events.slice(open + offset + 4, close - 3), context ) ) // Text close, marker close, label close. media = push(media, [ ['exit', text, context], events[close - 2], events[close - 1], ['exit', label, context] ]) // Reference, resource, or so. media = push(media, events.slice(close + 1)) // Media close. media = push(media, [['exit', group, context]]) splice(events, open, events.length, media) return events } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeLabelEnd(effects, ok, nok) { const self = this let index = self.events.length /** @type {Token} */ let labelStart /** @type {boolean} */ let defined // Find an opening. while (index--) { if ( (self.events[index][1].type === 'labelImage' || self.events[index][1].type === 'labelLink') && !self.events[index][1]._balanced ) { labelStart = self.events[index][1] break } } return start /** * Start of label end. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ``` * * @type {State} */ function start(code) { // If there is not an okay opening. if (!labelStart) { return nok(code) } // If the corresponding label (link) start is marked as inactive, // it means we’d be wrapping a link, like this: // // ```markdown // > | a [b [c](d) e](f) g. // ^ // ``` // // We can’t have that, so it’s just balanced brackets. if (labelStart._inactive) { return labelEndNok(code) } defined = self.parser.defined.includes( normalizeIdentifier( self.sliceSerialize({ start: labelStart.end, end: self.now() }) ) ) effects.enter('labelEnd') effects.enter('labelMarker') effects.consume(code) effects.exit('labelMarker') effects.exit('labelEnd') return after } /** * After `]`. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function after(code) { // Note: `markdown-rs` also parses GFM footnotes here, which for us is in // an extension. // Resource (`[asd](fgh)`)? if (code === 40) { return effects.attempt( resourceConstruct, labelEndOk, defined ? labelEndOk : labelEndNok )(code) } // Full (`[asd][fgh]`) or collapsed (`[asd][]`) reference? if (code === 91) { return effects.attempt( referenceFullConstruct, labelEndOk, defined ? referenceNotFull : labelEndNok )(code) } // Shortcut (`[asd]`) reference? return defined ? labelEndOk(code) : labelEndNok(code) } /** * After `]`, at `[`, but not at a full reference. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function referenceNotFull(code) { return effects.attempt( referenceCollapsedConstruct, labelEndOk, labelEndNok )(code) } /** * Done, we found something. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function labelEndOk(code) { // Note: `markdown-rs` does a bunch of stuff here. return ok(code) } /** * Done, it’s nothing. * * There was an okay opening, but we didn’t match anything. * * ```markdown * > | [a](b c * ^ * > | [a][b c * ^ * > | [a] b * ^ * ``` * * @type {State} */ function labelEndNok(code) { labelStart._balanced = true return nok(code) } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeResource(effects, ok, nok) { return resourceStart /** * At a resource. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceStart(code) { effects.enter('resource') effects.enter('resourceMarker') effects.consume(code) effects.exit('resourceMarker') return resourceBefore } /** * In resource, after `(`, at optional whitespace. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceBefore(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceOpen)(code) : resourceOpen(code) } /** * In resource, after optional whitespace, at `)` or a destination. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceOpen(code) { if (code === 41) { return resourceEnd(code) } return factoryDestination( effects, resourceDestinationAfter, resourceDestinationMissing, 'resourceDestination', 'resourceDestinationLiteral', 'resourceDestinationLiteralMarker', 'resourceDestinationRaw', 'resourceDestinationString', 32 )(code) } /** * In resource, after destination, at optional whitespace. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceDestinationAfter(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceBetween)(code) : resourceEnd(code) } /** * At invalid destination. * * ```markdown * > | [a](<<) b * ^ * ``` * * @type {State} */ function resourceDestinationMissing(code) { return nok(code) } /** * In resource, after destination and whitespace, at `(` or title. * * ```markdown * > | [a](b ) c * ^ * ``` * * @type {State} */ function resourceBetween(code) { if (code === 34 || code === 39 || code === 40) { return factoryTitle( effects, resourceTitleAfter, nok, 'resourceTitle', 'resourceTitleMarker', 'resourceTitleString' )(code) } return resourceEnd(code) } /** * In resource, after title, at optional whitespace. * * ```markdown * > | [a](b "c") d * ^ * ``` * * @type {State} */ function resourceTitleAfter(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceEnd)(code) : resourceEnd(code) } /** * In resource, at `)`. * * ```markdown * > | [a](b) d * ^ * ``` * * @type {State} */ function resourceEnd(code) { if (code === 41) { effects.enter('resourceMarker') effects.consume(code) effects.exit('resourceMarker') effects.exit('resource') return ok } return nok(code) } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeReferenceFull(effects, ok, nok) { const self = this return referenceFull /** * In a reference (full), at the `[`. * * ```markdown * > | [a][b] d * ^ * ``` * * @type {State} */ function referenceFull(code) { return factoryLabel.call( self, effects, referenceFullAfter, referenceFullMissing, 'reference', 'referenceMarker', 'referenceString' )(code) } /** * In a reference (full), after `]`. * * ```markdown * > | [a][b] d * ^ * ``` * * @type {State} */ function referenceFullAfter(code) { return self.parser.defined.includes( normalizeIdentifier( self.sliceSerialize(self.events[self.events.length - 1][1]).slice(1, -1) ) ) ? ok(code) : nok(code) } /** * In reference (full) that was missing. * * ```markdown * > | [a][b d * ^ * ``` * * @type {State} */ function referenceFullMissing(code) { return nok(code) } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeReferenceCollapsed(effects, ok, nok) { return referenceCollapsedStart /** * In reference (collapsed), at `[`. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] d * ^ * ``` * * @type {State} */ function referenceCollapsedStart(code) { // We only attempt a collapsed label if there’s a `[`. effects.enter('reference') effects.enter('referenceMarker') effects.consume(code) effects.exit('referenceMarker') return referenceCollapsedOpen } /** * In reference (collapsed), at `]`. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] d * ^ * ``` * * @type {State} */ function referenceCollapsedOpen(code) { if (code === 93) { effects.enter('referenceMarker') effects.consume(code) effects.exit('referenceMarker') effects.exit('reference') return ok } return nok(code) } }