618 lines
13 KiB
JavaScript
618 lines
13 KiB
JavaScript
|
/**
|
|||
|
* @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<Event>} */
|
|||
|
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)
|
|||
|
}
|
|||
|
}
|