Socket
Socket
Sign inDemoInstall

micromark-extension-gfm-table

Package Overview
Dependencies
Maintainers
1
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

micromark-extension-gfm-table - npm Package Compare versions

Comparing version 1.0.5 to 1.0.6

dev/lib/edit-map.d.ts

9

dev/lib/html.d.ts

@@ -1,4 +0,9 @@

/** @type {HtmlExtension} */
/**
* Extension for `micromark` that can be passed in `htmlExtensions` to support
* GFM tables when serializing to HTML.
*
* @type {HtmlExtension}
*/
export const gfmTableHtml: HtmlExtension
export type HtmlExtension = import('micromark-util-types').HtmlExtension
export type Align = import('./syntax.js').Align
export type Align = import('./infer.js').Align
/**
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
* @typedef {import('./syntax.js').Align} Align
*/
/**
* @typedef {import('./infer.js').Align} Align
*/
const alignment = {

@@ -13,3 +16,11 @@ none: '',

/** @type {HtmlExtension} */
// To do: next major: expose functions.
// To do: next major: use `infer` here, when all events are exposed.
/**
* Extension for `micromark` that can be passed in `htmlExtensions` to support
* GFM tables when serializing to HTML.
*
* @type {HtmlExtension}
*/
export const gfmTableHtml = {

@@ -26,4 +37,2 @@ enter: {

tableBody() {
// Clear slurping line ending from the delimiter row.
this.setData('slurpOneLineEnding')
this.tag('<tbody>')

@@ -80,4 +89,5 @@ },

this.setData('tableAlign')
// If there was no table body, make sure the slurping from the delimiter row
// is cleared.
// Note: we don’t set `slurpAllLineEndings` anymore, in delimiter rows,
// but we do need to reset it to match a funky newline GH generates for
// list items combined with tables.
this.setData('slurpAllLineEndings')

@@ -108,4 +118,2 @@ this.lineEndingIfNeeded()

this.tag('</thead>')
this.setData('slurpOneLineEnding', true)
// Slurp the line ending from the delimiter row.
},

@@ -112,0 +120,0 @@ tableHeader() {

@@ -1,8 +0,23 @@

/** @type {Extension} */
/**
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*
* @type {Extension}
*/
export const gfmTable: Extension
export type Event = import('micromark-util-types').Event
export type Extension = import('micromark-util-types').Extension
export type Point = import('micromark-util-types').Point
export type Resolver = import('micromark-util-types').Resolver
export type Tokenizer = import('micromark-util-types').Tokenizer
export type State = import('micromark-util-types').State
export type Token = import('micromark-util-types').Token
export type Align = 'left' | 'center' | 'right' | 'none'
export type TokenizeContext = import('micromark-util-types').TokenizeContext
export type Tokenizer = import('micromark-util-types').Tokenizer
/**
* Cell info.
*/
export type Range = [number, number, number, number]
/**
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/
export type RowKind = 0 | 1 | 2 | 3
/**
* @typedef {import('micromark-util-types').Event} Event
* @typedef {import('micromark-util-types').Extension} Extension
* @typedef {import('micromark-util-types').Point} Point
* @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 {import('micromark-util-types').TokenizeContext} TokenizeContext
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer
*/
/**
* @typedef {'left'|'center'|'right'|'none'} Align
* @typedef {[number, number, number, number]} Range
* Cell info.
*
* @typedef {0 | 1 | 2 | 3} RowKind
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/

@@ -23,220 +30,204 @@

import {types} from 'micromark-util-symbol/types.js'
import {EditMap} from './edit-map.js'
import {gfmTableAlign} from './infer.js'
/** @type {Extension} */
// To do: next major: expose functions.
/**
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*
* @type {Extension}
*/
export const gfmTable = {
flow: {null: {tokenize: tokenizeTable, resolve: resolveTable}}
flow: {null: {tokenize: tokenizeTable, resolveAll: resolveTable}}
}
const nextPrefixedOrBlank = {
tokenize: tokenizeNextPrefixedOrBlank,
partial: true
}
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeTable(effects, ok, nok) {
const self = this
let size = 0
let sizeB = 0
/** @type {boolean | undefined} */
let seen
/** @type {Resolver} */
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
return start
while (++index < events.length) {
const token = events[index][1]
/**
* Start of a GFM table.
*
* If there is a valid table row or table head before, then we try to parse
* another row.
* Otherwise, we try to parse a head.
*
* ```markdown
* > | | a |
* ^
* | | - |
* > | | b |
* ^
* ```
* @type {State}
*/
function start(code) {
let index = self.events.length - 1
if (inRow) {
if (token.type === 'temporaryTableCellContent') {
contentStart = contentStart || index
contentEnd = index
}
while (index > -1) {
const type = self.events[index][1].type
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
}
type === types.lineEnding ||
// Note: markdown-rs uses `whitespace` instead of `linePrefix`
type === types.linePrefix
)
index--
else break
}
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
}
const tail = index > -1 ? self.events[index][1].type : null
if (token.type === 'tableRow') {
inRow = events[index][0] === 'enter'
const next =
tail === 'tableHead' || tail === 'tableRow' ? bodyRowStart : headRowBefore
if (inRow) {
cellStart = index + 1
seenCellInRow = false
}
// Don’t allow lazy body rows.
if (next === bodyRowStart && self.parser.lazy[self.now().line]) {
return nok(code)
}
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 next(code)
}
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
/**
* Before table head row.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBefore(code) {
effects.enter('tableHead')
effects.enter('tableRow')
return headRowStart(code)
}
// If we start with a pipe, we open a cell marker.
/**
* Before table head row, after whitespace.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowStart(code) {
if (code === codes.verticalBar) {
return cellDividerHead(code)
return headRowBreak(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)
}
// To do: micromark-js should let us parse our own whitespace in extensions,
// like `markdown-rs`:
//
// ```js
// // 4+ spaces.
// if (markdownSpace(code)) {
// return nok(code)
// }
// ```
/** @type {State} */
function cellDividerHead(code) {
assert(code === codes.verticalBar, 'expected `|`')
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
seenDelimiter = true
return cellBreakHead
seen = true
// Count the first character, that isn’t a pipe, double.
sizeB += 1
return headRowBreak(code)
}
/** @type {State} */
function cellBreakHead(code) {
if (code === codes.eof || markdownLineEnding(code)) {
return atRowEndHead(code)
/**
* At break in table head row.
*
* ```markdown
* > | | a |
* ^
* ^
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBreak(code) {
if (code === codes.eof) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
if (markdownLineEnding(code)) {
// If anything other than one pipe (ignoring whitespace) was used, it’s fine.
if (sizeB > 1) {
sizeB = 0
// To do: check if this works.
// Feel free to interrupt:
self.interrupt = true
effects.exit('tableRow')
effects.enter(types.lineEnding)
effects.consume(code)
effects.exit(types.lineEnding)
return headDelimiterStart
}
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
if (markdownSpace(code)) {
effects.enter(types.whitespace)
effects.consume(code)
return inWhitespaceHead
// To do: check if this is fine.
// effects.attempt(State::Next(StateName::GfmTableHeadRowBreak), State::Nok)
// State::Retry(space_or_tab(tokenizer))
return factorySpace(effects, headRowBreak, types.whitespace)(code)
}
if (seenDelimiter) {
seenDelimiter = undefined
tableHeaderCount++
sizeB += 1
if (seen) {
seen = false
// Header cell count.
size += 1
}
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.enter('tableCellDivider')
effects.consume(code)
return inWhitespaceHead
effects.exit('tableCellDivider')
// Whether a delimiter was seen.
seen = true
return headRowBreak
}
effects.exit(types.whitespace)
return cellBreakHead(code)
// Anything else is cell data.
effects.enter(types.data)
return headRowData(code)
}
/** @type {State} */
function inCellContentHead(code) {
// EOF, whitespace, pipe
/**
* In table head row data.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowData(code) {
if (

@@ -247,254 +238,358 @@ code === codes.eof ||

) {
effects.exit('temporaryTableCellContent')
return cellBreakHead(code)
effects.exit(types.data)
return headRowBreak(code)
}
effects.consume(code)
return code === codes.backslash
? inCellContentEscapeHead
: inCellContentHead
return code === codes.backslash ? headRowEscape : headRowData
}
/** @type {State} */
function inCellContentEscapeHead(code) {
/**
* In table head row escape.
*
* ```markdown
* > | | a\-b |
* ^
* | | ---- |
* | | c |
* ```
*
* @type {State}
*/
function headRowEscape(code) {
if (code === codes.backslash || code === codes.verticalBar) {
effects.consume(code)
return inCellContentHead
return headRowData
}
// Anything else.
return inCellContentHead(code)
return headRowData(code)
}
/** @type {State} */
function atRowEndHead(code) {
if (code === codes.eof) {
/**
* Before delimiter row.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterStart(code) {
// Reset `interrupt`.
self.interrupt = false
// Note: in `markdown-rs`, we need to handle piercing here too.
if (self.parser.lazy[self.now().line]) {
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)
}
effects.enter('tableDelimiterRow')
// Track if we’ve seen a `:` or `|`.
seen = false
/** @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
return factorySpace(
effects,
headDelimiterBefore,
types.linePrefix,
self.parser.constructs.disable.null.includes('codeIndented')
? undefined
: constants.tabSize
)(code)
}
if (code === codes.dash) {
effects.enter('tableDelimiterFiller')
effects.consume(code)
hasDash = true
align.push('none')
return inFillerDelimiter
}
return headDelimiterBefore(code)
}
if (code === codes.colon) {
effects.enter('tableDelimiterAlignment')
effects.consume(code)
effects.exit('tableDelimiterAlignment')
align.push('left')
return afterLeftAlignment
/**
* Before delimiter row, after optional whitespace.
*
* Reused when a `|` is found later, to parse another cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterBefore(code) {
if (code === codes.dash || code === codes.colon) {
return headDelimiterValueBefore(code)
}
// If we start with a pipe, we open a cell marker.
if (code === codes.verticalBar) {
seen = true
// If we start with a pipe, we open a cell marker.
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return atDelimiterRowBreak
return headDelimiterCellBefore
}
return nok(code)
// More whitespace / empty row not allowed at start.
return headDelimiterNok(code)
}
/** @type {State} */
function inWhitespaceDelimiter(code) {
/**
* After `|`, before delimiter cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellBefore(code) {
if (markdownSpace(code)) {
effects.consume(code)
return inWhitespaceDelimiter
return factorySpace(
effects,
headDelimiterValueBefore,
types.whitespace
)(code)
}
effects.exit(types.whitespace)
return atDelimiterRowBreak(code)
return headDelimiterValueBefore(code)
}
/** @type {State} */
function inFillerDelimiter(code) {
if (code === codes.dash) {
/**
* Before delimiter cell value.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterValueBefore(code) {
// Align: left.
if (code === codes.colon) {
sizeB += 1
seen = true
effects.enter('tableDelimiterMarker')
effects.consume(code)
return inFillerDelimiter
effects.exit('tableDelimiterMarker')
return headDelimiterLeftAlignmentAfter
}
effects.exit('tableDelimiterFiller')
// Align: none.
if (code === codes.dash) {
sizeB += 1
// To do: seems weird that this *isn’t* left aligned, but that state is used?
return headDelimiterLeftAlignmentAfter(code)
}
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
if (code === codes.eof || markdownLineEnding(code)) {
return headDelimiterCellAfter(code)
}
return atDelimiterRowBreak(code)
return headDelimiterNok(code)
}
/** @type {State} */
function afterLeftAlignment(code) {
/**
* After delimiter cell left alignment marker.
*
* ```markdown
* | | a |
* > | | :- |
* ^
* ```
*
* @type {State}
*/
function headDelimiterLeftAlignmentAfter(code) {
if (code === codes.dash) {
effects.enter('tableDelimiterFiller')
effects.consume(code)
hasDash = true
return inFillerDelimiter
return headDelimiterFiller(code)
}
// Anything else is not ok.
return nok(code)
// Anything else is not ok after the left-align colon.
return headDelimiterNok(code)
}
/** @type {State} */
function afterRightAlignment(code) {
if (code === codes.eof || markdownLineEnding(code)) {
return rowEndDelimiter(code)
/**
* In delimiter cell filler.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterFiller(code) {
if (code === codes.dash) {
effects.consume(code)
return headDelimiterFiller
}
if (markdownSpace(code)) {
effects.enter(types.whitespace)
// Align is `center` if it was `left`, `right` otherwise.
if (code === codes.colon) {
seen = true
effects.exit('tableDelimiterFiller')
effects.enter('tableDelimiterMarker')
effects.consume(code)
return inWhitespaceDelimiter
effects.exit('tableDelimiterMarker')
return headDelimiterRightAlignmentAfter
}
// `|`
if (code === codes.verticalBar) {
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return atDelimiterRowBreak
effects.exit('tableDelimiterFiller')
return headDelimiterRightAlignmentAfter(code)
}
/**
* After delimiter cell right alignment marker.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterRightAlignmentAfter(code) {
if (markdownSpace(code)) {
return factorySpace(
effects,
headDelimiterCellAfter,
types.whitespace
)(code)
}
return nok(code)
return headDelimiterCellAfter(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)
/**
* After delimiter cell.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellAfter(code) {
if (code === codes.verticalBar) {
return headDelimiterBefore(code)
}
if (code === codes.eof) {
return tableClose(code)
if (code === codes.eof || markdownLineEnding(code)) {
// Exit when:
// * there was no `:` or `|` at all (it’s a thematic break or setext
// underline instead)
// * the header cell count is not the delimiter cell count
if (!seen || size !== sizeB) {
return headDelimiterNok(code)
}
// Note: in markdown-rs`, a reset is needed here.
effects.exit('tableDelimiterRow')
effects.exit('tableHead')
// To do: in `markdown-rs`, resolvers need to be registered manually.
// effects.register_resolver(ResolveName::GfmTable)
return ok(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)
return headDelimiterNok(code)
}
/** @type {State} */
function tableClose(code) {
effects.exit('table')
return ok(code)
/**
* In delimiter row, at a disallowed byte.
*
* ```markdown
* | | a |
* > | | x |
* ^
* ```
*
* @type {State}
*/
function headDelimiterNok(code) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
/** @type {State} */
function bodyStart(code) {
effects.enter('tableBody')
return rowStartBody(code)
/**
* Before table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowStart(code) {
// Note: in `markdown-rs` we need to manually take care of a prefix,
// but in `micromark-js` that is done for us, so if we’re here, we’re
// never at whitespace.
effects.enter('tableRow')
return bodyRowBreak(code)
}
/** @type {State} */
function rowStartBody(code) {
effects.enter('tableRow')
// If we start with a pipe, we open a cell marker.
/**
* At break in table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ^
* ^
* ```
*
* @type {State}
*/
function bodyRowBreak(code) {
if (code === codes.verticalBar) {
return cellDividerBody(code)
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return bodyRowBreak
}
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)
effects.exit('tableRow')
return ok(code)
}
if (markdownSpace(code)) {
effects.enter(types.whitespace)
effects.consume(code)
return inWhitespaceBody
return factorySpace(effects, bodyRowBreak, types.whitespace)(code)
}
// `|`
if (code === codes.verticalBar) {
return cellDividerBody(code)
}
// Anything else is cell content.
effects.enter('temporaryTableCellContent')
return inCellContentBody(code)
effects.enter(types.data)
return bodyRowData(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
/**
* In table body row data.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowData(code) {
if (

@@ -505,138 +600,351 @@ code === codes.eof ||

) {
effects.exit('temporaryTableCellContent')
return cellBreakBody(code)
effects.exit(types.data)
return bodyRowBreak(code)
}
effects.consume(code)
return code === codes.backslash
? inCellContentEscapeBody
: inCellContentBody
return code === codes.backslash ? bodyRowEscape : bodyRowData
}
/** @type {State} */
function inCellContentEscapeBody(code) {
/**
* In table body row escape.
*
* ```markdown
* | | a |
* | | ---- |
* > | | b\-c |
* ^
* ```
*
* @type {State}
*/
function bodyRowEscape(code) {
if (code === codes.backslash || code === codes.verticalBar) {
effects.consume(code)
return inCellContentBody
return bodyRowData
}
// Anything else.
return inCellContentBody(code)
return bodyRowData(code)
}
}
/** @type {State} */
function atRowEndBody(code) {
effects.exit('tableRow')
/** @type {Resolver} */
// eslint-disable-next-line complexity
function resolveTable(events, context) {
let index = -1
let inFirstCellAwaitingPipe = true
/** @type {RowKind} */
let rowKind = 0
/** @type {Range} */
let lastCell = [0, 0, 0, 0]
/** @type {Range} */
let cell = [0, 0, 0, 0]
let afterHeadAwaitingFirstBodyRow = false
let lastTableEnd = 0
/** @type {Token | undefined} */
let currentTable
/** @type {Token | undefined} */
let currentBody
/** @type {Token | undefined} */
let currentCell
if (code === codes.eof) {
return tableBodyClose(code)
}
const map = new EditMap()
return effects.check(
nextPrefixedOrBlank,
tableBodyClose,
effects.attempt(
{tokenize: tokenizeRowEnd, partial: true},
factorySpace(
effects,
rowStartBody,
types.linePrefix,
constants.tabSize
),
tableBodyClose
)
)(code)
}
while (++index < events.length) {
const event = events[index]
const token = event[1]
/** @type {State} */
function tableBodyClose(code) {
effects.exit('tableBody')
return tableClose(code)
}
if (event[0] === 'enter') {
// Start of head.
if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = false
/** @type {Tokenizer} */
function tokenizeRowEnd(effects, ok, nok) {
return start
// Inject previous (body end and) table end.
if (lastTableEnd !== 0) {
assert(currentTable, 'there should be a table opening')
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody)
currentBody = undefined
lastTableEnd = 0
}
/** @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)
// Inject table start.
currentTable = {
type: 'table',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
}
map.add(index, 0, [['enter', currentTable, context]])
} else if (
token.type === 'tableRow' ||
token.type === 'tableDelimiterRow'
) {
return nok(code)
}
inFirstCellAwaitingPipe = true
currentCell = undefined
lastCell = [0, 0, 0, 0]
cell = [0, index + 1, 0, 0]
const tail = self.events[self.events.length - 1]
// Inject table body start.
if (afterHeadAwaitingFirstBodyRow) {
afterHeadAwaitingFirstBodyRow = false
currentBody = {
type: 'tableBody',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
}
map.add(index, 0, [['enter', currentBody, context]])
}
// 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
rowKind = token.type === 'tableDelimiterRow' ? 2 : currentBody ? 3 : 1
}
// Cell data.
else if (
rowKind &&
(token.type === types.data ||
token.type === 'tableDelimiterMarker' ||
token.type === 'tableDelimiterFiller')
) {
return nok(code)
}
inFirstCellAwaitingPipe = false
self._gfmTableDynamicInterruptHack = true
// First value in cell.
if (cell[2] === 0) {
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
undefined,
currentCell
)
lastCell = [0, 0, 0, 0]
}
return effects.check(
self.parser.constructs.flow,
function (code) {
self._gfmTableDynamicInterruptHack = false
return nok(code)
},
function (code) {
self._gfmTableDynamicInterruptHack = false
return ok(code)
cell[2] = index
}
)(code)
} else if (token.type === 'tableCellDivider') {
if (inFirstCellAwaitingPipe) {
inFirstCellAwaitingPipe = false
} else {
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
undefined,
currentCell
)
}
lastCell = cell
cell = [lastCell[1], index, 0, 0]
}
}
}
// Exit events.
else if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = true
lastTableEnd = index
} else if (
token.type === 'tableRow' ||
token.type === 'tableDelimiterRow'
) {
lastTableEnd = index
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
index,
currentCell
)
} else if (cell[1] !== 0) {
currentCell = flushCell(map, context, cell, rowKind, index, currentCell)
}
rowKind = 0
} else if (
rowKind &&
(token.type === types.data ||
token.type === 'tableDelimiterMarker' ||
token.type === 'tableDelimiterFiller')
) {
cell[3] = index
}
}
if (lastTableEnd !== 0) {
assert(currentTable, 'expected table opening')
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody)
}
map.consume(context.events)
// To do: move this into `html`, when events are exposed there.
// That’s what `markdown-rs` does.
// That needs updates to `mdast-util-gfm-table`.
index = -1
while (++index < context.events.length) {
const event = context.events[index]
if (event[0] === 'enter' && event[1].type === 'table') {
// @ts-expect-error: custom field.
event[1]._align = gfmTableAlign(context.events, index)
}
}
return events
}
/** @type {Tokenizer} */
function tokenizeNextPrefixedOrBlank(effects, ok, nok) {
let size = 0
/// Generate a cell.
/**
*
* @param {EditMap} map
* @param {TokenizeContext} context
* @param {Range} range
* @param {RowKind} rowKind
* @param {number | undefined} rowEnd
* @param {Token | undefined} previousCell
* @returns {Token | undefined}
*/
// eslint-disable-next-line max-params
function flushCell(map, context, range, rowKind, rowEnd, previousCell) {
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCell' : 'tableCell'
const groupName =
rowKind === 1
? 'tableHeader'
: rowKind === 2
? 'tableDelimiter'
: 'tableData'
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCellValue' : 'tableCellText'
const valueName = 'tableContent'
return start
// Insert an exit for the previous cell, if there is one.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^-- this cell
// ```
if (range[0] !== 0) {
assert(previousCell, 'expected previous cell enter')
previousCell.end = Object.assign({}, getPoint(context.events, range[0]))
map.add(range[0], 0, [['exit', previousCell, context]])
}
/** @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
// Insert enter of this cell.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^^^^-- this cell
// ```
const now = getPoint(context.events, range[1])
previousCell = {
type: groupName,
start: Object.assign({}, now),
// Note: correct end is set later.
end: Object.assign({}, now)
}
map.add(range[1], 0, [['enter', previousCell, context]])
/** @type {State} */
function whitespace(code) {
if (code === codes.virtualSpace || code === codes.space) {
effects.consume(code)
size++
return size === constants.tabSize ? ok : whitespace
// Insert text start at first data start and end at last data end, and
// remove events between.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^-- exit
// ^^^^-- this cell
// ```
if (range[2] !== 0) {
const relatedStart = getPoint(context.events, range[2])
const relatedEnd = getPoint(context.events, range[3])
const valueToken = {
type: valueName,
start: Object.assign({}, relatedStart),
end: Object.assign({}, relatedEnd)
}
map.add(range[2], 0, [['enter', valueToken, context]])
assert(range[3] !== 0)
// EOF or whitespace
if (code === codes.eof || markdownLineEndingOrSpace(code)) {
return ok(code)
if (rowKind !== 2) {
// Fix positional info on remaining events
const start = context.events[range[2]]
const end = context.events[range[3]]
start[1].end = Object.assign({}, end[1].end)
start[1].type = types.chunkText
// @ts-expect-error It’s fine.
start[1].contentType = constants.contentTypeText
// Remove if needed.
if (range[3] > range[2] + 1) {
const a = range[2] + 1
const b = range[3] - range[2] - 1
map.add(a, b, [])
}
}
// Anything else.
return nok(code)
map.add(range[3] + 1, 0, [['exit', valueToken, context]])
}
// Insert an exit for the last cell, if at the row end.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^^^-- this cell (the last one contains two “between” parts)
// ```
if (rowEnd !== undefined) {
previousCell.end = Object.assign({}, getPoint(context.events, rowEnd))
map.add(rowEnd, 0, [['exit', previousCell, context]])
previousCell = undefined
}
return previousCell
}
/**
* Generate table end (and table body end).
*
* @param {EditMap} map
* @param {TokenizeContext} context
* @param {number} index
* @param {Token} table
* @param {Token | undefined} tableBody
*/
// eslint-disable-next-line max-params
function flushTableEnd(map, context, index, table, tableBody) {
/** @type {Array<Event>} */
const exits = []
const related = getPoint(context.events, index)
if (tableBody) {
tableBody.end = Object.assign({}, related)
exits.push(['exit', tableBody, context])
}
table.end = Object.assign({}, related)
exits.push(['exit', table, context])
map.add(index + 1, 0, exits)
}
/**
* @param {Array<Event>} events
* @param {number} index
* @returns {readonly Point}
*/
function getPoint(events, index) {
const event = events[index]
const side = event[0] === 'enter' ? 'start' : 'end'
return event[1][side]
}

@@ -1,4 +0,9 @@

/** @type {HtmlExtension} */
/**
* Extension for `micromark` that can be passed in `htmlExtensions` to support
* GFM tables when serializing to HTML.
*
* @type {HtmlExtension}
*/
export const gfmTableHtml: HtmlExtension
export type HtmlExtension = import('micromark-util-types').HtmlExtension
export type Align = import('./syntax.js').Align
export type Align = import('./infer.js').Align
/**
* @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension
* @typedef {import('./syntax.js').Align} Align
*/
/**
* @typedef {import('./infer.js').Align} Align
*/
const alignment = {

@@ -11,4 +15,12 @@ none: '',

}
/** @type {HtmlExtension} */
// To do: next major: expose functions.
// To do: next major: use `infer` here, when all events are exposed.
/**
* Extension for `micromark` that can be passed in `htmlExtensions` to support
* GFM tables when serializing to HTML.
*
* @type {HtmlExtension}
*/
export const gfmTableHtml = {

@@ -24,18 +36,9 @@ enter: {

},
tableBody() {
// Clear slurping line ending from the delimiter row.
this.setData('slurpOneLineEnding')
this.tag('<tbody>')
},
tableData() {
const tableAlign =
/** @type {Array<Align>} */
this.getData('tableAlign')
const tableColumn =
/** @type {number} */
this.getData('tableColumn')
const tableAlign = /** @type {Array<Align>} */ this.getData('tableAlign')
const tableColumn = /** @type {number} */ this.getData('tableColumn')
const align = alignment[tableAlign[tableColumn]]
if (align === undefined) {

@@ -49,3 +52,2 @@ // Capture results to ignore them.

},
tableHead() {

@@ -55,10 +57,5 @@ this.lineEndingIfNeeded()

},
tableHeader() {
const tableAlign =
/** @type {Array<Align>} */
this.getData('tableAlign')
const tableColumn =
/** @type {number} */
this.getData('tableColumn')
const tableAlign = /** @type {Array<Align>} */ this.getData('tableAlign')
const tableColumn = /** @type {number} */ this.getData('tableColumn')
const align = alignment[tableAlign[tableColumn]]

@@ -68,3 +65,2 @@ this.lineEndingIfNeeded()

},
tableRow() {

@@ -81,14 +77,12 @@ this.setData('tableColumn', 0)

let value = this.sliceSerialize(token)
if (this.getData('tableAlign')) {
value = value.replace(/\\([\\|])/g, replace)
}
this.raw(this.encode(value))
},
table() {
this.setData('tableAlign') // If there was no table body, make sure the slurping from the delimiter row
// is cleared.
this.setData('tableAlign')
// Note: we don’t set `slurpAllLineEndings` anymore, in delimiter rows,
// but we do need to reset it to match a funky newline GH generates for
// list items combined with tables.
this.setData('slurpAllLineEndings')

@@ -98,3 +92,2 @@ this.lineEndingIfNeeded()

},
tableBody() {

@@ -104,11 +97,5 @@ this.lineEndingIfNeeded()

},
tableData() {
const tableAlign =
/** @type {Array<Align>} */
this.getData('tableAlign')
const tableColumn =
/** @type {number} */
this.getData('tableColumn')
const tableAlign = /** @type {Array<Align>} */ this.getData('tableAlign')
const tableColumn = /** @type {number} */ this.getData('tableColumn')
if (tableColumn in tableAlign) {

@@ -122,25 +109,14 @@ this.tag('</td>')

},
tableHead() {
this.lineEndingIfNeeded()
this.tag('</thead>')
this.setData('slurpOneLineEnding', true) // Slurp the line ending from the delimiter row.
},
tableHeader() {
const tableColumn =
/** @type {number} */
this.getData('tableColumn')
const tableColumn = /** @type {number} */ this.getData('tableColumn')
this.tag('</th>')
this.setData('tableColumn', tableColumn + 1)
},
tableRow() {
const tableAlign =
/** @type {Array<Align>} */
this.getData('tableAlign')
let tableColumn =
/** @type {number} */
this.getData('tableColumn')
const tableAlign = /** @type {Array<Align>} */ this.getData('tableAlign')
let tableColumn = /** @type {number} */ this.getData('tableColumn')
while (tableColumn < tableAlign.length) {

@@ -151,3 +127,2 @@ this.lineEndingIfNeeded()

}
this.setData('tableColumn', tableColumn)

@@ -159,2 +134,3 @@ this.lineEndingIfNeeded()

}
/**

@@ -165,3 +141,2 @@ * @param {string} $0

*/
function replace($0, $1) {

@@ -168,0 +143,0 @@ // Pipes work, backslashes don’t (but can’t escape pipes).

@@ -1,8 +0,23 @@

/** @type {Extension} */
/**
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*
* @type {Extension}
*/
export const gfmTable: Extension
export type Event = import('micromark-util-types').Event
export type Extension = import('micromark-util-types').Extension
export type Point = import('micromark-util-types').Point
export type Resolver = import('micromark-util-types').Resolver
export type Tokenizer = import('micromark-util-types').Tokenizer
export type State = import('micromark-util-types').State
export type Token = import('micromark-util-types').Token
export type Align = 'left' | 'center' | 'right' | 'none'
export type TokenizeContext = import('micromark-util-types').TokenizeContext
export type Tokenizer = import('micromark-util-types').Tokenizer
/**
* Cell info.
*/
export type Range = [number, number, number, number]
/**
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/
export type RowKind = 0 | 1 | 2 | 3
/**
* @typedef {import('micromark-util-types').Event} Event
* @typedef {import('micromark-util-types').Extension} Extension
* @typedef {import('micromark-util-types').Point} Point
* @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 {import('micromark-util-types').TokenizeContext} TokenizeContext
* @typedef {import('micromark-util-types').Tokenizer} Tokenizer
*/
/**
* @typedef {'left'|'center'|'right'|'none'} Align
* @typedef {[number, number, number, number]} Range
* Cell info.
*
* @typedef {0 | 1 | 2 | 3} RowKind
* Where we are: `1` for head row, `2` for delimiter row, `3` for body row.
*/
import {factorySpace} from 'micromark-factory-space'

@@ -18,4 +26,13 @@ import {

} from 'micromark-util-character'
import {EditMap} from './edit-map.js'
import {gfmTableAlign} from './infer.js'
/** @type {Extension} */
// To do: next major: expose functions.
/**
* Extension for `micromark` that can be passed in `extensions` to enable GFM
* table syntax.
*
* @type {Extension}
*/
export const gfmTable = {

@@ -25,583 +42,844 @@ flow: {

tokenize: tokenizeTable,
resolve: resolveTable
resolveAll: resolveTable
}
}
}
const nextPrefixedOrBlank = {
tokenize: tokenizeNextPrefixedOrBlank,
partial: true
}
/** @type {Resolver} */
function resolveTable(events, context) {
let index = -1
/** @type {boolean|undefined} */
/**
* @this {TokenizeContext}
* @type {Tokenizer}
*/
function tokenizeTable(effects, ok, nok) {
const self = this
let size = 0
let sizeB = 0
/** @type {boolean | undefined} */
let seen
return start
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
}
/**
* Start of a GFM table.
*
* If there is a valid table row or table head before, then we try to parse
* another row.
* Otherwise, we try to parse a head.
*
* ```markdown
* > | | a |
* ^
* | | - |
* > | | b |
* ^
* ```
* @type {State}
*/
function start(code) {
let index = self.events.length - 1
while (index > -1) {
const type = self.events[index][1].type
if (
// Combine separate content parts into one.
(token.type === 'tableCellDivider' || token.type === 'tableRow') &&
contentEnd
) {
const content = {
type: 'tableContent',
start: events[contentStart][1].start,
end: events[contentEnd][1].end
}
/** @type {Token} */
const text = {
type: 'chunkText',
start: content.start,
end: content.end,
// @ts-expect-error It’s fine.
contentType: 'text'
}
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
}
type === 'lineEnding' ||
// Note: markdown-rs uses `whitespace` instead of `linePrefix`
type === 'linePrefix'
)
index--
else break
}
const tail = index > -1 ? self.events[index][1].type : null
const next =
tail === 'tableHead' || tail === 'tableRow' ? bodyRowStart : headRowBefore
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 !== '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
// Don’t allow lazy body rows.
if (next === bodyRowStart && self.parser.lazy[self.now().line]) {
return nok(code)
}
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 next(code)
}
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
/**
* Before table head row.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBefore(code) {
effects.enter('tableHead')
effects.enter('tableRow') // If we start with a pipe, we open a cell marker.
effects.enter('tableRow')
return headRowStart(code)
}
/**
* Before table head row, after whitespace.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowStart(code) {
if (code === 124) {
return cellDividerHead(code)
return headRowBreak(code)
}
tableHeaderCount++
effects.enter('temporaryTableCellContent') // Can’t be space or eols at the start of a construct, so we’re in a cell.
// To do: micromark-js should let us parse our own whitespace in extensions,
// like `markdown-rs`:
//
// ```js
// // 4+ spaces.
// if (markdownSpace(code)) {
// return nok(code)
// }
// ```
return inCellContentHead(code)
seen = true
// Count the first character, that isn’t a pipe, double.
sizeB += 1
return headRowBreak(code)
}
/** @type {State} */
function cellDividerHead(code) {
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
seenDelimiter = true
return cellBreakHead
}
/** @type {State} */
/**
* At break in table head row.
*
* ```markdown
* > | | a |
* ^
* ^
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowBreak(code) {
if (code === null) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
if (markdownLineEnding(code)) {
// If anything other than one pipe (ignoring whitespace) was used, it’s fine.
if (sizeB > 1) {
sizeB = 0
// To do: check if this works.
// Feel free to interrupt:
self.interrupt = true
effects.exit('tableRow')
effects.enter('lineEnding')
effects.consume(code)
effects.exit('lineEnding')
return headDelimiterStart
}
function cellBreakHead(code) {
if (code === null || markdownLineEnding(code)) {
return atRowEndHead(code)
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
if (markdownSpace(code)) {
effects.enter('whitespace')
effects.consume(code)
return inWhitespaceHead
// To do: check if this is fine.
// effects.attempt(State::Next(StateName::GfmTableHeadRowBreak), State::Nok)
// State::Retry(space_or_tab(tokenizer))
return factorySpace(effects, headRowBreak, 'whitespace')(code)
}
if (seenDelimiter) {
seenDelimiter = undefined
tableHeaderCount++
sizeB += 1
if (seen) {
seen = false
// Header cell count.
size += 1
}
if (code === 124) {
return cellDividerHead(code)
} // Anything else is cell content.
effects.enter('temporaryTableCellContent')
return inCellContentHead(code)
}
/** @type {State} */
function inWhitespaceHead(code) {
if (markdownSpace(code)) {
effects.enter('tableCellDivider')
effects.consume(code)
return inWhitespaceHead
effects.exit('tableCellDivider')
// Whether a delimiter was seen.
seen = true
return headRowBreak
}
effects.exit('whitespace')
return cellBreakHead(code)
// Anything else is cell data.
effects.enter('data')
return headRowData(code)
}
/** @type {State} */
function inCellContentHead(code) {
// EOF, whitespace, pipe
/**
* In table head row data.
*
* ```markdown
* > | | a |
* ^
* | | - |
* | | b |
* ```
*
* @type {State}
*/
function headRowData(code) {
if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
effects.exit('temporaryTableCellContent')
return cellBreakHead(code)
effects.exit('data')
return headRowBreak(code)
}
effects.consume(code)
return code === 92 ? inCellContentEscapeHead : inCellContentHead
return code === 92 ? headRowEscape : headRowData
}
/** @type {State} */
function inCellContentEscapeHead(code) {
/**
* In table head row escape.
*
* ```markdown
* > | | a\-b |
* ^
* | | ---- |
* | | c |
* ```
*
* @type {State}
*/
function headRowEscape(code) {
if (code === 92 || code === 124) {
effects.consume(code)
return inCellContentHead
} // Anything else.
return inCellContentHead(code)
return headRowData
}
return headRowData(code)
}
/** @type {State} */
function atRowEndHead(code) {
if (code === null) {
/**
* Before delimiter row.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterStart(code) {
// Reset `interrupt`.
self.interrupt = false
// Note: in `markdown-rs`, we need to handle piercing here too.
if (self.parser.lazy[self.now().line]) {
return nok(code)
}
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 === null || markdownLineEnding(code)) {
return rowEndDelimiter(code)
}
effects.enter('tableDelimiterRow')
// Track if we’ve seen a `:` or `|`.
seen = false
if (markdownSpace(code)) {
effects.enter('whitespace')
effects.consume(code)
return inWhitespaceDelimiter
return factorySpace(
effects,
headDelimiterBefore,
'linePrefix',
self.parser.constructs.disable.null.includes('codeIndented')
? undefined
: 4
)(code)
}
return headDelimiterBefore(code)
}
if (code === 45) {
effects.enter('tableDelimiterFiller')
effects.consume(code)
hasDash = true
align.push('none')
return inFillerDelimiter
/**
* Before delimiter row, after optional whitespace.
*
* Reused when a `|` is found later, to parse another cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* | | b |
* ```
*
* @type {State}
*/
function headDelimiterBefore(code) {
if (code === 45 || code === 58) {
return headDelimiterValueBefore(code)
}
if (code === 58) {
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 === 124) {
seen = true
// If we start with a pipe, we open a cell marker.
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return atDelimiterRowBreak
return headDelimiterCellBefore
}
return nok(code)
// More whitespace / empty row not allowed at start.
return headDelimiterNok(code)
}
/** @type {State} */
function inWhitespaceDelimiter(code) {
/**
* After `|`, before delimiter cell.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellBefore(code) {
if (markdownSpace(code)) {
effects.consume(code)
return inWhitespaceDelimiter
return factorySpace(effects, headDelimiterValueBefore, 'whitespace')(code)
}
effects.exit('whitespace')
return atDelimiterRowBreak(code)
return headDelimiterValueBefore(code)
}
/** @type {State} */
function inFillerDelimiter(code) {
if (code === 45) {
/**
* Before delimiter cell value.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterValueBefore(code) {
// Align: left.
if (code === 58) {
sizeB += 1
seen = true
effects.enter('tableDelimiterMarker')
effects.consume(code)
return inFillerDelimiter
effects.exit('tableDelimiterMarker')
return headDelimiterLeftAlignmentAfter
}
effects.exit('tableDelimiterFiller')
if (code === 58) {
effects.enter('tableDelimiterAlignment')
effects.consume(code)
effects.exit('tableDelimiterAlignment')
align[align.length - 1] =
align[align.length - 1] === 'left' ? 'center' : 'right'
return afterRightAlignment
// Align: none.
if (code === 45) {
sizeB += 1
// To do: seems weird that this *isn’t* left aligned, but that state is used?
return headDelimiterLeftAlignmentAfter(code)
}
return atDelimiterRowBreak(code)
if (code === null || markdownLineEnding(code)) {
return headDelimiterCellAfter(code)
}
return headDelimiterNok(code)
}
/** @type {State} */
function afterLeftAlignment(code) {
/**
* After delimiter cell left alignment marker.
*
* ```markdown
* | | a |
* > | | :- |
* ^
* ```
*
* @type {State}
*/
function headDelimiterLeftAlignmentAfter(code) {
if (code === 45) {
effects.enter('tableDelimiterFiller')
effects.consume(code)
hasDash = true
return inFillerDelimiter
} // Anything else is not ok.
return headDelimiterFiller(code)
}
return nok(code)
// Anything else is not ok after the left-align colon.
return headDelimiterNok(code)
}
/** @type {State} */
function afterRightAlignment(code) {
if (code === null || markdownLineEnding(code)) {
return rowEndDelimiter(code)
/**
* In delimiter cell filler.
*
* ```markdown
* | | a |
* > | | - |
* ^
* ```
*
* @type {State}
*/
function headDelimiterFiller(code) {
if (code === 45) {
effects.consume(code)
return headDelimiterFiller
}
if (markdownSpace(code)) {
effects.enter('whitespace')
// Align is `center` if it was `left`, `right` otherwise.
if (code === 58) {
seen = true
effects.exit('tableDelimiterFiller')
effects.enter('tableDelimiterMarker')
effects.consume(code)
return inWhitespaceDelimiter
} // `|`
effects.exit('tableDelimiterMarker')
return headDelimiterRightAlignmentAfter
}
effects.exit('tableDelimiterFiller')
return headDelimiterRightAlignmentAfter(code)
}
if (code === 124) {
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return atDelimiterRowBreak
/**
* After delimiter cell right alignment marker.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterRightAlignmentAfter(code) {
if (markdownSpace(code)) {
return factorySpace(effects, headDelimiterCellAfter, 'whitespace')(code)
}
return nok(code)
return headDelimiterCellAfter(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)
/**
* After delimiter cell.
*
* ```markdown
* | | a |
* > | | -: |
* ^
* ```
*
* @type {State}
*/
function headDelimiterCellAfter(code) {
if (code === 124) {
return headDelimiterBefore(code)
}
if (code === null || markdownLineEnding(code)) {
// Exit when:
// * there was no `:` or `|` at all (it’s a thematic break or setext
// underline instead)
// * the header cell count is not the delimiter cell count
if (!seen || size !== sizeB) {
return headDelimiterNok(code)
}
if (code === null) {
return tableClose(code)
// Note: in markdown-rs`, a reset is needed here.
effects.exit('tableDelimiterRow')
effects.exit('tableHead')
// To do: in `markdown-rs`, resolvers need to be registered manually.
// effects.register_resolver(ResolveName::GfmTable)
return ok(code)
}
return effects.check(
nextPrefixedOrBlank,
tableClose,
effects.attempt(
{
tokenize: tokenizeRowEnd,
partial: true
},
factorySpace(effects, bodyStart, 'linePrefix', 4),
tableClose
)
)(code)
return headDelimiterNok(code)
}
/** @type {State} */
function tableClose(code) {
effects.exit('table')
return ok(code)
/**
* In delimiter row, at a disallowed byte.
*
* ```markdown
* | | a |
* > | | x |
* ^
* ```
*
* @type {State}
*/
function headDelimiterNok(code) {
// Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t.
return nok(code)
}
/** @type {State} */
function bodyStart(code) {
effects.enter('tableBody')
return rowStartBody(code)
/**
* Before table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowStart(code) {
// Note: in `markdown-rs` we need to manually take care of a prefix,
// but in `micromark-js` that is done for us, so if we’re here, we’re
// never at whitespace.
effects.enter('tableRow')
return bodyRowBreak(code)
}
/** @type {State} */
function rowStartBody(code) {
effects.enter('tableRow') // If we start with a pipe, we open a cell marker.
/**
* At break in table body row.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ^
* ^
* ```
*
* @type {State}
*/
function bodyRowBreak(code) {
if (code === 124) {
return cellDividerBody(code)
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return bodyRowBreak
}
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) {
effects.enter('tableCellDivider')
effects.consume(code)
effects.exit('tableCellDivider')
return cellBreakBody
}
/** @type {State} */
function cellBreakBody(code) {
if (code === null || markdownLineEnding(code)) {
return atRowEndBody(code)
effects.exit('tableRow')
return ok(code)
}
if (markdownSpace(code)) {
effects.enter('whitespace')
effects.consume(code)
return inWhitespaceBody
} // `|`
if (code === 124) {
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
return factorySpace(effects, bodyRowBreak, 'whitespace')(code)
}
effects.exit('whitespace')
return cellBreakBody(code)
// Anything else is cell content.
effects.enter('data')
return bodyRowData(code)
}
/** @type {State} */
function inCellContentBody(code) {
// EOF, whitespace, pipe
/**
* In table body row data.
*
* ```markdown
* | | a |
* | | - |
* > | | b |
* ^
* ```
*
* @type {State}
*/
function bodyRowData(code) {
if (code === null || code === 124 || markdownLineEndingOrSpace(code)) {
effects.exit('temporaryTableCellContent')
return cellBreakBody(code)
effects.exit('data')
return bodyRowBreak(code)
}
effects.consume(code)
return code === 92 ? inCellContentEscapeBody : inCellContentBody
return code === 92 ? bodyRowEscape : bodyRowData
}
/** @type {State} */
function inCellContentEscapeBody(code) {
/**
* In table body row escape.
*
* ```markdown
* | | a |
* | | ---- |
* > | | b\-c |
* ^
* ```
*
* @type {State}
*/
function bodyRowEscape(code) {
if (code === 92 || code === 124) {
effects.consume(code)
return inCellContentBody
} // Anything else.
return inCellContentBody(code)
}
/** @type {State} */
function atRowEndBody(code) {
effects.exit('tableRow')
if (code === null) {
return tableBodyClose(code)
return bodyRowData
}
return effects.check(
nextPrefixedOrBlank,
tableBodyClose,
effects.attempt(
{
tokenize: tokenizeRowEnd,
partial: true
},
factorySpace(effects, rowStartBody, 'linePrefix', 4),
tableBodyClose
)
)(code)
return bodyRowData(code)
}
/** @type {State} */
}
function tableBodyClose(code) {
effects.exit('tableBody')
return tableClose(code)
}
/** @type {Tokenizer} */
/** @type {Resolver} */
// eslint-disable-next-line complexity
function resolveTable(events, context) {
let index = -1
let inFirstCellAwaitingPipe = true
/** @type {RowKind} */
let rowKind = 0
/** @type {Range} */
let lastCell = [0, 0, 0, 0]
/** @type {Range} */
let cell = [0, 0, 0, 0]
let afterHeadAwaitingFirstBodyRow = false
let lastTableEnd = 0
/** @type {Token | undefined} */
let currentTable
/** @type {Token | undefined} */
let currentBody
/** @type {Token | undefined} */
let currentCell
const map = new EditMap()
while (++index < events.length) {
const event = events[index]
const token = event[1]
if (event[0] === 'enter') {
// Start of head.
if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = false
function tokenizeRowEnd(effects, ok, nok) {
return start
/** @type {State} */
// Inject previous (body end and) table end.
if (lastTableEnd !== 0) {
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody)
currentBody = undefined
lastTableEnd = 0
}
function start(code) {
effects.enter('lineEnding')
effects.consume(code)
effects.exit('lineEnding')
return factorySpace(effects, prefixed, 'linePrefix')
}
/** @type {State} */
// Inject table start.
currentTable = {
type: 'table',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
}
map.add(index, 0, [['enter', currentTable, context]])
} else if (
token.type === 'tableRow' ||
token.type === 'tableDelimiterRow'
) {
inFirstCellAwaitingPipe = true
currentCell = undefined
lastCell = [0, 0, 0, 0]
cell = [0, index + 1, 0, 0]
function prefixed(code) {
// Blank or interrupting line.
if (
self.parser.lazy[self.now().line] ||
code === null ||
markdownLineEnding(code)
) {
return nok(code)
// Inject table body start.
if (afterHeadAwaitingFirstBodyRow) {
afterHeadAwaitingFirstBodyRow = false
currentBody = {
type: 'tableBody',
start: Object.assign({}, token.start),
// Note: correct end is set later.
end: Object.assign({}, token.end)
}
map.add(index, 0, [['enter', currentBody, context]])
}
rowKind = token.type === 'tableDelimiterRow' ? 2 : currentBody ? 3 : 1
}
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 === 'linePrefix' &&
tail[2].sliceSerialize(tail[1], true).length >= 4
// Cell data.
else if (
rowKind &&
(token.type === 'data' ||
token.type === 'tableDelimiterMarker' ||
token.type === 'tableDelimiterFiller')
) {
return nok(code)
}
inFirstCellAwaitingPipe = false
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)
// First value in cell.
if (cell[2] === 0) {
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
undefined,
currentCell
)
lastCell = [0, 0, 0, 0]
}
cell[2] = index
}
)(code)
} else if (token.type === 'tableCellDivider') {
if (inFirstCellAwaitingPipe) {
inFirstCellAwaitingPipe = false
} else {
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
undefined,
currentCell
)
}
lastCell = cell
cell = [lastCell[1], index, 0, 0]
}
}
}
// Exit events.
else if (token.type === 'tableHead') {
afterHeadAwaitingFirstBodyRow = true
lastTableEnd = index
} else if (
token.type === 'tableRow' ||
token.type === 'tableDelimiterRow'
) {
lastTableEnd = index
if (lastCell[1] !== 0) {
cell[0] = cell[1]
currentCell = flushCell(
map,
context,
lastCell,
rowKind,
index,
currentCell
)
} else if (cell[1] !== 0) {
currentCell = flushCell(map, context, cell, rowKind, index, currentCell)
}
rowKind = 0
} else if (
rowKind &&
(token.type === 'data' ||
token.type === 'tableDelimiterMarker' ||
token.type === 'tableDelimiterFiller')
) {
cell[3] = index
}
}
if (lastTableEnd !== 0) {
flushTableEnd(map, context, lastTableEnd, currentTable, currentBody)
}
map.consume(context.events)
// To do: move this into `html`, when events are exposed there.
// That’s what `markdown-rs` does.
// That needs updates to `mdast-util-gfm-table`.
index = -1
while (++index < context.events.length) {
const event = context.events[index]
if (event[0] === 'enter' && event[1].type === 'table') {
// @ts-expect-error: custom field.
event[1]._align = gfmTableAlign(context.events, index)
}
}
return events
}
/** @type {Tokenizer} */
function tokenizeNextPrefixedOrBlank(effects, ok, nok) {
let size = 0
return start
/** @type {State} */
/// Generate a cell.
/**
*
* @param {EditMap} map
* @param {TokenizeContext} context
* @param {Range} range
* @param {RowKind} rowKind
* @param {number | undefined} rowEnd
* @param {Token | undefined} previousCell
* @returns {Token | undefined}
*/
// eslint-disable-next-line max-params
function flushCell(map, context, range, rowKind, rowEnd, previousCell) {
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCell' : 'tableCell'
const groupName =
rowKind === 1
? 'tableHeader'
: rowKind === 2
? 'tableDelimiter'
: 'tableData'
// `markdown-rs` uses:
// rowKind === 2 ? 'tableDelimiterCellValue' : 'tableCellText'
const valueName = 'tableContent'
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.
// Insert an exit for the previous cell, if there is one.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^-- this cell
// ```
if (range[0] !== 0) {
previousCell.end = Object.assign({}, getPoint(context.events, range[0]))
map.add(range[0], 0, [['exit', previousCell, context]])
}
effects.consume(code)
return whitespace
// Insert enter of this cell.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^^^^-- this cell
// ```
const now = getPoint(context.events, range[1])
previousCell = {
type: groupName,
start: Object.assign({}, now),
// Note: correct end is set later.
end: Object.assign({}, now)
}
/** @type {State} */
map.add(range[1], 0, [['enter', previousCell, context]])
function whitespace(code) {
if (code === -1 || code === 32) {
effects.consume(code)
size++
return size === 4 ? ok : whitespace
} // EOF or whitespace
// Insert text start at first data start and end at last data end, and
// remove events between.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- enter
// ^-- exit
// ^^^^-- this cell
// ```
if (range[2] !== 0) {
const relatedStart = getPoint(context.events, range[2])
const relatedEnd = getPoint(context.events, range[3])
const valueToken = {
type: valueName,
start: Object.assign({}, relatedStart),
end: Object.assign({}, relatedEnd)
}
map.add(range[2], 0, [['enter', valueToken, context]])
if (rowKind !== 2) {
// Fix positional info on remaining events
const start = context.events[range[2]]
const end = context.events[range[3]]
start[1].end = Object.assign({}, end[1].end)
start[1].type = 'chunkText'
// @ts-expect-error It’s fine.
start[1].contentType = 'text'
if (code === null || markdownLineEndingOrSpace(code)) {
return ok(code)
} // Anything else.
// Remove if needed.
if (range[3] > range[2] + 1) {
const a = range[2] + 1
const b = range[3] - range[2] - 1
map.add(a, b, [])
}
}
map.add(range[3] + 1, 0, [['exit', valueToken, context]])
}
return nok(code)
// Insert an exit for the last cell, if at the row end.
//
// ```markdown
// > | | aa | bb | cc |
// ^-- exit
// ^^^^^^-- this cell (the last one contains two “between” parts)
// ```
if (rowEnd !== undefined) {
previousCell.end = Object.assign({}, getPoint(context.events, rowEnd))
map.add(rowEnd, 0, [['exit', previousCell, context]])
previousCell = undefined
}
return previousCell
}
/**
* Generate table end (and table body end).
*
* @param {EditMap} map
* @param {TokenizeContext} context
* @param {number} index
* @param {Token} table
* @param {Token | undefined} tableBody
*/
// eslint-disable-next-line max-params
function flushTableEnd(map, context, index, table, tableBody) {
/** @type {Array<Event>} */
const exits = []
const related = getPoint(context.events, index)
if (tableBody) {
tableBody.end = Object.assign({}, related)
exits.push(['exit', tableBody, context])
}
table.end = Object.assign({}, related)
exits.push(['exit', table, context])
map.add(index + 1, 0, exits)
}
/**
* @param {Array<Event>} events
* @param {number} index
* @returns {readonly Point}
*/
function getPoint(events, index) {
const event = events[index]
const side = event[0] === 'enter' ? 'start' : 'end'
return event[1][side]
}
{
"name": "micromark-extension-gfm-table",
"version": "1.0.5",
"version": "1.0.6",
"description": "micromark extension to support GFM tables",

@@ -50,3 +50,3 @@ "license": "MIT",

"devDependencies": {
"@types/tape": "^4.0.0",
"@types/node": "^20.0.0",
"c8": "^7.0.0",

@@ -57,15 +57,16 @@ "create-gfm-fixtures": "^1.0.0",

"prettier": "^2.0.0",
"remark-cli": "^10.0.0",
"remark-cli": "^11.0.0",
"remark-preset-wooorm": "^9.0.0",
"rimraf": "^3.0.0",
"tape": "^5.0.0",
"type-coverage": "^2.0.0",
"typescript": "^4.0.0",
"xo": "^0.47.0"
"typescript": "^5.0.0",
"xo": "^0.54.0"
},
"scripts": {
"build": "rimraf \"dev/**/*.d.ts\" \"test/**/*.d.ts\" && tsc && type-coverage && micromark-build",
"prepack": "npm run build && npm run format",
"build": "tsc --build --clean && tsc --build && type-coverage && micromark-build",
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
"test-api": "node --conditions development test/index.js",
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node --conditions development test/index.js",
"test-api-prod": "node --conditions production test/index.js",
"test-api-dev": "node --conditions development test/index.js",
"test-api": "npm run test-api-dev && npm run test-api-prod",
"test-coverage": "c8 --100 --reporter lcov npm run test-api",
"test": "npm run build && npm run format && npm run test-coverage"

@@ -84,10 +85,20 @@ },

"rules": {
"node/file-extension-in-import": "off",
"unicorn/no-this-assignment": "off",
"complexity": "off"
}
"max-depth": "off",
"n/file-extension-in-import": "off",
"unicorn/no-this-assignment": "off"
},
"overrides": [
{
"files": [
"test/**/*.js"
],
"rules": {
"no-await-in-loop": 0
}
}
]
},
"remarkConfig": {
"plugins": [
"preset-wooorm"
"remark-preset-wooorm"
]

@@ -94,0 +105,0 @@ },

@@ -11,4 +11,3 @@ # micromark-extension-gfm-table

**[micromark][]** extension to support GitHub flavored markdown (GFM)
[tables][].
[micromark][] extensions to support GFM [tables][].

@@ -24,2 +23,7 @@ ## Contents

* [`gfmTableHtml`](#gfmtablehtml)
* [Bugs](#bugs)
* [Authoring](#authoring)
* [HTML](#html)
* [CSS](#css)
* [Syntax](#syntax)
* [Types](#types)

@@ -34,23 +38,24 @@ * [Compatibility](#compatibility)

This package is a micromark extension to add support for GFM tables.
It matches how tables work on `github.com`.
This package contains extensions that add support for the table syntax enabled
by GFM to [`micromark`][micromark].
These extensions match github.com.
## When to use this
In many cases, when working with micromark, you’d want to use
[`micromark-extension-gfm`][micromark-extension-gfm] instead, which combines
this package with other GFM features.
This project is useful when you want to support tables in markdown.
When working with syntax trees, you’d want to combine this package with
[`mdast-util-gfm-table`][mdast-util-gfm-table] (or
[`mdast-util-gfm`][mdast-util-gfm] when using `micromark-extension-gfm`).
You can use these extensions when you are working with [`micromark`][micromark].
To support all GFM features, use
[`micromark-extension-gfm`][micromark-extension-gfm] instead.
These tools are all rather low-level.
In most cases, you’d instead want to use [`remark-gfm`][remark-gfm] with
[remark][].
When you need a syntax tree, combine this package with
[`mdast-util-gfm-table`][mdast-util-gfm-table].
All these packages are used in [`remark-gfm`][remark-gfm], which focusses on
making it easier to transform content by abstracting these internals away.
## Install
This package is [ESM only][esm].
In Node.js (version 12.20+, 14.14+, or 16.0+), install with [npm][]:
In Node.js (version 16+), install with [npm][]:

@@ -61,13 +66,13 @@ ```sh

In Deno with [Skypack][]:
In Deno with [`esm.sh`][esmsh]:
```js
import {gfmTable, gfmTableHtml} from 'https://cdn.skypack.dev/micromark-extension-gfm-table@1?dts'
import {gfmTable, gfmTableHtml} from 'https://esm.sh/micromark-extension-gfm-table@1'
```
In browsers with [Skypack][]:
In browsers with [`esm.sh`][esmsh]:
```html
<script type="module">
import {gfmTable, gfmTableHtml} from 'https://cdn.skypack.dev/micromark-extension-gfm-table@1?min'
import {gfmTable, gfmTableHtml} from 'https://esm.sh/micromark-extension-gfm-table@1?bundle'
</script>

@@ -104,7 +109,7 @@ ```

This package exports the following identifiers: `gfmTable`, `gfmTableHtml`.
This package exports the identifiers [`gfmTable`][api-gfm-table] and
[`gfmTableHtml`][api-gfm-table-html].
There is no default export.
The export map supports the endorsed
[`development` condition](https://nodejs.org/api/packages.html#packages_resolving_user_conditions).
The export map supports the [`development` condition][development].
Run `node --conditions development module.js` to get instrumented dev code.

@@ -115,19 +120,279 @@ Without this condition, production code is loaded.

An extension for micromark to parse GFM tables (can be passed in `extensions`).
Extension for `micromark` that can be passed in `extensions` to enable GFM
table syntax ([`Extension`][micromark-extension]).
### `gfmTableHtml`
An extension to compile them to HTML (can be passed in `htmlExtensions`).
Extension for `micromark` that can be passed in `htmlExtensions` to support
GFM tables when serializing to HTML
([`HtmlExtension`][micromark-html-extension]).
## Bugs
GitHub’s own algorithm to parse tables contains a bug.
This bug is not present in this project.
The issue relating to tables is:
* [GFM tables: escaped escapes are incorrectly treated as
escapes](https://github.com/github/cmark-gfm/issues/277)
## Authoring
When authoring markdown with GFM tables, it’s recommended to *always* put
pipes around cells.
Without them, it can be hard to infer whether the table will work, how many
columns there are, and which column you are currently editing.
It is recommended to not use many columns, as it results in very long lines,
making it hard to infer which column you are currently editing.
For larger tables, particularly when cells vary in size, it is recommended
*not* to manually “pad” cell text.
While it can look better, it results in a lot of time spent realigning
everything when a new, longer cell is added or the longest cell removed, as
every row then must be changed.
Other than costing time, it also causes large diffs in Git.
To illustrate, when authoring large tables, it is discouraged to pad cells
like this:
```markdown
| Alpha bravo charlie | delta |
| ------------------- | -----------------: |
| Echo | Foxtrot golf hotel |
```
Instead, use single spaces (and single filler dashes):
```markdown
| Alpha bravo charlie | delta |
| - | -: |
| Echo | Foxtrot golf hotel |
```
## HTML
GFM tables relate to several HTML elements: `<table>`, `<tbody>`, `<td>`,
`<th>`, `<thead>`, and `<tr>`.
See
[*§ 4.9.1 The `table` element*][html-table],
[*§ 4.9.5 The `tbody` element*][html-tbody],
[*§ 4.9.9 The `td` element*][html-td],
[*§ 4.9.10 The `th` element*][html-th],
[*§ 4.9.6 The `thead` element*][html-thead], and
[*§ 4.9.8 The `tr` element*][html-tr]
in the HTML spec for more info.
If the alignment of a column is left, right, or center, a deprecated
`align` attribute is added to each `<th>` and `<td>` element belonging to
that column.
That attribute is interpreted by browsers as if a CSS `text-align` property
was included, with its value set to that same keyword.
## CSS
The following CSS is needed to make tables look a bit like GitHub.
For the complete actual CSS see
[`sindresorhus/github-markdown-css`][github-markdown-css]
```css
/* Light theme. */
:root {
--color-canvas-default: #ffffff;
--color-canvas-subtle: #f6f8fa;
--color-border-default: #d0d7de;
--color-border-muted: hsla(210, 18%, 87%, 1);
}
/* Dark theme. */
@media (prefers-color-scheme: dark) {
:root {
--color-canvas-default: #0d1117;
--color-canvas-subtle: #161b22;
--color-border-default: #30363d;
--color-border-muted: #21262d;
}
}
table {
border-spacing: 0;
border-collapse: collapse;
display: block;
margin-top: 0;
margin-bottom: 16px;
width: max-content;
max-width: 100%;
overflow: auto;
}
tr {
background-color: var(--color-canvas-default);
border-top: 1px solid var(--color-border-muted);
}
tr:nth-child(2n) {
background-color: var(--color-canvas-subtle);
}
td,
th {
padding: 6px 13px;
border: 1px solid var(--color-border-default);
}
th {
font-weight: 600;
}
table img {
background-color: transparent;
}
```
## Syntax
Tables form with the following BNF:
```bnf
gfm_table ::= gfm_table_head 0*(eol gfm_table_body_row)
; Restriction: both rows must have the same number of cells.
gfm_table_head ::= gfm_table_row eol gfm_table_delimiter_row
gfm_table_row ::= ['|'] gfm_table_cell 0*('|' gfm_table_cell) ['|'] *space_or_tab
gfm_table_cell ::= *space_or_tab gfm_table_text *space_or_tab
gfm_table_text ::= 0*(line - '\\' - '|' | '\\' ['\\' | '|'])
gfm_table_delimiter_row ::= ['|'] gfm_table_delimiter_cell 0*('|' gfm_table_delimiter_cell) ['|'] *space_or_tab
gfm_table_delimiter_cell ::= *space_or_tab gfm_table_delimiter_value *space_or_tab
gfm_table_delimiter_value ::= [':'] 1*'-' [':']
```
As this construct occurs in flow, like all flow constructs, it must be
followed by an eol (line ending) or eof (end of file).
The above grammar shows that basically anything can be a cell or a row.
The main thing that makes something a row, is that it occurs directly before
or after a delimiter row, or after another row.
It is not required for a table to have a body: it can end right after the
delimiter row.
Each column can be marked with an alignment.
The alignment marker is a colon (`:`) used before and/or after delimiter row
filler.
To illustrate:
```markdown
| none | left | right | center |
| ---- | :--- | ----: | :----: |
```
The number of cells in the delimiter row, is the number of columns of the
table.
Only the head row is required to have the same number of cells.
Body rows are not required to have a certain number of cells.
For body rows that have less cells than the number of columns of the table,
empty cells are injected.
When a row has more cells than the number of columns of the table, the
superfluous cells are dropped.
To illustrate:
```markdown
| a | b |
| - | - |
| c |
| d | e | f |
```
Yields:
```html
<table>
<thead>
<tr>
<th>a</th>
<th>b</th>
</tr>
</thead>
<tbody>
<tr>
<td>c</td>
<td></td>
</tr>
<tr>
<td>d</td>
<td>e</td>
</tr>
</tbody>
</table>
```
Each cell’s text is interpreted as the [text][micromark-content-type] content
type.
That means that it can include constructs such as attention (emphasis, strong).
The grammar for cells prohibits the use of `|` in them.
To use pipes in cells, encode them as a character reference or character
escape: `&vert;` (or `&VerticalLine;`, `&verbar;`, `&#124;`, `&#x7c;`) or
`\|`.
Escapes will typically work, but they are not supported in
code (text) (and the math (text) extension).
To work around this, GitHub came up with a rather weird “trick”.
When inside a table cell *and* inside code, escaped pipes *are* decoded.
To illustrate:
```markdown
| Name | Character |
| - | - |
| Left curly brace | `{` |
| Pipe | `\|` |
| Right curly brace | `}` |
```
Yields:
```html
<table>
<thead>
<tr>
<th>Name</th>
<th>Character</th>
</tr>
</thead>
<tbody>
<tr>
<td>Left curly brace</td>
<td><code>{</code></td>
</tr>
<tr>
<td>Pipe</td>
<td><code>|</code></td>
</tr>
<tr>
<td>Right curly brace</td>
<td><code>}</code></td>
</tr>
</tbody>
</table>
```
> 👉 **Note**: no other character can be escaped like this.
> Escaping pipes in code does not work when not inside a table, either.
## Types
This package is fully typed with [TypeScript][].
There are no additional exported types.
It exports no additional types.
## Compatibility
This package is at least compatible with all maintained versions of Node.js.
As of now, that is Node.js 12.20+, 14.14+, and 16.0+.
It also works in Deno and modern browsers.
Projects maintained by the unified collective are compatible with all maintained
versions of Node.js.
As of now, that is Node.js 16+.
Our projects sometimes work with older versions, but this is not guaranteed.
These extensions work with `micromark` version 3+.
## Security

@@ -139,8 +404,10 @@

* [`syntax-tree/mdast-util-gfm-table`][mdast-util-gfm-table]
— support GFM tables in mdast
* [`syntax-tree/mdast-util-gfm`][mdast-util-gfm]
— support GFM in mdast
* [`remarkjs/remark-gfm`][remark-gfm]
— support GFM in remark
* [`micromark-extension-gfm`][micromark-extension-gfm]
— support all of GFM
* [`mdast-util-gfm-table`][mdast-util-gfm-table]
— support all of GFM in mdast
* [`mdast-util-gfm`][mdast-util-gfm]
— support all of GFM in mdast
* [`remark-gfm`][remark-gfm]
— support all of GFM in remark

@@ -191,3 +458,3 @@ ## Contribute

[skypack]: https://www.skypack.dev
[esmsh]: https://esm.sh

@@ -208,14 +475,38 @@ [license]: license

[development]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions
[micromark]: https://github.com/micromark/micromark
[remark]: https://github.com/remarkjs/remark
[micromark-extension]: https://github.com/micromark/micromark#syntaxextension
[micromark-html-extension]: https://github.com/micromark/micromark#htmlextension
[micromark-content-type]: https://github.com/micromark/micromark#content-types
[micromark-extension-gfm]: https://github.com/micromark/micromark-extension-gfm
[mdast-util-gfm]: https://github.com/syntax-tree/mdast-util-gfm
[mdast-util-gfm-table]: https://github.com/syntax-tree/mdast-util-gfm-table
[mdast-util-gfm]: https://github.com/syntax-tree/mdast-util-gfm
[remark-gfm]: https://github.com/remarkjs/remark-gfm
[tables]: https://github.github.com/gfm/#tables-extension-
[html-table]: https://html.spec.whatwg.org/multipage/tables.html#the-table-element
[html-tbody]: https://html.spec.whatwg.org/multipage/tables.html#the-tbody-element
[html-thead]: https://html.spec.whatwg.org/multipage/tables.html#the-thead-element
[html-tr]: https://html.spec.whatwg.org/multipage/tables.html#the-tr-element
[html-td]: https://html.spec.whatwg.org/multipage/tables.html#the-td-element
[html-th]: https://html.spec.whatwg.org/multipage/tables.html#the-th-element
[github-markdown-css]: https://github.com/sindresorhus/github-markdown-css
[api-gfm-table]: #gfmtable
[api-gfm-table-html]: #gfmtablehtml
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc