Compare commits

...

2 Commits

Author SHA1 Message Date
zzj3720
9bee2cb0fa fix(editor): improve string conversion logic for checkbox property
- Add a FALSE_VALUES set containing various falsy string representations

- Support Chinese negation terms like "否", "不", "错", etc.

- Optimize the implementation of cellFromString method
2025-02-26 00:11:36 +08:00
zzj3720
1addd17d64 fix(editor): table block supports parsing rich text 2025-02-25 18:52:13 +08:00
5 changed files with 63 additions and 45 deletions

View File

@@ -30,7 +30,10 @@ export const tableBlockHtmlAdapterMatcher: BlockHtmlAdapterMatcher = {
} }
const { walkerContext } = context; const { walkerContext } = context;
if (o.node.tagName === 'table') { if (o.node.tagName === 'table') {
const tableProps = parseTableFromHtml(o.node); const astToDelta = context.deltaConverter.astToDelta.bind(
context.deltaConverter
);
const tableProps = parseTableFromHtml(o.node, astToDelta);
walkerContext.openNode( walkerContext.openNode(
{ {
type: 'block', type: 'block',

View File

@@ -25,12 +25,15 @@ export const tableBlockMarkdownAdapterMatcher: BlockMarkdownAdapterMatcher = {
enter: (o, context) => { enter: (o, context) => {
const { walkerContext } = context; const { walkerContext } = context;
if (o.node.type === 'table') { if (o.node.type === 'table') {
const astToDelta = context.deltaConverter.astToDelta.bind(
context.deltaConverter
);
walkerContext.openNode( walkerContext.openNode(
{ {
type: 'block', type: 'block',
id: nanoid(), id: nanoid(),
flavour: TableModelFlavour, flavour: TableModelFlavour,
props: parseTableFromMarkdown(o.node), props: parseTableFromMarkdown(o.node, astToDelta),
children: [], children: [],
}, },
'children' 'children'

View File

@@ -7,6 +7,7 @@ import {
BlockPlainTextAdapterExtension, BlockPlainTextAdapterExtension,
type BlockPlainTextAdapterMatcher, type BlockPlainTextAdapterMatcher,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import { createTableProps, formatTable, processTable } from './utils.js'; import { createTableProps, formatTable, processTable } from './utils.js';
@@ -21,10 +22,14 @@ export const tableBlockPlainTextAdapterMatcher: BlockPlainTextAdapterMatcher = {
const text = o.node.content; const text = o.node.content;
const rowTexts = text.split('\n'); const rowTexts = text.split('\n');
if (rowTexts.length <= 1) return; if (rowTexts.length <= 1) return;
const rowTextLists: string[][] = []; const rowTextLists: DeltaInsert[][][] = [];
let columnCount: number | null = null; let columnCount: number | null = null;
for (const row of rowTexts) { for (const row of rowTexts) {
const cells = row.split('\t'); const cells = row.split('\t').map<DeltaInsert[]>(text => [
{
insert: text,
},
]);
if (cells.length <= 1) return; if (cells.length <= 1) return;
if (columnCount == null) { if (columnCount == null) {
columnCount = cells.length; columnCount = cells.length;

View File

@@ -5,14 +5,23 @@ import type {
TableRow, TableRow,
} from '@blocksuite/affine-model'; } from '@blocksuite/affine-model';
import { import {
AdapterTextUtils, type HtmlAST,
HastUtils, type MarkdownAST,
} from '@blocksuite/affine-shared/adapters'; } from '@blocksuite/affine-shared/adapters';
import { HastUtils } from '@blocksuite/affine-shared/adapters';
import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils'; import { generateFractionalIndexingKeyBetween } from '@blocksuite/affine-shared/utils';
import type { DeltaInsert } from '@blocksuite/inline'; import type { DeltaInsert } from '@blocksuite/inline';
import { nanoid } from '@blocksuite/store'; import { nanoid } from '@blocksuite/store';
import type { Element, ElementContent } from 'hast'; import type { Element } from 'hast';
import type { PhrasingContent, Table as MarkdownTable, TableCell } from 'mdast'; 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[] { function calculateColumnWidths(rows: string[][]): number[] {
return ( return (
rows[0]?.map((_, colIndex) => rows[0]?.map((_, colIndex) =>
@@ -92,15 +101,6 @@ export const processTable = (
}); });
return table; return table;
}; };
const getTextFromElement = (element: ElementContent): string => {
if (element.type === 'text') {
return element.value.trim();
}
if (element.type === 'element') {
return element.children.map(child => getTextFromElement(child)).join('');
}
return '';
};
const getAllTag = (node: Element | undefined, tagName: string): Element[] => { const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
if (!node) { if (!node) {
@@ -120,7 +120,7 @@ const getAllTag = (node: Element | undefined, tagName: string): Element[] => {
return []; return [];
}; };
export const createTableProps = (rowTextLists: string[][]) => { export const createTableProps = (deltasLists: RichTextType[][]) => {
const createIdAndOrder = (count: number) => { const createIdAndOrder = (count: number) => {
const result: { id: string; order: string }[] = Array.from({ const result: { id: string; order: string }[] = Array.from({
length: count, length: count,
@@ -135,8 +135,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
} }
return result; return result;
}; };
const columnCount = Math.max(...rowTextLists.map(row => row.length)); const columnCount = Math.max(...deltasLists.map(row => row.length));
const rowCount = rowTextLists.length; const rowCount = deltasLists.length;
const columns: TableColumn[] = createIdAndOrder(columnCount).map(v => ({ const columns: TableColumn[] = createIdAndOrder(columnCount).map(v => ({
columnId: v.id, columnId: v.id,
@@ -156,9 +156,9 @@ export const createTableProps = (rowTextLists: string[][]) => {
continue; continue;
} }
const cellId = `${row.rowId}:${column.columnId}`; const cellId = `${row.rowId}:${column.columnId}`;
const text = rowTextLists[i]?.[j]; const text = deltasLists[i]?.[j];
cells[cellId] = { cells[cellId] = {
text: AdapterTextUtils.createText(text ?? ''), text: createRichText(text ?? []),
}; };
} }
} }
@@ -172,7 +172,8 @@ export const createTableProps = (rowTextLists: string[][]) => {
}; };
export const parseTableFromHtml = ( export const parseTableFromHtml = (
element: Element element: Element,
astToDelta: (ast: HtmlAST) => RichTextType
): TableBlockPropsSerialized => { ): TableBlockPropsSerialized => {
const headerRows = getAllTag(element, 'thead').flatMap(node => const headerRows = getAllTag(element, 'thead').flatMap(node =>
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'th')) getAllTag(node, 'tr').map(tr => getAllTag(tr, 'th'))
@@ -184,33 +185,26 @@ export const parseTableFromHtml = (
getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td')) getAllTag(node, 'tr').map(tr => getAllTag(tr, 'td'))
); );
const allRows = [...headerRows, ...bodyRows, ...footerRows]; const allRows = [...headerRows, ...bodyRows, ...footerRows];
const rowTextLists: string[][] = []; const rowTextLists: RichTextType[][] = [];
allRows.forEach(cells => { allRows.forEach(cells => {
const row: string[] = []; const row: RichTextType[] = [];
cells.forEach(cell => { cells.forEach(cell => {
row.push(getTextFromElement(cell)); row.push(astToDelta(cell));
}); });
rowTextLists.push(row); rowTextLists.push(row);
}); });
return createTableProps(rowTextLists); return createTableProps(rowTextLists);
}; };
const getTextFromTableCell = (node: TableCell) => { export const parseTableFromMarkdown = (
const getTextFromPhrasingContent = (node: PhrasingContent) => { node: MarkdownTable,
if (node.type === 'text') { astToDelta: (ast: MarkdownAST) => RichTextType
return node.value; ) => {
} const rowTextLists: RichTextType[][] = [];
return '';
};
return node.children.map(child => getTextFromPhrasingContent(child)).join('');
};
export const parseTableFromMarkdown = (node: MarkdownTable) => {
const rowTextLists: string[][] = [];
node.children.forEach(row => { node.children.forEach(row => {
const rowText: string[] = []; const rowText: RichTextType[] = [];
row.children.forEach(cell => { row.children.forEach(cell => {
rowText.push(getTextFromTableCell(cell)); rowText.push(astToDelta(cell));
}); });
rowTextLists.push(rowText); rowTextLists.push(rowText);
}); });

View File

@@ -3,17 +3,30 @@ import { propertyType } from '../../core/property/property-config.js';
export const checkboxPropertyType = propertyType('checkbox'); export const checkboxPropertyType = propertyType('checkbox');
const FALSE_VALUES = new Set([
'false',
'no',
'0',
'',
'undefined',
'null',
'否',
'不',
'错',
'错误',
'取消',
'关闭',
]);
export const checkboxPropertyModelConfig = export const checkboxPropertyModelConfig =
checkboxPropertyType.modelConfig<boolean>({ checkboxPropertyType.modelConfig<boolean>({
name: 'Checkbox', name: 'Checkbox',
type: () => t.boolean.instance(), type: () => t.boolean.instance(),
defaultData: () => ({}), defaultData: () => ({}),
cellToString: ({ value }) => (value ? 'True' : 'False'), cellToString: ({ value }) => (value ? 'True' : 'False'),
cellFromString: ({ value }) => { cellFromString: ({ value }) => ({
return { value: !FALSE_VALUES.has((value?.trim() ?? '').toLowerCase()),
value: value !== 'False', }),
};
},
cellToJson: ({ value }) => value ?? null, cellToJson: ({ value }) => value ?? null,
cellFromJson: ({ value }) => cellFromJson: ({ value }) =>
typeof value !== 'boolean' ? undefined : value, typeof value !== 'boolean' ? undefined : value,