/** * @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 { 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'; import { ok as assert } from 'uvu/assert'; /** * 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 = { // eslint-disable-next-line no-nested-ternary 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) { // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; /** @type {Array} */ const align = []; let tableHeaderCount = 0; /** @type {boolean|undefined} */ let seenDelimiter; /** @type {boolean|undefined} */ let hasDash; return start; /** @type {State} */ function start(code) { const { containerState } = self; const prevRowCount = containerState.rowCount ?? 0; const hasDelimiterRow = containerState.hasDelimiterRow ?? false; // @ts-expect-error Custom. effects.enter('table')._align = align; effects.enter('tableHead'); effects.enter('tableRow'); // increment row count const crrRowCount = prevRowCount + 1; containerState.rowCount = crrRowCount; // Max 2 rows processing before delimiter row if (hasDelimiterRow || crrRowCount > 2) { return nok(code); } // 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) { // for debug -- 2023.05.06 Yuki Takei // const { containerState } = self; // let atRowEndHeadCount = containerState.atRowEndHeadCount ?? 0; // atRowEndHeadCount++; // containerState.atRowEndHeadCount = atRowEndHeadCount; // console.log({ atRowEndHeadCount }); if (code === codes.eof) { return tableExit(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 }, (code) => { self.interrupt = originalInterrupt; effects.enter('tableDelimiterRow'); return atDelimiterRowBreak(code); }, (code) => { self.interrupt = originalInterrupt; return tableExit(code); }, )(code); } /** @type {State} */ function atDelimiterRowBreak(code) { // persist that the table has a delimiter row self.containerState.hasDelimiterRow = true; 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 tableExit(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 tableExit(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 tableExit(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 tableExit(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 tableExit(code) { // delete persisted states delete self.containerState.rowCount; delete self.containerState.hasDelimiterRow; return nok(code); } /** @type {State} */ function tableClose(code) { effects.exit('table'); // delete persisted states delete self.containerState.rowCount; delete self.containerState.hasDelimiterRow; 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, (code) => { self._gfmTableDynamicInterruptHack = false; return nok(code); }, (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); } }