| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682 |
- /**
- * @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<Align>} */
- 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);
- }
- }
|