diff --git a/src/automerge/AutomergeToTLStore.ts b/src/automerge/AutomergeToTLStore.ts index 9db1da2..d0dd894 100644 --- a/src/automerge/AutomergeToTLStore.ts +++ b/src/automerge/AutomergeToTLStore.ts @@ -11,39 +11,23 @@ function isValidIndexKey(index: string): boolean { return false } - // The first character indicates the integer part length: - // 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers - // 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers - // But for normal shapes, 'a' followed by a digit is the most common pattern + // tldraw uses fractional indexing where: + // - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.) + // - Followed by alphanumeric characters for the value and optional jitter + // Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz" + // + // Also uppercase letters for negative indices (Z=1, Y=2, etc.) - // Simple invalid patterns that are definitely wrong: - // - Just a number like "1", "2" - // - Old format like "b1", "c1" (letter + single digit that's not a valid fractional index) - // - Empty or whitespace - - // Valid fractional indices from tldraw start with 'a' for small positive numbers - // and follow with digits + optional alphanumeric jitter - // Pattern: starts with 'a', followed by at least one digit, then optional alphanumeric chars - - // Simple patterns that are DEFINITELY invalid for tldraw: - // "b1", "c1", "d1" etc - these are old non-fractional indices - if (/^[b-z]\d$/i.test(index)) { - return false - } - - // Valid tldraw indices should start with lowercase 'a' followed by digits - // and optionally more alphanumeric characters for the fractional part - // Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr" - if (/^a\d/.test(index)) { + // Valid fractional index: lowercase letter followed by alphanumeric characters + if (/^[a-z][a-zA-Z0-9]+$/.test(index)) { return true } - // Also allow 'Z' prefix for very high indices (though rare) - if (/^Z[a-z]/i.test(index)) { + // Also allow uppercase prefix for negative/very high indices + if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) { return true } - // If none of the above, it's likely invalid return false } diff --git a/src/automerge/MinimalSanitization.ts b/src/automerge/MinimalSanitization.ts index ece4131..1613e85 100644 --- a/src/automerge/MinimalSanitization.ts +++ b/src/automerge/MinimalSanitization.ts @@ -23,13 +23,15 @@ function minimalSanitizeRecord(record: any): any { // NOTE: Index assignment is handled by assignSequentialIndices() during format conversion // Here we only ensure index exists with a valid format, not strictly validate // This preserves layer order that was established during conversion - // Valid formats: a1, a2, a10, a1V, a1Lz, etc. (fractional indexing) + // tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc. + // - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.) + // - Uppercase (A-Z) for negative/special indices if (!sanitized.index || typeof sanitized.index !== 'string' || sanitized.index.length === 0) { // Only assign default if truly missing sanitized.index = 'a1' - } else if (!/^a\d/.test(sanitized.index) && !/^Z[a-z]/i.test(sanitized.index)) { - // Accept any index starting with 'a' + digit, or 'Z' prefix - // Only reset clearly invalid formats + } else if (!/^[a-zA-Z][a-zA-Z0-9]+$/.test(sanitized.index)) { + // Accept any letter followed by alphanumeric characters + // Only reset clearly invalid formats (e.g., numbers, empty, single char) console.warn(`⚠️ MinimalSanitization: Invalid index format "${sanitized.index}" for shape ${sanitized.id}`) sanitized.index = 'a1' } diff --git a/src/automerge/useAutomergeSyncRepo.ts b/src/automerge/useAutomergeSyncRepo.ts index 3b393ef..23438bd 100644 --- a/src/automerge/useAutomergeSyncRepo.ts +++ b/src/automerge/useAutomergeSyncRepo.ts @@ -20,31 +20,23 @@ import { getDocumentId, saveDocumentId } from "./documentIdMapping" function isValidTldrawIndex(index: string): boolean { if (!index || typeof index !== 'string' || index.length === 0) return false - // The first character indicates the integer part length: - // 'a' = 1 digit, 'b' = 2 digits, etc. for positive integers - // 'Z' = 1 digit, 'Y' = 2 digits, etc. for negative integers - // But for normal shapes, 'a' followed by a digit is the most common pattern + // tldraw uses fractional indexing where: + // - First character is a lowercase letter indicating integer part length (a=1, b=2, c=3, etc.) + // - Followed by alphanumeric characters for the value and optional jitter + // Examples: "a0", "a1", "b10", "b99", "c100", "a1V4rr", "b10Lz" + // + // Also uppercase letters for negative indices (Z=1, Y=2, etc.) - // Simple patterns that are DEFINITELY invalid for tldraw: - // "b1", "c1", "d1" etc - these are old non-fractional indices (single letter + single digit) - // These were used before tldraw switched to fractional indexing - if (/^[b-z]\d$/i.test(index)) { - return false - } - - // Valid tldraw indices should start with lowercase 'a' followed by digits - // and optionally more alphanumeric characters for the fractional/jitter part - // Examples from actual tldraw: "a0", "a1", "a24sT", "a1V4rr" - if (/^a\d/.test(index)) { + // Valid fractional index: lowercase letter followed by alphanumeric characters + if (/^[a-z][a-zA-Z0-9]+$/.test(index)) { return true } - // Also allow 'Z' prefix for very high indices (though rare) - if (/^Z[a-z]/i.test(index)) { + // Also allow uppercase prefix for negative/very high indices + if (/^[A-Z][a-zA-Z0-9]+$/.test(index)) { return true } - // If none of the above, it's likely invalid return false } diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 54fec9d..4fc7ddc 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -68,20 +68,18 @@ import "@/css/style.css" import "@/css/obsidian-browser.css" // Helper to validate and fix tldraw IndexKey format -// Valid: "a0", "a1", "a24sT", "a1V4rr" - Invalid: "b1", "c1" (old format) +// tldraw uses fractional indexing: a0, a1, b10, c100, a1V4rr, etc. +// - First letter (a-z) indicates integer part length (a=1 digit, b=2 digits, etc.) +// - Uppercase (A-Z) for negative/special indices function sanitizeIndex(index: any): IndexKey { if (!index || typeof index !== 'string' || index.length === 0) { return 'a1' as IndexKey } - // Old format "b1", "c1" etc are invalid (single letter + single digit) - if (/^[b-z]\d$/i.test(index)) { - return 'a1' as IndexKey - } - // Valid: starts with 'a' followed by at least one digit - if (/^a\d/.test(index)) { + // Valid: letter followed by alphanumeric characters + if (/^[a-zA-Z][a-zA-Z0-9]+$/.test(index)) { return index as IndexKey } - // Fallback + // Fallback for invalid formats return 'a1' as IndexKey } diff --git a/worker/AutomergeDurableObject.ts b/worker/AutomergeDurableObject.ts index ba85b70..260dc2c 100644 --- a/worker/AutomergeDurableObject.ts +++ b/worker/AutomergeDurableObject.ts @@ -747,11 +747,12 @@ export class AutomergeDurableObject { shapesNeedingIndex.sort((a, b) => a.originalIndex - b.originalIndex) // Check if shapes already have valid indices we should preserve - // Valid index: starts with 'a' followed by digits, optionally followed by alphanumeric jitter + // Valid tldraw fractional index: starts with a lowercase letter followed by alphanumeric characters + // Examples: a1, a2, b1, c10, a1V, a1Lz, etc. (the letter increments as indices grow) const isValidIndex = (idx: any): boolean => { if (!idx || typeof idx !== 'string' || idx.length === 0) return false - // Valid fractional index format: a1, a2, a1V, a10, a1Lz, etc. - if (/^a\d/.test(idx)) return true + // Valid fractional index format: lowercase letter followed by alphanumeric (a1, b1, c10, a1V, etc.) + if (/^[a-z][a-zA-Z0-9]+$/.test(idx)) return true // Also allow 'Z' prefix for very high indices if (/^Z[a-z]/i.test(idx)) return true return false