fix: recover paragraph editor index errors
This commit is contained in:
@@ -41,6 +41,20 @@ describe('blockNoteRenderRecovery', () => {
|
||||
expect(isRecoverableBlockNoteRenderError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes production paragraph index render errors from slash input', () => {
|
||||
const error = new RangeError('Index 1 out of range for <paragraph("/")>')
|
||||
|
||||
expect(blockNoteRenderRecoveryReason(error)).toBe('paragraph_index_out_of_range')
|
||||
expect(isRecoverableBlockNoteRenderError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes production paragraph index render errors that are plain Error instances', () => {
|
||||
const error = new Error('Index 1 out of range for <paragraph("/")>')
|
||||
|
||||
expect(blockNoteRenderRecoveryReason(error)).toBe('paragraph_index_out_of_range')
|
||||
expect(isRecoverableBlockNoteRenderError(error)).toBe(true)
|
||||
})
|
||||
|
||||
it('recognizes recovered BlockNote block type mismatch render errors', () => {
|
||||
const error = new Error('Block type does not match')
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ const BLOCKNOTE_BLOCK_TYPE_MISMATCH_ERROR = 'Block type does not match'
|
||||
const BLOCKNOTE_RECOVERY_BOUNDARY_NAME = 'BlockNoteRenderRecoveryBoundary'
|
||||
const RECOVERED_BLOCKNOTE_RENDER_ERROR_MARK = '__tolariaRecoveredBlockNoteRenderError'
|
||||
const BLOCKNOTE_TABLE_ROW_INDEX_ERROR = /^Index \d+ out of range for <tableRow\(/
|
||||
const BLOCKNOTE_PARAGRAPH_INDEX_ERROR = /^Index \d+ out of range for <paragraph\(/
|
||||
|
||||
export type BlockNoteRenderRecoveryReason =
|
||||
| 'block_type_mismatch'
|
||||
| 'block_missing_id'
|
||||
| 'paragraph_index_out_of_range'
|
||||
| 'table_row_index_out_of_range'
|
||||
|
||||
type MarkedRecoveredBlockNoteRenderError = Error & {
|
||||
@@ -29,6 +31,9 @@ export function blockNoteRenderRecoveryReason(error: unknown): BlockNoteRenderRe
|
||||
if (BLOCKNOTE_TABLE_ROW_INDEX_ERROR.test(error.message)) {
|
||||
return 'table_row_index_out_of_range'
|
||||
}
|
||||
if (BLOCKNOTE_PARAGRAPH_INDEX_ERROR.test(error.message)) {
|
||||
return 'paragraph_index_out_of_range'
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -95,6 +95,12 @@ describe('isRecoverableEditorTransformError', () => {
|
||||
expect(isRecoverableEditorTransformError(new Error(
|
||||
'Index 1 out of range for <tableRow(tableCell(tableParagraph("A")))>',
|
||||
))).toBe(true)
|
||||
expect(isRecoverableEditorTransformError(new RangeError(
|
||||
'Index 1 out of range for <paragraph("/")>',
|
||||
))).toBe(true)
|
||||
expect(isRecoverableEditorTransformError(new Error(
|
||||
'Index 1 out of range for <paragraph("/")>',
|
||||
))).toBe(true)
|
||||
expect(isRecoverableEditorTransformError(new Error(
|
||||
'Block with ID 6c1c3bb4-e218-4f00-aaf5-40606852d286 not found',
|
||||
))).toBe(true)
|
||||
@@ -106,9 +112,6 @@ describe('isRecoverableEditorTransformError', () => {
|
||||
expect(isRecoverableEditorTransformError(new TypeError(
|
||||
"Cannot read properties of null (reading 'append')",
|
||||
))).toBe(false)
|
||||
expect(isRecoverableEditorTransformError(new RangeError(
|
||||
'Index 1 out of range for <paragraph("A")>',
|
||||
))).toBe(false)
|
||||
expect(isRecoverableEditorTransformError(new Error('unrelated'))).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -218,6 +221,13 @@ describe('installRichEditorTransformErrorRecovery', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('recovers production paragraph index transactions from stale slash input', () => {
|
||||
expectDocumentRepairRecovery(
|
||||
new RangeError('Index 1 out of range for <paragraph("/")>'),
|
||||
'paragraph_position_out_of_range',
|
||||
)
|
||||
})
|
||||
|
||||
it('recovers invalid insertion-depth transactions after note switching and saves', () => {
|
||||
expectDocumentRepairRecovery(
|
||||
new RangeError('Inserted content deeper than insertion position'),
|
||||
|
||||
@@ -49,11 +49,17 @@ type RecoveryReason =
|
||||
| 'invalid_insertion_depth'
|
||||
| 'mismatched_transaction'
|
||||
| 'null_fragment_append'
|
||||
| 'paragraph_position_out_of_range'
|
||||
| 'stale_block_reference'
|
||||
| 'stale_transaction'
|
||||
| 'table_position_out_of_range'
|
||||
| 'transform_error'
|
||||
|
||||
interface RecoveryReasonMatcher {
|
||||
matches: (error: unknown) => boolean
|
||||
reason: RecoveryReason
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null
|
||||
}
|
||||
@@ -92,6 +98,10 @@ function isTablePositionOutOfRangeError(error: unknown): boolean {
|
||||
return error instanceof Error && /^Index \d+ out of range for <tableRow\(/.test(error.message)
|
||||
}
|
||||
|
||||
function isParagraphPositionOutOfRangeError(error: unknown): boolean {
|
||||
return error instanceof Error && /^Index \d+ out of range for <paragraph\(/.test(error.message)
|
||||
}
|
||||
|
||||
function isInvalidBlockJoinError(error: unknown): boolean {
|
||||
return isTransformError(error) && /^Cannot join (blockGroup|tableCell) onto blockContainer$/.test(error.message)
|
||||
}
|
||||
@@ -114,6 +124,7 @@ function isTransformError(error: unknown): error is Error {
|
||||
function isRecoverableRangeError(error: unknown): boolean {
|
||||
return isInvalidContentTransactionError(error)
|
||||
|| isInvalidInsertionDepthError(error)
|
||||
|| isParagraphPositionOutOfRangeError(error)
|
||||
|| isTablePositionOutOfRangeError(error)
|
||||
}
|
||||
|
||||
@@ -125,6 +136,16 @@ const RECOVERABLE_EDITOR_ERROR_PREDICATES = [
|
||||
isStaleBlockReferenceError,
|
||||
]
|
||||
|
||||
const RECOVERY_REASON_MATCHERS: RecoveryReasonMatcher[] = [
|
||||
{ matches: isMismatchedTransactionError, reason: 'mismatched_transaction' },
|
||||
{ matches: isStaleBlockReferenceError, reason: 'stale_block_reference' },
|
||||
{ matches: isInvalidBlockJoinError, reason: 'invalid_block_join' },
|
||||
{ matches: isInvalidInsertionDepthError, reason: 'invalid_insertion_depth' },
|
||||
{ matches: isNullFragmentAppendError, reason: 'null_fragment_append' },
|
||||
{ matches: isParagraphPositionOutOfRangeError, reason: 'paragraph_position_out_of_range' },
|
||||
{ matches: isTablePositionOutOfRangeError, reason: 'table_position_out_of_range' },
|
||||
]
|
||||
|
||||
export function isRecoverableEditorTransformError(error: unknown): boolean {
|
||||
return RECOVERABLE_EDITOR_ERROR_PREDICATES.some((predicate) => predicate(error))
|
||||
}
|
||||
@@ -135,13 +156,7 @@ function recoveryReason(
|
||||
view: RichEditorDispatchView,
|
||||
): RecoveryReason {
|
||||
if (transactionDocIsStale(transaction, view)) return 'stale_transaction'
|
||||
if (isMismatchedTransactionError(error)) return 'mismatched_transaction'
|
||||
if (isStaleBlockReferenceError(error)) return 'stale_block_reference'
|
||||
if (isInvalidBlockJoinError(error)) return 'invalid_block_join'
|
||||
if (isInvalidInsertionDepthError(error)) return 'invalid_insertion_depth'
|
||||
if (isNullFragmentAppendError(error)) return 'null_fragment_append'
|
||||
if (isTablePositionOutOfRangeError(error)) return 'table_position_out_of_range'
|
||||
return 'transform_error'
|
||||
return RECOVERY_REASON_MATCHERS.find(({ matches }) => matches(error))?.reason ?? 'transform_error'
|
||||
}
|
||||
|
||||
function shouldRepairEditorDocument(error: unknown): boolean {
|
||||
|
||||
Reference in New Issue
Block a user