import { Token } from '../types.js'; import type { ContextStash, TokenHandler, } from '../MarkdownSerializer.js'; import { getHtmlInlineTokensHandlers } from './inline_token_handlers.js'; import { TokenSource } from '../TokenSource.js'; import { getHtmlBasicTokensHandlers } from './basic_token_handlers.js'; import { getHtmlListsTokensHandlers } from './lists_token_handlers.js'; export type TextAlign = 'left' | 'right'; function getHtmlTableTokensHandlers(): Record> { return { ...getHtmlInlineTokensHandlers(), ...getHtmlBasicTokensHandlers(), // ...getHtmlFootnoteTokensHandlers(), ...getHtmlListsTokensHandlers(), 'paragraph_open': [ (token: Token, ctx: ContextStash) => { if (token.attrGet('not_first_para')) { ctx.current.log('
\n', token); } }, ], 'paragraph_close': [], 'table_open': [ (token: Token, ctx: ContextStash) => { ctx.stash('getHtmlTableTokensHandlers.table_open'); ctx.current.meta['html_mode'] = true; ctx.current.log('\n', token); }, ], 'table_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('
\n', token); ctx.unstash('getHtmlTableTokensHandlers.table_close'); }, ], 'thead_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'thead_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'tr_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'tr_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'th_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log('', token); }, ], 'th_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'tbody_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'tbody_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], 'td_open': [ (token: Token, ctx: ContextStash) => { ctx.current.log('', token); }, ], 'td_close': [ (token: Token, ctx: ContextStash) => { ctx.current.log('\n', token); }, ], }; } interface TableCell { value: [string, Token][]; align: 'left' | 'right'; startPos?: number; } interface TableRow { type: 'header' | 'body'; cells: Array; } class TableBuilder { private rows: Array; public currentType: 'header' | 'body'; constructor() { this.currentType = 'body'; this.rows = []; } appendRow() { this.rows.push({ type: this.currentType, cells: [], }); } changeRowType(type: 'header' | 'body') { if (this.rows.length === 0) { return; } this.rows[this.rows.length - 1].type = type; } appendCell(align: 'left' | 'right', token: Token) { const lastRow = this.rows[this.rows.length - 1]; const startPos = token.map && token.map.length > 0 ? token.map[0] : 0; lastRow.cells.push({ value: [], startPos, align, }); } appendText(content: string, token: Token) { const lastRow = this.rows[this.rows.length - 1]; if (lastRow.cells.length > 0) { lastRow.cells[lastRow.cells.length - 1].value.push([content, token]); } } render(log: (txt: string, token?: Token) => void) { let prevType = ''; let lastHeader: Array = []; const columnsWidth: Array = []; for (let rowNo = 0; rowNo < this.rows.length; rowNo++) { const row = this.rows[rowNo]; for (let cellNo = 0; cellNo < row.cells.length; cellNo++) { while (columnsWidth.length <= cellNo) { columnsWidth.push(1); } const headCell = lastHeader[cellNo]; const cell = row.cells[cellNo]; const textLen: number = cell.value.reduce( (prev, current) => prev + current[0].length, 0, ); if (columnsWidth[cellNo] < textLen + 1) { columnsWidth[cellNo] = textLen + 1; } } } for (let rowNo = 0; rowNo < this.rows.length; rowNo++) { const row = this.rows[rowNo]; if (prevType === 'header' && prevType !== row.type) { log('|'); for (let cellNo = 0; cellNo < lastHeader.length; cellNo++) { const cell = lastHeader[cellNo]; log(' '); if (cell.align === 'right') { log('-'.repeat(columnsWidth[cellNo] - 2)); log(': |'); } else { log('-'.repeat(columnsWidth[cellNo] - 1)); log(' |'); } } log('\n'); } log('|'); for (let cellNo = 0; cellNo < row.cells.length; cellNo++) { const cell = row.cells[cellNo]; const textLen: number = cell.value.reduce( (prev, current) => prev + current[0].length, 0, ); if (cell.align === 'right') { if (textLen === 0 && cell.startPos) { log( ' '.repeat(columnsWidth[cellNo] - textLen), { map: [cell.startPos + 2] } as Token, ); } else { log(' '.repeat(columnsWidth[cellNo] - textLen)); } } else { log(' '.repeat(1)); } for (const pair of cell.value) { log(pair[0], pair[1]); } if (textLen < columnsWidth[cellNo]) { if (cell.align === 'right') { log(' '.repeat(1)); } else { if (textLen === 0 && cell.startPos) { log( ' '.repeat(columnsWidth[cellNo] - textLen), { map: [cell.startPos + 2] } as Token, ); } else { log(' '.repeat(columnsWidth[cellNo] - textLen)); } } } log('|'); } log('\n'); if (row.type === 'header') { lastHeader = row.cells; } prevType = row.type; } } } export interface RollbackData { sourcePos: number; outputPos: number; ctxDepth: number; } function getMdTableTokensHandler(): Record> { const rollbackMdTable = ( token: Token, ctx: ContextStash, tokenSource: TokenSource, ) => { if (!ctx.current.meta['html_rollback']) { throw new Error('No html_rollback'); } const rollback: RollbackData = ctx.current.meta['html_rollback']; tokenSource.rewind(rollback.sourcePos); ctx.rollback(rollback.ctxDepth, 'rollbackMdTable, ' + token.type); ctx.output.rollback(rollback.outputPos); ctx.stash('getMdTableTokensHandler.rollbackTable'); ctx.current.meta['html_mode'] = true; ctx.current.log('\n', token); ctx.current.handlers = getHtmlTableTokensHandlers(); }; return { 'text': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.appendText(token.content, token); }, ], 'paragraph_open': [], 'paragraph_close': [ (token: Token, ctx: ContextStash, tokenSource: TokenSource) => { ctx.current.meta['table_cell_para_count'] = +ctx.current.meta['table_cell_para_count'] + 1; if (ctx.current.meta['table_cell_para_count'] > 1) { // Only 1 line in markdown pipe table rollbackMdTable(token, ctx, tokenSource); } }, ], 'default': [ rollbackMdTable, ], 'table_open': [ (token: Token, ctx: ContextStash, tokenSource: TokenSource) => { const rollbackDepth = ctx.stash('getMdTableTokensHandler.table_open'); if (!ctx.current.meta['html_rollback']) { const rollback: RollbackData = { sourcePos: tokenSource.pos, outputPos: ctx.output.chunkPos, ctxDepth: rollbackDepth, }; ctx.current.meta['html_rollback'] = rollback; } ctx.current.metaObj['table_builder'] = new TableBuilder(); ctx.current.handlers = getMdTableTokensHandler(); }, ], 'table_close': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.render(ctx.current.log); ctx.unstash('getMdTableTokensHandler.table_close'); }, ], 'thead_open': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.currentType = 'header'; }, ], 'thead_close': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.currentType = 'body'; }, ], 'tr_open': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.appendRow(); }, ], 'tr_close': [ (token: Token, ctx: ContextStash) => { }, ], 'th_open': [ (token: Token, ctx: ContextStash) => { // const style = token.attrGet('style'); // const align = style === 'text-align:right' ? 'right' : 'left'; const align = token.attrGet('align') || 'left'; const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.appendCell(align as TextAlign, token); tableBuilder.changeRowType('header'); ctx.current.meta['table_cell_para_count'] = 0; }, ], 'th_close': [ (token: Token, ctx: ContextStash) => { }, ], 'tbody_open': [ (token: Token, ctx: ContextStash) => { const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.currentType = 'body'; }, ], 'tbody_close': [ (token: Token, ctx: ContextStash) => { }, ], 'td_open': [ (token: Token, ctx: ContextStash) => { const align = token.attrGet('align') || 'left'; const tableBuilder: TableBuilder = ctx.current.metaObj['table_builder']; tableBuilder.appendCell(align as TextAlign, token); ctx.current.meta['table_cell_para_count'] = 0; }, ], 'td_close': [ (token: Token, ctx: ContextStash) => { }, ], }; } export function getTableTokensHandlers(): Record> { return { 'table_open': getMdTableTokensHandler()['table_open'], }; }