| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644 |
- /**
- * @typedef {import('micromark-util-types').Extension} Extension
- * @typedef {import('micromark-util-types').Resolver} Resolver
- * @typedef {import('micromark-util-types').Tokenizer} Tokenizer
- * @typedef {import('micromark-util-types').State} State
- * @typedef {import('micromark-util-types').Token} Token
- */
- /**
- * @typedef {'left'|'center'|'right'|'none'} Align
- */
- import {ok as assert} from 'uvu/assert'
- import {factorySpace} from 'micromark-factory-space'
- import {
- markdownLineEnding,
- markdownLineEndingOrSpace,
- markdownSpace
- } from 'micromark-util-character'
- import {codes} from 'micromark-util-symbol/codes.js'
- import {constants} from 'micromark-util-symbol/constants.js'
- import {types} from 'micromark-util-symbol/types.js'
- /**
- * Syntax extension for micromark (passed in `extensions`).
- *
- * @type {Extension}
- */
- export const gfmTable = {
- flow: {null: {tokenize: tokenizeTable, resolve: resolveTable}}
- }
- const nextPrefixedOrBlank = {
- tokenize: tokenizeNextPrefixedOrBlank,
- partial: true
- }
- /** @type {Resolver} */
- // eslint-disable-next-line complexity
- function resolveTable(events, context) {
- let index = -1
- /** @type {boolean|undefined} */
- let inHead
- /** @type {boolean|undefined} */
- let inDelimiterRow
- /** @type {boolean|undefined} */
- let inRow
- /** @type {number|undefined} */
- let contentStart
- /** @type {number|undefined} */
- let contentEnd
- /** @type {number|undefined} */
- let cellStart
- /** @type {boolean|undefined} */
- let seenCellInRow
- while (++index < events.length) {
- const token = events[index][1]
- if (inRow) {
- if (token.type === 'temporaryTableCellContent') {
- contentStart = contentStart || index
- contentEnd = index
- }
- if (
- // Combine separate content parts into one.
- (token.type === 'tableCellDivider' || token.type === 'tableRow') &&
- contentEnd
- ) {
- assert(
- contentStart,
- 'expected `contentStart` to be defined if `contentEnd` is'
- )
- const content = {
- type: 'tableContent',
- start: events[contentStart][1].start,
- end: events[contentEnd][1].end
- }
- /** @type {Token} */
- const text = {
- type: types.chunkText,
- start: content.start,
- end: content.end,
- // @ts-expect-error It’s fine.
- contentType: constants.contentTypeText
- }
- assert(
- contentStart,
- 'expected `contentStart` to be defined if `contentEnd` is'
- )
- events.splice(
- contentStart,
- contentEnd - contentStart + 1,
- ['enter', content, context],
- ['enter', text, context],
- ['exit', text, context],
- ['exit', content, context]
- )
- index -= contentEnd - contentStart - 3
- contentStart = undefined
- contentEnd = undefined
- }
- }
- if (
- events[index][0] === 'exit' &&
- cellStart !== undefined &&
- cellStart + (seenCellInRow ? 0 : 1) < index &&
- (token.type === 'tableCellDivider' ||
- (token.type === 'tableRow' &&
- (cellStart + 3 < index ||
- events[cellStart][1].type !== types.whitespace)))
- ) {
- const cell = {
- type: inDelimiterRow
- ? 'tableDelimiter'
- : inHead
- ? 'tableHeader'
- : 'tableData',
- start: events[cellStart][1].start,
- end: events[index][1].end
- }
- events.splice(index + (token.type === 'tableCellDivider' ? 1 : 0), 0, [
- 'exit',
- cell,
- context
- ])
- events.splice(cellStart, 0, ['enter', cell, context])
- index += 2
- cellStart = index + 1
- seenCellInRow = true
- }
- if (token.type === 'tableRow') {
- inRow = events[index][0] === 'enter'
- if (inRow) {
- cellStart = index + 1
- seenCellInRow = false
- }
- }
- if (token.type === 'tableDelimiterRow') {
- inDelimiterRow = events[index][0] === 'enter'
- if (inDelimiterRow) {
- cellStart = index + 1
- seenCellInRow = false
- }
- }
- if (token.type === 'tableHead') {
- inHead = events[index][0] === 'enter'
- }
- }
- return events
- }
- /** @type {Tokenizer} */
- function tokenizeTable(effects, ok, nok) {
- const self = this
- /** @type {Array<Align>} */
- const align = []
- let tableHeaderCount = 0
- /** @type {boolean|undefined} */
- let seenDelimiter
- /** @type {boolean|undefined} */
- let hasDash
- return start
- /** @type {State} */
- function start(code) {
- // @ts-expect-error Custom.
- effects.enter('table')._align = align
- effects.enter('tableHead')
- effects.enter('tableRow')
- // If we start with a pipe, we open a cell marker.
- if (code === codes.verticalBar) {
- return cellDividerHead(code)
- }
- tableHeaderCount++
- effects.enter('temporaryTableCellContent')
- // Can’t be space or eols at the start of a construct, so we’re in a cell.
- assert(!markdownLineEndingOrSpace(code), 'expected non-space')
- return inCellContentHead(code)
- }
- /** @type {State} */
- function cellDividerHead(code) {
- assert(code === codes.verticalBar, 'expected `|`')
- effects.enter('tableCellDivider')
- effects.consume(code)
- effects.exit('tableCellDivider')
- seenDelimiter = true
- return cellBreakHead
- }
- /** @type {State} */
- function cellBreakHead(code) {
- if (code === codes.eof || markdownLineEnding(code)) {
- return atRowEndHead(code)
- }
- if (markdownSpace(code)) {
- effects.enter(types.whitespace)
- effects.consume(code)
- return inWhitespaceHead
- }
- if (seenDelimiter) {
- seenDelimiter = undefined
- tableHeaderCount++
- }
- if (code === codes.verticalBar) {
- return cellDividerHead(code)
- }
- // Anything else is cell content.
- effects.enter('temporaryTableCellContent')
- return inCellContentHead(code)
- }
- /** @type {State} */
- function inWhitespaceHead(code) {
- if (markdownSpace(code)) {
- effects.consume(code)
- return inWhitespaceHead
- }
- effects.exit(types.whitespace)
- return cellBreakHead(code)
- }
- /** @type {State} */
- function inCellContentHead(code) {
- // EOF, whitespace, pipe
- if (
- code === codes.eof ||
- code === codes.verticalBar ||
- markdownLineEndingOrSpace(code)
- ) {
- effects.exit('temporaryTableCellContent')
- return cellBreakHead(code)
- }
- effects.consume(code)
- return code === codes.backslash
- ? inCellContentEscapeHead
- : inCellContentHead
- }
- /** @type {State} */
- function inCellContentEscapeHead(code) {
- if (code === codes.backslash || code === codes.verticalBar) {
- effects.consume(code)
- return inCellContentHead
- }
- // Anything else.
- return inCellContentHead(code)
- }
- /** @type {State} */
- function atRowEndHead(code) {
- if (code === codes.eof) {
- return nok(code)
- }
- assert(markdownLineEnding(code), 'expected eol')
- effects.exit('tableRow')
- effects.exit('tableHead')
- const originalInterrupt = self.interrupt
- self.interrupt = true
- return effects.attempt(
- {tokenize: tokenizeRowEnd, partial: true},
- function (code) {
- self.interrupt = originalInterrupt
- effects.enter('tableDelimiterRow')
- return atDelimiterRowBreak(code)
- },
- function (code) {
- self.interrupt = originalInterrupt
- return nok(code)
- }
- )(code)
- }
- /** @type {State} */
- function atDelimiterRowBreak(code) {
- if (code === codes.eof || markdownLineEnding(code)) {
- return rowEndDelimiter(code)
- }
- if (markdownSpace(code)) {
- effects.enter(types.whitespace)
- effects.consume(code)
- return inWhitespaceDelimiter
- }
- if (code === codes.dash) {
- effects.enter('tableDelimiterFiller')
- effects.consume(code)
- hasDash = true
- align.push('none')
- return inFillerDelimiter
- }
- if (code === codes.colon) {
- effects.enter('tableDelimiterAlignment')
- effects.consume(code)
- effects.exit('tableDelimiterAlignment')
- align.push('left')
- return afterLeftAlignment
- }
- // If we start with a pipe, we open a cell marker.
- if (code === codes.verticalBar) {
- effects.enter('tableCellDivider')
- effects.consume(code)
- effects.exit('tableCellDivider')
- return atDelimiterRowBreak
- }
- return nok(code)
- }
- /** @type {State} */
- function inWhitespaceDelimiter(code) {
- if (markdownSpace(code)) {
- effects.consume(code)
- return inWhitespaceDelimiter
- }
- effects.exit(types.whitespace)
- return atDelimiterRowBreak(code)
- }
- /** @type {State} */
- function inFillerDelimiter(code) {
- if (code === codes.dash) {
- effects.consume(code)
- return inFillerDelimiter
- }
- effects.exit('tableDelimiterFiller')
- if (code === codes.colon) {
- effects.enter('tableDelimiterAlignment')
- effects.consume(code)
- effects.exit('tableDelimiterAlignment')
- align[align.length - 1] =
- align[align.length - 1] === 'left' ? 'center' : 'right'
- return afterRightAlignment
- }
- return atDelimiterRowBreak(code)
- }
- /** @type {State} */
- function afterLeftAlignment(code) {
- if (code === codes.dash) {
- effects.enter('tableDelimiterFiller')
- effects.consume(code)
- hasDash = true
- return inFillerDelimiter
- }
- // Anything else is not ok.
- return nok(code)
- }
- /** @type {State} */
- function afterRightAlignment(code) {
- if (code === codes.eof || markdownLineEnding(code)) {
- return rowEndDelimiter(code)
- }
- if (markdownSpace(code)) {
- effects.enter(types.whitespace)
- effects.consume(code)
- return inWhitespaceDelimiter
- }
- // `|`
- if (code === codes.verticalBar) {
- effects.enter('tableCellDivider')
- effects.consume(code)
- effects.exit('tableCellDivider')
- return atDelimiterRowBreak
- }
- return nok(code)
- }
- /** @type {State} */
- function rowEndDelimiter(code) {
- effects.exit('tableDelimiterRow')
- // Exit if there was no dash at all, or if the header cell count is not the
- // delimiter cell count.
- if (!hasDash || tableHeaderCount !== align.length) {
- return nok(code)
- }
- if (code === codes.eof) {
- return tableClose(code)
- }
- assert(markdownLineEnding(code), 'expected eol')
- return effects.check(
- nextPrefixedOrBlank,
- tableClose,
- effects.attempt(
- {tokenize: tokenizeRowEnd, partial: true},
- factorySpace(effects, bodyStart, types.linePrefix, constants.tabSize),
- tableClose
- )
- )(code)
- }
- /** @type {State} */
- function tableClose(code) {
- effects.exit('table')
- return ok(code)
- }
- /** @type {State} */
- function bodyStart(code) {
- effects.enter('tableBody')
- return rowStartBody(code)
- }
- /** @type {State} */
- function rowStartBody(code) {
- effects.enter('tableRow')
- // If we start with a pipe, we open a cell marker.
- if (code === codes.verticalBar) {
- return cellDividerBody(code)
- }
- effects.enter('temporaryTableCellContent')
- // Can’t be space or eols at the start of a construct, so we’re in a cell.
- return inCellContentBody(code)
- }
- /** @type {State} */
- function cellDividerBody(code) {
- assert(code === codes.verticalBar, 'expected `|`')
- effects.enter('tableCellDivider')
- effects.consume(code)
- effects.exit('tableCellDivider')
- return cellBreakBody
- }
- /** @type {State} */
- function cellBreakBody(code) {
- if (code === codes.eof || markdownLineEnding(code)) {
- return atRowEndBody(code)
- }
- if (markdownSpace(code)) {
- effects.enter(types.whitespace)
- effects.consume(code)
- return inWhitespaceBody
- }
- // `|`
- if (code === codes.verticalBar) {
- return cellDividerBody(code)
- }
- // Anything else is cell content.
- effects.enter('temporaryTableCellContent')
- return inCellContentBody(code)
- }
- /** @type {State} */
- function inWhitespaceBody(code) {
- if (markdownSpace(code)) {
- effects.consume(code)
- return inWhitespaceBody
- }
- effects.exit(types.whitespace)
- return cellBreakBody(code)
- }
- /** @type {State} */
- function inCellContentBody(code) {
- // EOF, whitespace, pipe
- if (
- code === codes.eof ||
- code === codes.verticalBar ||
- markdownLineEndingOrSpace(code)
- ) {
- effects.exit('temporaryTableCellContent')
- return cellBreakBody(code)
- }
- effects.consume(code)
- return code === codes.backslash
- ? inCellContentEscapeBody
- : inCellContentBody
- }
- /** @type {State} */
- function inCellContentEscapeBody(code) {
- if (code === codes.backslash || code === codes.verticalBar) {
- effects.consume(code)
- return inCellContentBody
- }
- // Anything else.
- return inCellContentBody(code)
- }
- /** @type {State} */
- function atRowEndBody(code) {
- effects.exit('tableRow')
- if (code === codes.eof) {
- return tableBodyClose(code)
- }
- return effects.check(
- nextPrefixedOrBlank,
- tableBodyClose,
- effects.attempt(
- {tokenize: tokenizeRowEnd, partial: true},
- factorySpace(
- effects,
- rowStartBody,
- types.linePrefix,
- constants.tabSize
- ),
- tableBodyClose
- )
- )(code)
- }
- /** @type {State} */
- function tableBodyClose(code) {
- effects.exit('tableBody')
- return tableClose(code)
- }
- /** @type {Tokenizer} */
- function tokenizeRowEnd(effects, ok, nok) {
- return start
- /** @type {State} */
- function start(code) {
- assert(markdownLineEnding(code), 'expected eol')
- effects.enter(types.lineEnding)
- effects.consume(code)
- effects.exit(types.lineEnding)
- return factorySpace(effects, prefixed, types.linePrefix)
- }
- /** @type {State} */
- function prefixed(code) {
- // Blank or interrupting line.
- if (
- self.parser.lazy[self.now().line] ||
- code === codes.eof ||
- markdownLineEnding(code)
- ) {
- return nok(code)
- }
- const tail = self.events[self.events.length - 1]
- // Indented code can interrupt delimiter and body rows.
- if (
- !self.parser.constructs.disable.null.includes('codeIndented') &&
- tail &&
- tail[1].type === types.linePrefix &&
- tail[2].sliceSerialize(tail[1], true).length >= constants.tabSize
- ) {
- return nok(code)
- }
- self._gfmTableDynamicInterruptHack = true
- return effects.check(
- self.parser.constructs.flow,
- function (code) {
- self._gfmTableDynamicInterruptHack = false
- return nok(code)
- },
- function (code) {
- self._gfmTableDynamicInterruptHack = false
- return ok(code)
- }
- )(code)
- }
- }
- }
- /** @type {Tokenizer} */
- function tokenizeNextPrefixedOrBlank(effects, ok, nok) {
- let size = 0
- return start
- /** @type {State} */
- function start(code) {
- // This is a check, so we don’t care about tokens, but we open a bogus one
- // so we’re valid.
- effects.enter('check')
- // EOL.
- effects.consume(code)
- return whitespace
- }
- /** @type {State} */
- function whitespace(code) {
- if (code === codes.virtualSpace || code === codes.space) {
- effects.consume(code)
- size++
- return size === constants.tabSize ? ok : whitespace
- }
- // EOF or whitespace
- if (code === codes.eof || markdownLineEndingOrSpace(code)) {
- return ok(code)
- }
- // Anything else.
- return nok(code)
- }
- }
|