Saul-Mirone
2024-12-30 12:59:57 +00:00
parent e526106f45
commit c28f918527
63 changed files with 268 additions and 127 deletions
@@ -0,0 +1,13 @@
import type { ExtensionType } from '@blocksuite/block-std';
import { DatabaseBlockHtmlAdapterExtension } from './html.js';
import { DatabaseBlockMarkdownAdapterExtension } from './markdown.js';
import { DatabaseBlockNotionHtmlAdapterExtension } from './notion-html.js';
import { DatabaseBlockPlainTextAdapterExtension } from './plain-text.js';
export const DatabaseBlockAdapterExtensions: ExtensionType[] = [
DatabaseBlockHtmlAdapterExtension,
DatabaseBlockMarkdownAdapterExtension,
DatabaseBlockNotionHtmlAdapterExtension,
DatabaseBlockPlainTextAdapterExtension,
];
@@ -0,0 +1,292 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockHtmlAdapterExtension,
type BlockHtmlAdapterMatcher,
HastUtils,
type InlineHtmlAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
import { format } from 'date-fns/format';
import type { Element } from 'hast';
const DATABASE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']);
export const databaseBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o =>
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
const tableHeader = HastUtils.querySelector(o.node, 'thead');
if (!tableHeader) {
return;
}
const tableHeaderRow = HastUtils.querySelector(tableHeader, 'tr');
if (!tableHeaderRow) {
return;
}
// Table header row as database header row
const viewsColumns = tableHeaderRow.children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
// Build database cells from table body rows
const cells = Object.create(null);
const tableBody = HastUtils.querySelector(o.node, 'tbody');
tableBody?.children.forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
(row as Element).children.forEach((cell, index) => {
cells[rowId][viewsColumns[index].id] = {
columnId: viewsColumns[index].id,
value: TextUtils.createText(
(cell as Element).children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
// Build database columns from table header row
const columns = tableHeaderRow.children.map((_child, index) => {
return {
type: index === 0 ? 'title' : 'rich-text',
name: (_child as Element).children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: viewsColumns[index].id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext('affine:table:rowid', Object.keys(cells));
walkerContext.skipChildren(1);
}
// The first child of each table body row is the database title cell
if (o.node.tagName === 'tr') {
const { deltaConverter } = context;
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node.children[0]),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
if (o.node.tagName === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const createAstTableCell = (
children: InlineHtmlAST[]
): InlineHtmlAST => ({
type: 'element',
tagName: 'td',
properties: Object.create(null),
children,
});
const createAstTableHeaderCell = (
children: InlineHtmlAST[]
): InlineHtmlAST => ({
type: 'element',
tagName: 'th',
properties: Object.create(null),
children,
});
const createAstTableRow = (cells: InlineHtmlAST[]): Element => ({
type: 'element',
tagName: 'tr',
properties: Object.create(null),
children: cells,
});
const { deltaConverter } = context;
const htmlAstRows = Array.prototype.map.call(
children,
(v: BlockSnapshot) => {
const rowCells = Array.prototype.map.call(columns, col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return createAstTableCell([{ type: 'text', value: '' }]);
}
switch (col.type) {
case 'rich-text':
return createAstTableCell(
deltaConverter.deltaToAST(
(cell.value as { delta: DeltaInsert[] }).delta
)
);
case 'title':
return createAstTableCell(
deltaConverter.deltaToAST(
(v.props.text as { delta: DeltaInsert[] }).delta
)
);
case 'date':
return createAstTableCell([
{
type: 'text',
value: format(new Date(cell.value as number), 'yyyy-MM-dd'),
},
]);
case 'select': {
const value =
(col.data.options.find(
(opt: Record<string, string>) => opt.id === cell.value
)?.value as string) ?? '';
return createAstTableCell([{ type: 'text', value }]);
}
case 'multi-select': {
const value = Array.prototype.map
.call(
cell.value,
val =>
col.data.options.find(
(opt: Record<string, string>) => val === opt.id
).value ?? ''
)
.filter(Boolean)
.join(',');
return createAstTableCell([{ type: 'text', value }]);
}
case 'checkbox': {
return createAstTableCell([
{ type: 'text', value: String(cell.value) },
]);
}
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return createAstTableCell([
{ type: 'text', value: String(cell.value) },
]);
}
}) as InlineHtmlAST[];
return createAstTableRow(rowCells);
}
) as Element[];
// Handle first row (header).
const headerRow = createAstTableRow(
Array.prototype.map.call(columns, v =>
createAstTableHeaderCell([
{
type: 'text',
value: v.name ?? '',
},
])
) as Element[]
);
const tableHeaderAst: Element = {
type: 'element',
tagName: 'thead',
properties: Object.create(null),
children: [headerRow],
};
const tableBodyAst: Element = {
type: 'element',
tagName: 'tbody',
properties: Object.create(null),
children: [...htmlAstRows],
};
walkerContext
.openNode({
type: 'element',
tagName: 'table',
properties: Object.create(null),
children: [tableHeaderAst, tableBodyAst],
})
.closeNode();
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
databaseBlockHtmlAdapterMatcher
);
@@ -0,0 +1,4 @@
export * from './html';
export * from './markdown';
export * from './notion-html';
export * from './plain-text';
@@ -0,0 +1,253 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockMarkdownAdapterExtension,
type BlockMarkdownAdapterMatcher,
type MarkdownAST,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
import { format } from 'date-fns/format';
import type { TableRow } from 'mdast';
const DATABASE_NODE_TYPES = new Set(['table', 'tableRow']);
const isDatabaseNode = (node: MarkdownAST) =>
DATABASE_NODE_TYPES.has(node.type);
export const databaseBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o => isDatabaseNode(o.node),
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {
enter: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
const viewsColumns = o.node.children[0].children.map(() => {
return {
id: nanoid(),
hide: false,
width: 180,
};
});
const cells = Object.create(null);
o.node.children.slice(1).forEach(row => {
const rowId = nanoid();
cells[rowId] = Object.create(null);
row.children.slice(1).forEach((cell, index) => {
cells[rowId][viewsColumns[index + 1].id] = {
columnId: viewsColumns[index + 1].id,
value: TextUtils.createText(
cell.children
.map(child => ('value' in child ? child.value : ''))
.join('')
),
};
});
});
const columns = o.node.children[0].children.map((_child, index) => {
return {
type: index === 0 ? 'title' : 'rich-text',
name: _child.children
.map(child => ('value' in child ? child.value : ''))
.join(''),
data: {},
id: viewsColumns[index].id,
};
});
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: 'affine:database',
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn: viewsColumns[0]?.id,
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: [],
},
cells,
columns,
},
children: [],
},
'children'
);
walkerContext.setNodeContext(
'affine:table:rowid',
Object.keys(cells)
);
walkerContext.skipChildren(1);
}
if (o.node.type === 'tableRow') {
const { deltaConverter } = context;
walkerContext
.openNode({
type: 'block',
id:
(
walkerContext.getNodeContext(
'affine:table:rowid'
) as Array<string>
).shift() ?? nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(o.node.children[0]),
},
type: 'text',
},
children: [],
})
.closeNode();
walkerContext.skipAllChildren();
}
},
leave: (o, context) => {
const { walkerContext } = context;
if (o.node.type === 'table') {
walkerContext.closeNode();
}
},
},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
const rows: TableRow[] = [];
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const createAstCell = (children: MarkdownAST[]) => ({
type: 'tableCell',
children,
});
const mdAstCells = Array.prototype.map.call(
children,
(v: BlockSnapshot) =>
Array.prototype.map.call(columns, col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return createAstCell([{ type: 'text', value: '' }]);
}
switch (col.type) {
case 'link':
case 'progress':
case 'number':
return createAstCell([
{
type: 'text',
value: cell.value as string,
},
]);
case 'rich-text':
return createAstCell(
deltaConverter.deltaToAST(
(cell.value as { delta: DeltaInsert[] }).delta
)
);
case 'title':
return createAstCell(
deltaConverter.deltaToAST(
(v.props.text as { delta: DeltaInsert[] }).delta
)
);
case 'date':
return createAstCell([
{
type: 'text',
value: format(
new Date(cell.value as number),
'yyyy-MM-dd'
),
},
]);
case 'select': {
const value = col.data.options.find(
(opt: Record<string, string>) => opt.id === cell.value
)?.value;
return createAstCell([{ type: 'text', value }]);
}
case 'multi-select': {
const value = Array.prototype.map
.call(
cell.value,
val =>
col.data.options.find(
(opt: Record<string, string>) => val === opt.id
).value
)
.filter(Boolean)
.join(',');
return createAstCell([{ type: 'text', value }]);
}
case 'checkbox': {
return createAstCell([
{ type: 'text', value: cell.value as string },
]);
}
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return createAstCell([
{ type: 'text', value: cell.value as string },
]);
}
})
);
// Handle first row.
if (Array.isArray(columns)) {
rows.push({
type: 'tableRow',
children: Array.prototype.map.call(columns, v =>
createAstCell([
{
type: 'text',
value: v.name,
},
])
) as [],
});
}
// Handle 2-... rows
Array.prototype.forEach.call(mdAstCells, children => {
rows.push({ type: 'tableRow', children });
});
walkerContext
.openNode({
type: 'table',
children: rows,
})
.closeNode();
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockMarkdownAdapterExtension =
BlockMarkdownAdapterExtension(databaseBlockMarkdownAdapterMatcher);
@@ -0,0 +1,348 @@
import { DatabaseBlockSchema } from '@blocksuite/affine-model';
import {
BlockNotionHtmlAdapterExtension,
type BlockNotionHtmlAdapterMatcher,
HastUtils,
TextUtils,
} from '@blocksuite/affine-shared/adapters';
import { getTagColor } from '@blocksuite/data-view';
import { type BlockSnapshot, nanoid } from '@blocksuite/store';
const ColumnClassMap: Record<string, string> = {
typesSelect: 'select',
typesMultipleSelect: 'multi-select',
typesNumber: 'number',
typesCheckbox: 'checkbox',
typesText: 'rich-text',
typesTitle: 'title',
};
const NotionDatabaseToken = '.collection-content';
const NotionDatabaseTitleToken = '.collection-title';
type BlocksuiteTableColumn = {
type: string;
name: string;
data: {
options?: {
id: string;
value: string;
color: string;
}[];
};
id: string;
};
type BlocksuiteTableRow = Record<
string,
{
columnId: string;
value: unknown;
}
>;
const DATABASE_NODE_TYPES = new Set(['table', 'th', 'tr']);
export const databaseBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: o =>
HastUtils.isElement(o.node) && DATABASE_NODE_TYPES.has(o.node.tagName),
fromMatch: () => false,
toBlockSnapshot: {
enter: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext, deltaConverter, pageMap } = context;
switch (o.node.tagName) {
case 'th': {
const columnId = nanoid();
const columnTypeClass = HastUtils.querySelector(o.node, 'svg')
?.properties?.className;
const columnType = Array.isArray(columnTypeClass)
? (ColumnClassMap[columnTypeClass[0]] ?? 'rich-text')
: 'rich-text';
walkerContext.pushGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column',
{
type: columnType,
name: HastUtils.getTextContent(
HastUtils.getTextChildrenOnlyAst(o.node)
),
data: Object.create(null),
id: columnId,
}
);
// disable icon img in th
walkerContext.setGlobalContext('hast:disableimg', true);
break;
}
case 'tr': {
if (
o.parent?.node.type === 'element' &&
o.parent.node.tagName === 'tbody'
) {
const columns =
walkerContext.getGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column'
);
const row = Object.create(null);
let plainTable = false;
HastUtils.getElementChildren(o.node).forEach((child, index) => {
if (plainTable || columns[index] === undefined) {
plainTable = true;
if (columns[index] === undefined) {
columns.push({
type: 'rich-text',
name: '',
data: Object.create(null),
id: nanoid(),
});
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child),
},
type: 'text',
},
children: [],
}
);
}
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child),
},
type: 'text',
},
children: [],
}
);
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.getTextContent(child),
};
} else if (HastUtils.querySelector(child, '.cell-title')) {
walkerContext.pushGlobalContextStack<BlockSnapshot>(
'hast:table:children',
{
type: 'block',
id: nanoid(),
flavour: 'affine:paragraph',
props: {
text: {
'$blocksuite:internal:text$': true,
delta: deltaConverter.astToDelta(child, { pageMap }),
},
type: 'text',
},
children: [],
}
);
columns[index].type = 'title';
return;
}
const optionIds: string[] = [];
if (HastUtils.querySelector(child, '.selected-value')) {
if (!('options' in columns[index].data)) {
columns[index].data.options = [];
}
if (
!['multi-select', 'select'].includes(columns[index].type)
) {
columns[index].type = 'select';
}
if (
columns[index].type === 'select' &&
child.type === 'element' &&
child.children.length > 1
) {
columns[index].type = 'multi-select';
}
child.type === 'element' &&
child.children.forEach(span => {
const filteredArray = columns[index].data.options?.filter(
option =>
option.value === HastUtils.getTextContent(span)
);
const id = filteredArray?.length
? filteredArray[0].id
: nanoid();
if (!filteredArray?.length) {
columns[index].data.options?.push({
id,
value: HastUtils.getTextContent(span),
color: getTagColor(),
});
}
optionIds.push(id);
});
// Expand will be done when leaving the table
row[columns[index].id] = {
columnId: columns[index].id,
value: optionIds,
};
} else if (HastUtils.querySelector(child, '.checkbox')) {
if (columns[index].type !== 'checkbox') {
columns[index].type = 'checkbox';
}
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.querySelector(child, '.checkbox-on')
? true
: false,
};
} else if (columns[index].type === 'number') {
const text = HastUtils.getTextContent(child);
const number = Number(text);
if (Number.isNaN(number)) {
columns[index].type = 'rich-text';
row[columns[index].id] = {
columnId: columns[index].id,
value: TextUtils.createText(text),
};
} else {
row[columns[index].id] = {
columnId: columns[index].id,
value: number,
};
}
} else {
row[columns[index].id] = {
columnId: columns[index].id,
value: HastUtils.getTextContent(child),
};
}
if (
columns[index].type === 'rich-text' &&
!TextUtils.isText(row[columns[index].id].value)
) {
row[columns[index].id] = {
columnId: columns[index].id,
value: TextUtils.createText(row[columns[index].id].value),
};
}
});
walkerContext.setGlobalContextStack('hast:table:column', columns);
walkerContext.pushGlobalContextStack('hast:table:rows', row);
}
}
}
},
leave: (o, context) => {
if (!HastUtils.isElement(o.node)) {
return;
}
const { walkerContext } = context;
switch (o.node.tagName) {
case 'table': {
const columns =
walkerContext.getGlobalContextStack<BlocksuiteTableColumn>(
'hast:table:column'
);
walkerContext.setGlobalContextStack('hast:table:column', []);
const children = walkerContext.getGlobalContextStack<BlockSnapshot>(
'hast:table:children'
);
walkerContext.setGlobalContextStack('hast:table:children', []);
const cells = Object.create(null);
walkerContext
.getGlobalContextStack<BlocksuiteTableRow>('hast:table:rows')
.forEach((row, i) => {
Object.keys(row).forEach(columnId => {
if (
columns.find(column => column.id === columnId)?.type ===
'select'
) {
row[columnId].value = (row[columnId].value as string[])[0];
}
});
cells[children.at(i)?.id ?? nanoid()] = row;
});
walkerContext.setGlobalContextStack('hast:table:cells', []);
let databaseTitle = '';
if (
o.parent?.node.type === 'element' &&
HastUtils.querySelector(o.parent.node, NotionDatabaseToken)
) {
databaseTitle = HastUtils.getTextContent(
HastUtils.querySelector(o.parent.node, NotionDatabaseTitleToken)
);
}
walkerContext.openNode(
{
type: 'block',
id: nanoid(),
flavour: DatabaseBlockSchema.model.flavour,
props: {
views: [
{
id: nanoid(),
name: 'Table View',
mode: 'table',
columns: [],
filter: {
type: 'group',
op: 'and',
conditions: [],
},
header: {
titleColumn:
columns.find(column => column.type === 'title')?.id ??
'',
iconColumn: 'type',
},
},
],
title: {
'$blocksuite:internal:text$': true,
delta: databaseTitle
? [
{
insert: databaseTitle,
},
]
: [],
},
columns,
cells,
},
children: [],
},
'children'
);
children.forEach(child => {
walkerContext.openNode(child, 'children').closeNode();
});
walkerContext.closeNode();
walkerContext.cleanGlobalContextStack('hast:table:column');
walkerContext.cleanGlobalContextStack('hast:table:rows');
walkerContext.cleanGlobalContextStack('hast:table:children');
break;
}
case 'th': {
walkerContext.setGlobalContext('hast:disableimg', false);
break;
}
}
},
},
fromBlockSnapshot: {},
};
export const DatabaseBlockNotionHtmlAdapterExtension =
BlockNotionHtmlAdapterExtension(databaseBlockNotionHtmlAdapterMatcher);
@@ -0,0 +1,92 @@
import {
type Column,
DatabaseBlockSchema,
type SerializedCells,
} from '@blocksuite/affine-model';
import {
BlockPlainTextAdapterExtension,
type BlockPlainTextAdapterMatcher,
} from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import type { BlockSnapshot } from '@blocksuite/store';
import { format } from 'date-fns/format';
import { formatTable } from './utils.js';
export const databaseBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher =
{
flavour: DatabaseBlockSchema.model.flavour,
toMatch: () => false,
fromMatch: o => o.node.flavour === DatabaseBlockSchema.model.flavour,
toBlockSnapshot: {},
fromBlockSnapshot: {
enter: (o, context) => {
const { walkerContext, deltaConverter } = context;
const rows: string[][] = [];
const columns = o.node.props.columns as Array<Column>;
const children = o.node.children;
const cells = o.node.props.cells as SerializedCells;
const tableCells = children.map((v: BlockSnapshot) =>
columns.map(col => {
const cell = cells[v.id]?.[col.id];
if (!cell && col.type !== 'title') {
return '';
}
switch (col.type) {
case 'rich-text':
return deltaConverter
.deltaToAST((cell.value as { delta: DeltaInsert[] }).delta)
.join('');
case 'title':
return deltaConverter
.deltaToAST((v.props.text as { delta: DeltaInsert[] }).delta)
.join('');
case 'date':
return format(new Date(cell.value as number), 'yyyy-MM-dd');
case 'select': {
const value = (
col.data as { options: Array<Record<string, string>> }
).options.find(opt => opt.id === cell.value)?.value;
return value || '';
}
case 'multi-select': {
const value = (cell.value as string[])
.map(
val =>
(
col.data as { options: Array<Record<string, string>> }
).options.find(opt => val === opt.id)?.value
)
.filter(Boolean)
.join(',');
return value || '';
}
default:
return String(cell.value);
}
})
);
// Handle first row.
if (Array.isArray(columns)) {
rows.push(columns.map(col => col.name));
}
// Handle 2-... rows
tableCells.forEach(children => {
rows.push(children);
});
// Convert rows to table string
const tableString = formatTable(rows);
context.textBuffer.content += tableString;
context.textBuffer.content += '\n';
walkerContext.skipAllChildren();
},
},
};
export const DatabaseBlockPlainTextAdapterExtension =
BlockPlainTextAdapterExtension(databaseBlockPlainTextAdapterMatcher);
@@ -0,0 +1,32 @@
function calculateColumnWidths(rows: string[][]): number[] {
return rows[0].map((_, colIndex) =>
Math.max(...rows.map(row => (row[colIndex] || '').length))
);
}
function formatRow(
row: string[],
columnWidths: number[],
isHeader: boolean
): string {
const cells = row.map((cell, colIndex) =>
cell.padEnd(columnWidths[colIndex], ' ')
);
const rowString = `| ${cells.join(' | ')} |`;
return isHeader
? `${rowString}\n${formatSeparator(columnWidths)}`
: rowString;
}
function formatSeparator(columnWidths: number[]): string {
const separator = columnWidths.map(width => '-'.repeat(width)).join(' | ');
return `| ${separator} |`;
}
export function formatTable(rows: string[][]): string {
const columnWidths = calculateColumnWidths(rows);
const formattedRows = rows.map((row, index) =>
formatRow(row, columnWidths, index === 0)
);
return formattedRows.join('\n');
}