fix: repair malformed nested editor blocks

This commit is contained in:
lucaronin
2026-05-27 18:32:18 +02:00
parent af83d270fd
commit 908994c253
5 changed files with 151 additions and 75 deletions

View File

@@ -500,11 +500,12 @@ describe('SingleEditorView', () => {
expect(editor.replaceBlocks.mock.calls[0][1]).toEqual([
expect.objectContaining({
id: expect.any(String),
children: [
expect.objectContaining({
id: expect.any(String),
}),
],
children: [],
}),
expect.objectContaining({
id: expect.any(String),
content: [{ type: 'text', text: 'Recovered child', styles: {} }],
children: [],
}),
])
} finally {

View File

@@ -15,7 +15,7 @@ function block(type: string, text: string, children: unknown[] = []) {
}
describe('repairMalformedEditorBlocks', () => {
it('promotes numbered-list children out of paragraph blocks', () => {
it('promotes all children out of paragraph blocks', () => {
const nestedParagraph = block('paragraph', 'Nested paragraph')
const nestedList = block('numberedListItem', 'Step one', [
block('numberedListItem', 'Nested step'),
@@ -26,8 +26,9 @@ describe('repairMalformedEditorBlocks', () => {
expect(repairMalformedEditorBlocks([paragraph, tail])).toEqual([
{
...paragraph,
children: [nestedParagraph],
children: [],
},
nestedParagraph,
nestedList,
tail,
])
@@ -39,4 +40,11 @@ describe('repairMalformedEditorBlocks', () => {
expect(repairMalformedEditorBlocks([parent])).toEqual([parent])
})
it('keeps nested toggle-list children under toggle-list items', () => {
const nested = block('paragraph', 'Toggle detail')
const parent = block('toggleListItem', 'Toggle summary', [nested])
expect(repairMalformedEditorBlocks([parent])).toEqual([parent])
})
})

View File

@@ -1,5 +1,10 @@
let fallbackBlockIdSequence = 0
const LIST_ITEM_TYPES = new Set(['bulletListItem', 'numberedListItem', 'checkListItem'])
const NESTABLE_LIST_ITEM_TYPES = new Set([
'bulletListItem',
'numberedListItem',
'checkListItem',
'toggleListItem',
])
type RepairResult = {
blocks: unknown[]
@@ -25,8 +30,8 @@ function isEditorBlockRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function isListItemBlock(block: Record<string, unknown>): boolean {
return typeof block.type === 'string' && LIST_ITEM_TYPES.has(block.type)
function canNestChildBlocks(block: Record<string, unknown>): boolean {
return typeof block.type === 'string' && NESTABLE_LIST_ITEM_TYPES.has(block.type)
}
function hasUsableBlockId(block: Record<string, unknown>): boolean {
@@ -46,20 +51,11 @@ function splitChildrenForBlock(
block: Record<string, unknown>,
children: unknown[],
): { safeChildren: unknown[], promotedChildren: unknown[] } {
if (isListItemBlock(block)) {
if (canNestChildBlocks(block)) {
return { safeChildren: children, promotedChildren: [] }
}
const safeChildren: unknown[] = []
const promotedChildren: unknown[] = []
for (const child of children) {
if (isEditorBlockRecord(child) && isListItemBlock(child)) {
promotedChildren.push(child)
} else {
safeChildren.push(child)
}
}
return { safeChildren, promotedChildren }
return { safeChildren: [], promotedChildren: children }
}
function repairBlockList(blocks: unknown[]): RepairResult {

View File

@@ -558,13 +558,14 @@ describe('useEditorTabSwap raw mode sync', () => {
expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }),
expect.objectContaining({
id: expect.any(String),
children: [
expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }),
expect.objectContaining({
id: expect.any(String),
content: [{ type: 'text', text: 'Child', styles: {} }],
}),
],
content: [{ type: 'text', text: 'Parent', styles: {} }],
children: [],
}),
expect.objectContaining({ id: expect.any(String), type: 'paragraph', content: [], children: [] }),
expect.objectContaining({
id: expect.any(String),
content: [{ type: 'text', text: 'Child', styles: {} }],
children: [],
}),
])
})