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:
Saul-Mirone
2025-04-07 12:34:40 +00:00
parent e1bd2047c4
commit 1f45cc5dec
893 changed files with 439 additions and 460 deletions
@@ -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);
};