From 68e5dbc523dd1f119230650e6096bd946f6e4807 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Wed, 18 Oct 2023 22:11:30 +0530 Subject: [PATCH] chore: add table markdown transformer to super --- .../SuperEditor/MarkdownTransformers.ts | 166 +++++++++++++++++- 1 file changed, 164 insertions(+), 2 deletions(-) diff --git a/packages/web/src/javascripts/Components/SuperEditor/MarkdownTransformers.ts b/packages/web/src/javascripts/Components/SuperEditor/MarkdownTransformers.ts index 66d10e698..0d3d4c0ba 100644 --- a/packages/web/src/javascripts/Components/SuperEditor/MarkdownTransformers.ts +++ b/packages/web/src/javascripts/Components/SuperEditor/MarkdownTransformers.ts @@ -5,14 +5,27 @@ import { TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS, TextMatchTransformer, + $convertToMarkdownString, + $convertFromMarkdownString, } from '@lexical/markdown' - +import { + $createTableCellNode, + $createTableNode, + $createTableRowNode, + $isTableCellNode, + $isTableNode, + $isTableRowNode, + TableCellHeaderStates, + TableCellNode, + TableNode, + TableRowNode, +} from '@lexical/table' import { HorizontalRuleNode, $createHorizontalRuleNode, $isHorizontalRuleNode, } from '@lexical/react/LexicalHorizontalRuleNode' -import { LexicalNode } from 'lexical' +import { $isParagraphNode, $isTextNode, LexicalNode } from 'lexical' import { $createRemoteImageNode, $isRemoteImageNode, @@ -59,7 +72,156 @@ const IMAGE: TextMatchTransformer = { type: 'text-match', } +// Table transformer, taken from Lexical Playground +const TABLE_ROW_REG_EXP = /^(?:\|)(.+)(?:\|)\s?$/ +const TABLE_ROW_DIVIDER_REG_EXP = /^(\| ?:?-*:? ?)+\|\s?$/ + +function getTableColumnsSize(table: TableNode) { + const row = table.getFirstChild() + return $isTableRowNode(row) ? row.getChildrenSize() : 0 +} + +function createTableCell(textContent: string): TableCellNode { + textContent = textContent.replace(/\\n/g, '\n') + const cell = $createTableCellNode(TableCellHeaderStates.NO_STATUS) + $convertFromMarkdownString(textContent, MarkdownTransformers, cell) + return cell +} + +function mapToTableCells(textContent: string): Array | null { + const match = textContent.match(TABLE_ROW_REG_EXP) + if (!match || !match[1]) { + return null + } + return match[1].split('|').map((text) => createTableCell(text)) +} + +export const TABLE: ElementTransformer = { + dependencies: [TableNode, TableRowNode, TableCellNode], + export: (node: LexicalNode) => { + if (!$isTableNode(node)) { + return null + } + + const output: string[] = [] + + for (const row of node.getChildren()) { + const rowOutput = [] + if (!$isTableRowNode(row)) { + continue + } + + let isHeaderRow = false + for (const cell of row.getChildren()) { + // It's TableCellNode so it's just to make flow happy + if ($isTableCellNode(cell)) { + rowOutput.push($convertToMarkdownString(MarkdownTransformers, cell).replace(/\n/g, '\\n')) + if (cell.__headerState === TableCellHeaderStates.ROW) { + isHeaderRow = true + } + } + } + + output.push(`| ${rowOutput.join(' | ')} |`) + if (isHeaderRow) { + output.push(`| ${rowOutput.map((_) => '---').join(' | ')} |`) + } + } + + return output.join('\n') + }, + regExp: TABLE_ROW_REG_EXP, + replace: (parentNode, _1, match) => { + // Header row + if (TABLE_ROW_DIVIDER_REG_EXP.test(match[0])) { + const table = parentNode.getPreviousSibling() + if (!table || !$isTableNode(table)) { + return + } + + const rows = table.getChildren() + const lastRow = rows[rows.length - 1] + if (!lastRow || !$isTableRowNode(lastRow)) { + return + } + + // Add header state to row cells + lastRow.getChildren().forEach((cell) => { + if (!$isTableCellNode(cell)) { + return + } + cell.toggleHeaderStyle(TableCellHeaderStates.ROW) + }) + + // Remove line + parentNode.remove() + return + } + + const matchCells = mapToTableCells(match[0]) + + if (matchCells == null) { + return + } + + const rows = [matchCells] + let sibling = parentNode.getPreviousSibling() + let maxCells = matchCells.length + + while (sibling) { + if (!$isParagraphNode(sibling)) { + break + } + + if (sibling.getChildrenSize() !== 1) { + break + } + + const firstChild = sibling.getFirstChild() + + if (!$isTextNode(firstChild)) { + break + } + + const cells = mapToTableCells(firstChild.getTextContent()) + + if (cells == null) { + break + } + + maxCells = Math.max(maxCells, cells.length) + rows.unshift(cells) + const previousSibling = sibling.getPreviousSibling() + sibling.remove() + sibling = previousSibling + } + + const table = $createTableNode() + + for (const cells of rows) { + const tableRow = $createTableRowNode() + table.append(tableRow) + + for (let i = 0; i < maxCells; i++) { + tableRow.append(i < cells.length ? cells[i] : createTableCell('')) + } + } + + const previousSibling = parentNode.getPreviousSibling() + if ($isTableNode(previousSibling) && getTableColumnsSize(previousSibling) === maxCells) { + previousSibling.append(...table.getChildren()) + parentNode.remove() + } else { + parentNode.replace(table) + } + + table.selectEnd() + }, + type: 'element', +} + export const MarkdownTransformers = [ + TABLE, CHECK_LIST, IMAGE, ...ELEMENT_TRANSFORMERS,