mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-02 18:09:58 +08:00
refactor(editor): unify directories naming (#11516)
**Directory Structure Changes** - Renamed multiple block-related directories by removing the "block-" prefix: - `block-attachment` → `attachment` - `block-bookmark` → `bookmark` - `block-callout` → `callout` - `block-code` → `code` - `block-data-view` → `data-view` - `block-database` → `database` - `block-divider` → `divider` - `block-edgeless-text` → `edgeless-text` - `block-embed` → `embed`
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import type { ExtensionType } from '@blocksuite/store';
|
||||
|
||||
import { TableBlockHtmlAdapterExtension } from './html.js';
|
||||
import { TableBlockMarkdownAdapterExtension } from './markdown.js';
|
||||
import { TableBlockNotionHtmlAdapterExtension } from './notion-html.js';
|
||||
import { TableBlockPlainTextAdapterExtension } from './plain-text.js';
|
||||
|
||||
export const TableBlockAdapterExtensions: ExtensionType[] = [
|
||||
TableBlockHtmlAdapterExtension,
|
||||
TableBlockMarkdownAdapterExtension,
|
||||
TableBlockNotionHtmlAdapterExtension,
|
||||
TableBlockPlainTextAdapterExtension,
|
||||
];
|
||||
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
type TableBlockPropsSerialized,
|
||||
TableBlockSchema,
|
||||
TableModelFlavour,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockHtmlAdapterExtension,
|
||||
type BlockHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
type InlineHtmlAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { Element } from 'hast';
|
||||
|
||||
import { DefaultColumnWidth } from '../consts';
|
||||
import { parseTableFromHtml, processTable } from './utils';
|
||||
|
||||
const TABLE_NODE_TYPES = new Set(['table', 'thead', 'tbody', 'th', 'tr']);
|
||||
|
||||
export const tableBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
|
||||
flavour: TableBlockSchema.model.flavour,
|
||||
toMatch: o => {
|
||||
return HastUtils.isElement(o.node) && TABLE_NODE_TYPES.has(o.node.tagName);
|
||||
},
|
||||
fromMatch: o => o.node.flavour === TableBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
if (!HastUtils.isElement(o.node)) {
|
||||
return;
|
||||
}
|
||||
const { walkerContext } = context;
|
||||
if (o.node.tagName === 'table') {
|
||||
const astToDelta = context.deltaConverter.astToDelta.bind(
|
||||
context.deltaConverter
|
||||
);
|
||||
const tableProps = parseTableFromHtml(o.node, astToDelta);
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: TableModelFlavour,
|
||||
props: tableProps as unknown as Record<string, unknown>,
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
);
|
||||
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, rows, cells } = o.node
|
||||
.props as unknown as TableBlockPropsSerialized;
|
||||
const table = processTable(columns, rows, cells);
|
||||
const createAstTableCell = (
|
||||
children: InlineHtmlAST[]
|
||||
): InlineHtmlAST => ({
|
||||
type: 'element',
|
||||
tagName: 'td',
|
||||
properties: Object.create(null),
|
||||
children: [
|
||||
{
|
||||
type: 'element',
|
||||
tagName: 'div',
|
||||
properties: {
|
||||
style: `min-height: 22px;min-width:${DefaultColumnWidth}px;padding: 8px 12px;`,
|
||||
},
|
||||
children,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const createAstTableRow = (cells: InlineHtmlAST[]): Element => ({
|
||||
type: 'element',
|
||||
tagName: 'tr',
|
||||
properties: Object.create(null),
|
||||
children: cells,
|
||||
});
|
||||
|
||||
const { deltaConverter } = context;
|
||||
|
||||
const tableBodyAst: Element = {
|
||||
type: 'element',
|
||||
tagName: 'tbody',
|
||||
properties: Object.create(null),
|
||||
children: table.rows.map(v => {
|
||||
return createAstTableRow(
|
||||
v.cells.map(cell => {
|
||||
return createAstTableCell(
|
||||
typeof cell.value === 'string'
|
||||
? [{ type: 'text', value: cell.value }]
|
||||
: deltaConverter.deltaToAST(cell.value.delta)
|
||||
);
|
||||
})
|
||||
);
|
||||
}),
|
||||
};
|
||||
|
||||
walkerContext
|
||||
.openNode({
|
||||
type: 'element',
|
||||
tagName: 'table',
|
||||
properties: {
|
||||
border: true,
|
||||
style: 'border-collapse: collapse;border-spacing: 0;',
|
||||
},
|
||||
children: [tableBodyAst],
|
||||
})
|
||||
.closeNode();
|
||||
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TableBlockHtmlAdapterExtension = BlockHtmlAdapterExtension(
|
||||
tableBlockHtmlAdapterMatcher
|
||||
);
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './html';
|
||||
export * from './markdown';
|
||||
export * from './notion-html';
|
||||
export * from './plain-text';
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
type TableBlockPropsSerialized,
|
||||
TableBlockSchema,
|
||||
TableModelFlavour,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockMarkdownAdapterExtension,
|
||||
type BlockMarkdownAdapterMatcher,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { TableRow } from 'mdast';
|
||||
|
||||
import { parseTableFromMarkdown, processTable } from './utils';
|
||||
|
||||
const TABLE_NODE_TYPES = new Set(['table', 'tableRow']);
|
||||
|
||||
const isTableNode = (node: MarkdownAST) => TABLE_NODE_TYPES.has(node.type);
|
||||
|
||||
export const tableBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
|
||||
flavour: TableBlockSchema.model.flavour,
|
||||
toMatch: o => isTableNode(o.node),
|
||||
fromMatch: o => o.node.flavour === TableBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (o.node.type === 'table') {
|
||||
const astToDelta = context.deltaConverter.astToDelta.bind(
|
||||
context.deltaConverter
|
||||
);
|
||||
walkerContext.openNode(
|
||||
{
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: TableModelFlavour,
|
||||
props: parseTableFromMarkdown(o.node, astToDelta),
|
||||
children: [],
|
||||
},
|
||||
'children'
|
||||
);
|
||||
walkerContext.skipAllChildren();
|
||||
}
|
||||
},
|
||||
leave: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
if (o.node.type === 'table') {
|
||||
walkerContext.closeNode();
|
||||
}
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext, deltaConverter } = context;
|
||||
const { columns, rows, cells } = o.node
|
||||
.props as unknown as TableBlockPropsSerialized;
|
||||
const table = processTable(columns, rows, cells);
|
||||
const result: TableRow[] = [];
|
||||
table.rows.forEach(v => {
|
||||
result.push({
|
||||
type: 'tableRow',
|
||||
children: v.cells.map(v => ({
|
||||
type: 'tableCell',
|
||||
children: deltaConverter.deltaToAST(v.value.delta),
|
||||
})),
|
||||
});
|
||||
});
|
||||
|
||||
walkerContext
|
||||
.openNode({
|
||||
type: 'table',
|
||||
children: result,
|
||||
})
|
||||
.closeNode();
|
||||
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TableBlockMarkdownAdapterExtension = BlockMarkdownAdapterExtension(
|
||||
tableBlockMarkdownAdapterMatcher
|
||||
);
|
||||
@@ -0,0 +1,21 @@
|
||||
import { TableBlockSchema } from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockNotionHtmlAdapterExtension,
|
||||
type BlockNotionHtmlAdapterMatcher,
|
||||
HastUtils,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
|
||||
const TABLE_NODE_TYPES = new Set(['table', 'th', 'tr']);
|
||||
|
||||
export const tableBlockNotionHtmlAdapterMatcher: BlockNotionHtmlAdapterMatcher =
|
||||
{
|
||||
flavour: TableBlockSchema.model.flavour,
|
||||
toMatch: o =>
|
||||
HastUtils.isElement(o.node) && TABLE_NODE_TYPES.has(o.node.tagName),
|
||||
fromMatch: () => false,
|
||||
toBlockSnapshot: {},
|
||||
fromBlockSnapshot: {},
|
||||
};
|
||||
|
||||
export const TableBlockNotionHtmlAdapterExtension =
|
||||
BlockNotionHtmlAdapterExtension(tableBlockNotionHtmlAdapterMatcher);
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
type TableBlockPropsSerialized,
|
||||
TableBlockSchema,
|
||||
TableModelFlavour,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
BlockPlainTextAdapterExtension,
|
||||
type BlockPlainTextAdapterMatcher,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
|
||||
import { createTableProps, formatTable, processTable } from './utils.js';
|
||||
|
||||
export const tableBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = {
|
||||
flavour: TableBlockSchema.model.flavour,
|
||||
toMatch: () => true,
|
||||
fromMatch: o => o.node.flavour === TableBlockSchema.model.flavour,
|
||||
toBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext } = context;
|
||||
const text = o.node.content;
|
||||
const rowTexts = text.split('\n');
|
||||
if (rowTexts.length <= 1) return;
|
||||
const rowTextLists: DeltaInsert[][][] = [];
|
||||
let columnCount: number | null = null;
|
||||
for (const row of rowTexts) {
|
||||
const cells = row.split('\t').map<DeltaInsert[]>(text => [
|
||||
{
|
||||
insert: text,
|
||||
},
|
||||
]);
|
||||
if (cells.length <= 1) return;
|
||||
if (columnCount == null) {
|
||||
columnCount = cells.length;
|
||||
} else if (columnCount !== cells.length) {
|
||||
return;
|
||||
}
|
||||
rowTextLists.push(cells);
|
||||
}
|
||||
const tableProps = createTableProps(rowTextLists);
|
||||
walkerContext.openNode({
|
||||
type: 'block',
|
||||
id: nanoid(),
|
||||
flavour: TableModelFlavour,
|
||||
props: tableProps,
|
||||
children: [],
|
||||
});
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
leave: (_, context) => {
|
||||
const { walkerContext } = context;
|
||||
walkerContext.closeNode();
|
||||
},
|
||||
},
|
||||
fromBlockSnapshot: {
|
||||
enter: (o, context) => {
|
||||
const { walkerContext, deltaConverter } = context;
|
||||
const result: string[][] = [];
|
||||
const { columns, rows, cells } = o.node
|
||||
.props as unknown as TableBlockPropsSerialized;
|
||||
const table = processTable(columns, rows, cells);
|
||||
table.rows.forEach(v => {
|
||||
result.push(
|
||||
v.cells.map(v => deltaConverter.deltaToAST(v.value.delta).join(''))
|
||||
);
|
||||
});
|
||||
|
||||
const tableString = formatTable(result);
|
||||
|
||||
context.textBuffer.content += tableString;
|
||||
context.textBuffer.content += '\n';
|
||||
walkerContext.skipAllChildren();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const TableBlockPlainTextAdapterExtension =
|
||||
BlockPlainTextAdapterExtension(tableBlockPlainTextAdapterMatcher);
|
||||
@@ -0,0 +1,212 @@
|
||||
import type {
|
||||
TableBlockPropsSerialized,
|
||||
TableCellSerialized,
|
||||
TableColumn,
|
||||
TableRow,
|
||||
} from '@blocksuite/affine-model';
|
||||
import {
|
||||
type HtmlAST,
|
||||
type MarkdownAST,
|
||||
} from '@blocksuite/affine-shared/adapters';
|
||||
import { HastUtils } from '@blocksuite/affine-shared/adapters';
|
||||
import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils';
|
||||
import type { DeltaInsert } from '@blocksuite/store';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import type { Element } from 'hast';
|
||||
import type { Table as MarkdownTable } from 'mdast';
|
||||
|
||||
type RichTextType = DeltaInsert[];
|
||||
const createRichText = (text: RichTextType) => {
|
||||
return {
|
||||
'$blocksuite:internal:text$': true,
|
||||
delta: text,
|
||||
};
|
||||
};
|
||||
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] ?? 0, ' ')
|
||||
);
|
||||
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');
|
||||
}
|
||||
type Table = {
|
||||
rows: Row[];
|
||||
};
|
||||
type Row = {
|
||||
cells: Cell[];
|
||||
};
|
||||
type Cell = {
|
||||
value: { delta: DeltaInsert[] };
|
||||
};
|
||||
export const processTable = (
|
||||
columns: Record<string, TableColumn>,
|
||||
rows: Record<string, TableRow>,
|
||||
cells: Record<string, TableCellSerialized>
|
||||
): Table => {
|
||||
const sortedColumns = Object.values(columns).sort((a, b) =>
|
||||
a.order.localeCompare(b.order)
|
||||
);
|
||||
const sortedRows = Object.values(rows).sort((a, b) =>
|
||||
a.order.localeCompare(b.order)
|
||||
);
|
||||
const table: Table = {
|
||||
rows: [],
|
||||
};
|
||||
sortedRows.forEach(r => {
|
||||
const row: Row = {
|
||||
cells: [],
|
||||
};
|
||||
sortedColumns.forEach(col => {
|
||||
const cell = cells[`${r.rowId}:${col.columnId}`];
|
||||
if (!cell) {
|
||||
row.cells.push({
|
||||
value: {
|
||||
delta: [],
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
row.cells.push({
|
||||
value: cell.text,
|
||||
});
|
||||
});
|
||||
table.rows.push(row);
|
||||
});
|
||||
return table;
|
||||
};
|
||||
|
||||
const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
|
||||
if (!node) {
|
||||
return [];
|
||||
}
|
||||
if (HastUtils.isElement(node)) {
|
||||
if (node.tagName === tagName) {
|
||||
return [node];
|
||||
}
|
||||
return node.children.flatMap(child => {
|
||||
if (HastUtils.isElement(child)) {
|
||||
return getAllTag(child, tagName);
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const createTableProps = (deltasLists: RichTextType[][]) => {
|
||||
const createIdAndOrder = (count: number) => {
|
||||
const result: { id: string; order: string }[] = Array.from({
|
||||
length: count,
|
||||
});
|
||||
for (let i = 0; i < count; i++) {
|
||||
const id = nanoid();
|
||||
const order = generateFractionalIndexingKeyBetween(
|
||||
result[i - 1]?.order ?? null,
|
||||
null
|
||||
);
|
||||
result[i] = { id, order };
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const columnCount = Math.max(...deltasLists.map(row => row.length));
|
||||
const rowCount = deltasLists.length;
|
||||
|
||||
const columns: TableColumn[] = createIdAndOrder(columnCount).map(v => ({
|
||||
columnId: v.id,
|
||||
order: v.order,
|
||||
}));
|
||||
const rows: TableRow[] = createIdAndOrder(rowCount).map(v => ({
|
||||
rowId: v.id,
|
||||
order: v.order,
|
||||
}));
|
||||
|
||||
const cells: Record<string, TableCellSerialized> = {};
|
||||
for (let i = 0; i < rowCount; i++) {
|
||||
for (let j = 0; j < columnCount; j++) {
|
||||
const row = rows[i];
|
||||
const column = columns[j];
|
||||
if (!row || !column) {
|
||||
continue;
|
||||
}
|
||||
const cellId = `${row.rowId}:${column.columnId}`;
|
||||
const text = deltasLists[i]?.[j];
|
||||
cells[cellId] = {
|
||||
text: createRichText(text ?? []),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
columns: Object.fromEntries(
|
||||
columns.map(column => [column.columnId, column])
|
||||
),
|
||||
rows: Object.fromEntries(rows.map(row => [row.rowId, row])),
|
||||
cells,
|
||||
};
|
||||
};
|
||||
|
||||
export const parseTableFromHtml = (
|
||||
element: Element,
|
||||
astToDelta: (ast: HtmlAST) => RichTextType
|
||||
): TableBlockPropsSerialized => {
|
||||
const headerRows = getAllTag(element, 'thead').flatMap(node =>
|
||||
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'th'))
|
||||
);
|
||||
const bodyRows = getAllTag(element, 'tbody').flatMap(node =>
|
||||
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td'))
|
||||
);
|
||||
const footerRows = getAllTag(element, 'tfoot').flatMap(node =>
|
||||
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td'))
|
||||
);
|
||||
const allRows = [...headerRows, ...bodyRows, ...footerRows];
|
||||
const rowTextLists: RichTextType[][] = [];
|
||||
allRows.forEach(cells => {
|
||||
const row: RichTextType[] = [];
|
||||
cells.forEach(cell => {
|
||||
row.push(astToDelta(cell));
|
||||
});
|
||||
rowTextLists.push(row);
|
||||
});
|
||||
return createTableProps(rowTextLists);
|
||||
};
|
||||
|
||||
export const parseTableFromMarkdown = (
|
||||
node: MarkdownTable,
|
||||
astToDelta: (ast: MarkdownAST) => RichTextType
|
||||
) => {
|
||||
const rowTextLists: RichTextType[][] = [];
|
||||
node.children.forEach(row => {
|
||||
const rowText: RichTextType[] = [];
|
||||
row.children.forEach(cell => {
|
||||
rowText.push(astToDelta(cell));
|
||||
});
|
||||
rowTextLists.push(rowText);
|
||||
});
|
||||
return createTableProps(rowTextLists);
|
||||
};
|
||||
Reference in New Issue
Block a user