/* * Copyright (C) 2026 Fluxer Contributors * * This file is part of Fluxer. * * Fluxer is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Fluxer is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Fluxer. If not, see . */ import {NodeType, TableAlignment} from '../types/enums'; import type {Node, TableCellNode, TableNode, TableRowNode} from '../types/nodes'; interface TableParseResult { node: TableNode | null; newLineIndex: number; } const PIPE = 124; const SPACE = 32; const BACKSLASH = 92; const DASH = 45; const COLON = 58; const HASH = 35; const GREATER_THAN = 62; const ASTERISK = 42; const DIGIT_0 = 48; const DIGIT_9 = 57; const PERIOD = 46; const MAX_CACHE_SIZE = 1000; const inlineContentCache = new Map>(); export function parseTable( lines: Array, currentLineIndex: number, _parserFlags: number, parseInline: (text: string) => Array, ): TableParseResult { const startIndex = currentLineIndex; if (startIndex + 2 >= lines.length) { return {node: null, newLineIndex: currentLineIndex}; } const headerLine = lines[currentLineIndex]; const alignmentLine = lines[currentLineIndex + 1]; if (!containsPipe(headerLine) || !containsPipe(alignmentLine)) { return {node: null, newLineIndex: currentLineIndex}; } try { const headerCells = fastSplitTableCells(headerLine.trim()); if (headerCells.length === 0 || !hasContent(headerCells)) { return {node: null, newLineIndex: currentLineIndex}; } const headerRow = createTableRow(headerCells, parseInline); const columnCount = headerRow.cells.length; currentLineIndex++; const alignmentCells = fastSplitTableCells(alignmentLine.trim()); if (!validateAlignmentRow(alignmentCells)) { return {node: null, newLineIndex: startIndex}; } const alignments = parseAlignments(alignmentCells); if (!alignments || headerRow.cells.length !== alignments.length) { return {node: null, newLineIndex: startIndex}; } currentLineIndex++; const rows: Array = []; while (currentLineIndex < lines.length) { const line = lines[currentLineIndex]; if (!containsPipe(line)) break; const trimmed = line.trim(); if (isBlockBreakFast(trimmed)) break; const cellContents = fastSplitTableCells(trimmed); if (cellContents.length !== columnCount) { normalizeColumnCount(cellContents, columnCount); } const row = createTableRow(cellContents, parseInline); rows.push(row); currentLineIndex++; } if (rows.length === 0) { return {node: null, newLineIndex: startIndex}; } let hasAnyContent = hasRowContent(headerRow); if (!hasAnyContent) { for (const row of rows) { if (hasRowContent(row)) { hasAnyContent = true; break; } } } if (!hasAnyContent) { return {node: null, newLineIndex: startIndex}; } if (inlineContentCache.size > MAX_CACHE_SIZE) { inlineContentCache.clear(); } return { node: { type: NodeType.Table, header: headerRow, alignments: alignments, rows, }, newLineIndex: currentLineIndex, }; } catch (_err) { return {node: null, newLineIndex: startIndex}; } } function containsPipe(text: string): boolean { return text.indexOf('|') !== -1; } function hasContent(cells: Array): boolean { for (const cell of cells) { if (cell.trim().length > 0) { return true; } } return false; } function hasRowContent(row: TableRowNode): boolean { for (const cell of row.cells) { if ( cell.children.length > 0 && !(cell.children.length === 1 && cell.children[0].type === NodeType.Text && cell.children[0].content.trim() === '') ) { return true; } } return false; } function validateAlignmentRow(cells: Array): boolean { if (cells.length === 0) return false; for (const cell of cells) { const trimmed = cell.trim(); if (trimmed.length === 0 || trimmed.indexOf('-') === -1) { return false; } for (let i = 0; i < trimmed.length; i++) { const charCode = trimmed.charCodeAt(i); if (charCode !== SPACE && charCode !== COLON && charCode !== DASH && charCode !== PIPE) { return false; } } } return true; } function fastSplitTableCells(line: string): Array { let start = 0; let end = line.length; if (line.length > 0 && line.charCodeAt(0) === PIPE) { start = 1; } if (line.length > 0 && end > start && line.charCodeAt(end - 1) === PIPE) { end--; } if (start >= end) { return []; } const content = line.substring(start, end); const cells: Array = []; let currentCell = ''; let i = 0; while (i < content.length) { if (content.charCodeAt(i) === BACKSLASH && i + 1 < content.length && content.charCodeAt(i + 1) === PIPE) { currentCell += '|'; i += 2; continue; } if (content.charCodeAt(i) === PIPE) { cells.push(currentCell); currentCell = ''; i++; continue; } currentCell += content[i]; i++; } cells.push(currentCell); return cells; } function parseAlignments(cells: Array): Array | null { if (cells.length === 0) return null; const alignments: Array = []; for (const cell of cells) { const trimmed = cell.trim(); if (!trimmed || trimmed.indexOf('-') === -1) return null; const left = trimmed.charCodeAt(0) === COLON; const right = trimmed.charCodeAt(trimmed.length - 1) === COLON; if (left && right) { alignments.push(TableAlignment.Center); } else if (left) { alignments.push(TableAlignment.Left); } else if (right) { alignments.push(TableAlignment.Right); } else { alignments.push(TableAlignment.None); } } return alignments; } function createTableRow(cellContents: Array, parseInline: (text: string) => Array): TableRowNode { const cells: Array = []; for (const cellContent of cellContents) { const trimmed = cellContent.trim(); let inlineNodes: Array; if (inlineContentCache.has(trimmed)) { inlineNodes = inlineContentCache.get(trimmed)!; } else { inlineNodes = parseInline(trimmed); inlineContentCache.set(trimmed, inlineNodes); } cells.push({ type: NodeType.TableCell, children: inlineNodes.length > 0 ? inlineNodes : [{type: NodeType.Text, content: trimmed}], }); } return {type: NodeType.TableRow, cells}; } function normalizeColumnCount(cells: Array, expectedColumns: number): void { if (cells.length > expectedColumns) { const lastCellIndex = expectedColumns - 1; cells[lastCellIndex] = `${cells[lastCellIndex]}|${cells.slice(expectedColumns).join('|')}`; cells.length = expectedColumns; } else { while (cells.length < expectedColumns) { cells.push(''); } } } function isBlockBreakFast(text: string): boolean { if (!text || text.length === 0) return false; const firstChar = text.charCodeAt(0); if (firstChar === HASH || firstChar === GREATER_THAN || firstChar === DASH || firstChar === ASTERISK) { return true; } if ( text.length >= 4 && text.charCodeAt(0) === GREATER_THAN && text.charCodeAt(1) === GREATER_THAN && text.charCodeAt(2) === GREATER_THAN && text.charCodeAt(3) === SPACE ) { return true; } if (text.length >= 2 && text.charCodeAt(0) === DASH && text.charCodeAt(1) === HASH) { return true; } if (firstChar >= DIGIT_0 && firstChar <= DIGIT_9) { for (let i = 1; i < Math.min(text.length, 4); i++) { if (text.charCodeAt(i) === PERIOD) { return true; } } } return false; }