export const flattenText = (value) => { if (typeof value === 'string') return value; if (Array.isArray(value)) return value.map(flattenText).join(''); if (value && typeof value === 'object') { return `${flattenText(value.text)}${flattenText(value.content)}`; } return ''; }; export const firstNonEmptyText = (...values) => { for (const value of values) { const text = flattenText(value).trim(); if (text) return text; } return ''; }; export const normalizeMindmapDimension = (value, kind) => { const raw = typeof value === 'number' ? value : typeof value === 'string' ? Number(value.trim().replace(/px$/i, '')) : NaN; if (!Number.isFinite(raw)) return null; const rounded = Math.round(raw); const min = kind === 'height' ? 240 : 900; return rounded >= min ? rounded : null; }; // 过渡适配(TODO step-4):legacy→Tiptap inline marks 转换函数组。 // AST/block 迁移 complete 后,前端应直接消费 block document 中的 // tiptap 格式 marks(已由 Rust 侧 local_markdown_parser 输出), // 不再需要此 JS 侧样式→marks 适配层。 export const legacyStylesToTiptapMarks = (styles) => { if (!styles || typeof styles !== 'object') return []; const marks = []; if (styles.bold) marks.push({ type: 'bold' }); if (styles.italic) marks.push({ type: 'italic' }); if (styles.underline) marks.push({ type: 'underline' }); if (styles.strike || styles.strikethrough) marks.push({ type: 'strike' }); if (styles.code || styles.inlineCode) marks.push({ type: 'code' }); const href = typeof styles.link === 'string' && styles.link.trim() ? styles.link.trim() : typeof styles.href === 'string' && styles.href.trim() ? styles.href.trim() : ''; if (href) marks.push({ type: 'link', attrs: { href } }); return marks; }; export const legacyMarkArrayToTiptapMarks = (inlineMarks) => { if (!Array.isArray(inlineMarks)) return []; return inlineMarks.flatMap((mark) => { if (!mark || typeof mark !== 'object') return []; if (mark.type === 'bold' || mark.type === 'italic' || mark.type === 'underline' || mark.type === 'strike' || mark.type === 'code') { return [{ type: mark.type }]; } if (mark.type === 'link') { const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : ''; return href ? [{ type: 'link', attrs: { href } }] : []; } return []; }); }; export const mergeTiptapMarks = (...groups) => { const seen = new Set(); return groups.flat().filter((mark) => { const key = `${mark.type}:${JSON.stringify(mark.attrs || {})}`; if (seen.has(key)) return false; seen.add(key); return true; }); }; export const legacyInlineContentToTiptap = (value) => { if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : []; if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap); if (value && typeof value === 'object') { const text = typeof value.text === 'string' ? value.text : ''; if (text) { const marks = mergeTiptapMarks( legacyStylesToTiptapMarks(value.styles), legacyMarkArrayToTiptapMarks(value.marks) ); return [{ type: 'text', text, ...(marks.length ? { marks } : {}) }]; } return legacyInlineContentToTiptap(value.content || value.contentNodes); } return []; }; export const legacyBlockToTiptap = (block, index = 0) => { const type = typeof block?.type === 'string' ? block.type : 'paragraph'; const blockId = typeof block?.id === 'string' && block.id.trim() ? block.id.trim() : typeof block?.blockId === 'string' && block.blockId.trim() ? block.blockId.trim() : `block-${index + 1}`; const content = legacyInlineContentToTiptap(block?.content ?? block?.contentNodes); const textAlign = typeof block?.props?.textAlign === 'string' ? block.props.textAlign : typeof block?.props?.text_align === 'string' ? block.props.text_align : undefined; const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs; const nestedChildren = Array.isArray(block?.children) ? block.children.map((child, childIndex) => legacyBlockToTiptap(child, childIndex)).filter(Boolean) : []; const withListChildren = (itemType, listType, attrs = {}) => ({ type: listType, attrs: { blockId }, content: [{ type: itemType, attrs: { blockId, ...attrs }, content: [ { type: 'paragraph', attrs: { blockId }, content }, ...nestedChildren, ], }], }); if (type === 'heading') { const level = Number(block?.props?.level || block?.level || 1) || 1; const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {}; return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content }; } if (type === 'bulletListItem' || type === 'bullet_list_item') return withListChildren('listItem', 'bulletList'); if (type === 'numberedListItem' || type === 'numbered_list_item') return withListChildren('listItem', 'orderedList'); if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') { return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) }); } if (type === 'quote' || type === 'blockquote') { return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] }; } if (type === 'codeBlock' || type === 'code_block') { return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content }; } if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') return { type: 'horizontalRule', attrs: { blockId } }; if (type === 'mindmap') { const attrs = block?.attrs && typeof block.attrs === 'object' ? block.attrs : null; const data = block?.props?.data && typeof block.props.data === 'object' ? block.props.data : null; const mindmapId = firstNonEmptyText( block?.props?.mindmapId, block?.props?.mindmap_id, block?.props?.sourcePath, block?.props?.source_path, attrs?.mindmapId, attrs?.mindmap_id, attrs?.sourcePath, attrs?.source_path, block?.mindmapId, block?.mindmap_id, data?.mindmapId, data?.mindmap_id, data?.id ); const rootNodeId = firstNonEmptyText(block?.props?.rootNodeId, block?.props?.root_node_id, attrs?.rootNodeId, attrs?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root'; const mindmapWidth = normalizeMindmapDimension(block?.props?.mindmapWidth ?? block?.props?.mindmap_width ?? attrs?.mindmapWidth ?? attrs?.mindmap_width ?? data?.mindmapWidth ?? data?.mindmap_width, 'width'); const mindmapHeight = normalizeMindmapDimension(block?.props?.mindmapHeight ?? block?.props?.mindmap_height ?? attrs?.mindmapHeight ?? attrs?.mindmap_height ?? data?.mindmapHeight ?? data?.mindmap_height, 'height'); return { type: 'paragraph', attrs: withTextAlign({ blockId, mnoteBlockType: 'mindmap', mindmapId, rootNodeId, ...(mindmapWidth !== null ? { mindmapWidth } : {}), ...(mindmapHeight !== null ? { mindmapHeight } : {}), }), }; } if (type === 'media') { const sourcePath = firstNonEmptyText(block?.props?.sourcePath, block?.props?.url, block?.props?.src); const name = firstNonEmptyText(block?.props?.name, block?.props?.fileName, block?.props?.title, sourcePath); const mediaContent = name ? [{ type: 'text', text: name, ...(sourcePath ? { marks: [{ type: 'link', attrs: { href: sourcePath } }] } : {}), }] : content; return { type: 'paragraph', attrs: withTextAlign({ blockId }), content: mediaContent }; } if (type === 'table') { const tableSnapshot = block?.props?.tiptapTable; if (tableSnapshot && typeof tableSnapshot === 'object' && tableSnapshot.type === 'table') return tableSnapshot; return { type: 'table', content: [{ type: 'tableRow', content: [{ type: 'tableCell', attrs: { colspan: 1, rowspan: 1, colwidth: null }, content: [{ type: 'paragraph', content }], }], }], }; } if (type === 'toc' || type === 'tocNode' || type === 'toc_node') { const tocSnapshot = block?.props?.tiptapTocNode || block?.props?.tiptapToc; if (tocSnapshot && typeof tocSnapshot === 'object' && tocSnapshot.type === 'tocNode') return tocSnapshot; return { type: 'tocNode', attrs: { topOffset: Number(block?.props?.topOffset || block?.props?.top_offset || 0) || 0, maxShowCount: Number(block?.props?.maxShowCount || block?.props?.max_show_count || 20) || 20, showTitle: block?.props?.showTitle !== false, }, }; } if (type === 'image') { const imageSnapshot = block?.props?.tiptapImage; if (imageSnapshot && typeof imageSnapshot === 'object' && imageSnapshot.type === 'image') return imageSnapshot; const attrs = { src: String(block?.props?.src || block?.src || ''), alt: block?.props?.alt || block?.alt || null, title: block?.props?.title || block?.title || null, }; return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null; } return { type: 'paragraph', attrs: withTextAlign({ blockId }), content }; }; export const textToTiptapDocument = (text) => ({ type: 'doc', content: [{ type: 'paragraph', content: text ? [{ type: 'text', text }] : [], }], }); export const isTiptapDocument = (content) => ( content && typeof content === 'object' && !Array.isArray(content) && content.type === 'doc' ); export const mindmapDomDescriptors = (root) => { if (!(root instanceof HTMLElement)) return []; return Array.from(root.querySelectorAll('[data-testid="mnote-mindmap-editor-root"]')) .flatMap((node) => { if (!(node instanceof HTMLElement)) return []; const mindmapId = typeof node.dataset.mnoteMindmapId === 'string' ? node.dataset.mnoteMindmapId.trim() : ''; if (!mindmapId) return []; const rootNodeId = typeof node.dataset.mnoteRootNodeId === 'string' && node.dataset.mnoteRootNodeId.trim() ? node.dataset.mnoteRootNodeId.trim() : 'root'; const mindmapWidth = normalizeMindmapDimension(node.dataset.mnoteMindmapWidth, 'width'); const mindmapHeight = normalizeMindmapDimension(node.dataset.mnoteMindmapHeight, 'height'); return [{ mindmapId, rootNodeId, mindmapWidth, mindmapHeight }]; }); }; export const hydrateMindmapAttrsFromDom = (tiptapDocument, root) => { if (!isTiptapDocument(tiptapDocument) || !Array.isArray(tiptapDocument.content)) return tiptapDocument; const descriptors = mindmapDomDescriptors(root); if (!descriptors.length) return tiptapDocument; let index = 0; for (const node of tiptapDocument.content) { if (node?.type !== 'paragraph' || node?.attrs?.mnoteBlockType !== 'mindmap') continue; const descriptor = descriptors[index]; index += 1; if (!descriptor) continue; node.attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {}; if (typeof node.attrs.mindmapId !== 'string' || !node.attrs.mindmapId.trim()) { node.attrs.mindmapId = descriptor.mindmapId; } if (typeof node.attrs.rootNodeId !== 'string' || !node.attrs.rootNodeId.trim()) { node.attrs.rootNodeId = descriptor.rootNodeId; } if (normalizeMindmapDimension(node.attrs.mindmapWidth, 'width') === null && descriptor.mindmapWidth !== null) { node.attrs.mindmapWidth = descriptor.mindmapWidth; } if (normalizeMindmapDimension(node.attrs.mindmapHeight, 'height') === null && descriptor.mindmapHeight !== null) { node.attrs.mindmapHeight = descriptor.mindmapHeight; } } return tiptapDocument; }; export const toTiptapDocument = (content, fallbackText = '') => { if (isTiptapDocument(content)) return content; const blocks = Array.isArray(content) ? content : Array.isArray(content?.blocks) ? content.blocks : []; const nodes = blocks.map(legacyBlockToTiptap).filter(Boolean); if (nodes.length) return { type: 'doc', content: nodes }; return textToTiptapDocument(fallbackText); }; export const decodeLocalIdSegment = (segment) => { const encoded = String(segment || '').replace(/~([0-9a-fA-F]{2})/g, '%$1'); try { return decodeURIComponent(encoded); } catch (_) { return String(segment || '').replace(/~2F/g, '/').replace(/~20/g, ' '); } }; export const localMarkdownRelativePathFromDocumentId = (documentId) => { const raw = String(documentId || '').trim(); const segment = raw.startsWith('local-md:') ? raw.slice('local-md:'.length) : raw; return decodeLocalIdSegment(segment).replace(/^\/+/, ''); }; export const localMarkdownDocumentIdFromRelativePath = (relativePath) => { const normalized = String(relativePath || '').trim().replace(/^\/+/, ''); if (!normalized) return ''; return 'local-md:' + normalized .split('/') .map((part) => part.replace(/ /g, '~20')) .join('~2F'); }; export const localMarkdownDirectoryFromDocumentId = (documentId) => { const relativePath = localMarkdownRelativePathFromDocumentId(documentId); const slash = relativePath.lastIndexOf('/'); return slash >= 0 ? relativePath.slice(0, slash) : ''; }; export const isExternalOrSpecialUrl = (value) => { const text = String(value || '').trim(); return !text || text.startsWith('#') || text.startsWith('data:') || text.startsWith('blob:') || text.startsWith('mailto:') || text.startsWith('http://') || text.startsWith('https://') || text.startsWith('/api/'); }; export const normalizeLocalAssetRelativePath = (value, context) => { const text = String(value || '').trim(); if (!text || isExternalOrSpecialUrl(text)) return text; if (text.startsWith('/')) return text.replace(/^\/+/, ''); const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId); return (baseDir ? `${baseDir}/${text}` : text) .split('/') .filter((part) => part && part !== '.') .join('/'); }; export const localFileOpenUrlForTiptap = (value, context) => { if (!context || context.sourceKind !== 'local_folder' || !context.rootUri) return value; const relativePath = normalizeLocalAssetRelativePath(value, context); if (!relativePath || isExternalOrSpecialUrl(relativePath)) return value; const url = new URL('/api/local-folder/files/open', window.location.origin); url.searchParams.set('rootUri', context.rootUri); url.searchParams.set('path', relativePath); return url.toString(); }; export const localFileOpenPathFromTiptapHref = (href) => { try { const url = new URL(String(href || ''), window.location.origin); if (url.pathname !== '/api/local-folder/files/open') return ''; return String(url.searchParams.get('path') || '').trim(); } catch (_) { return ''; } }; export const fileNameFromPath = (path) => { const value = String(path || '').trim(); return value.includes('/') ? value.split('/').pop() : value; }; export const attachmentClassForFileName = (fileName) => { const name = String(fileName || '').trim().toLowerCase(); const ext = name.includes('.') ? name.split('.').pop() : ''; if (['doc', 'docx', 'odt', 'rtf'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word'; if (['ppt', 'pptx', 'odp'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt'; if (['xls', 'xlsx', 'ods', 'csv'].includes(ext)) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet'; if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf'; if (['md', 'markdown', 'txt', 'json', 'js', 'ts', 'tsx', 'jsx', 'rs', 'py', 'go', 'java', 'kt', 'swift', 'c', 'cpp', 'h', 'hpp', 'css', 'scss', 'html', 'xml', 'yaml', 'yml', 'toml', 'ini', 'sh', 'bash', 'zsh', 'sql', 'vue', 'svelte'].includes(ext)) { return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code'; } return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file'; }; export const mergeClassNames = (...values) => { const seen = new Set(); return values .flatMap((value) => String(value || '').split(/\s+/)) .filter((name) => { if (!name || seen.has(name)) return false; seen.add(name); return true; }) .join(' '); }; export const localAttachmentClassForTiptapHref = (href) => { const localFilePath = localFileOpenPathFromTiptapHref(href); if (localFilePath) return attachmentClassForFileName(fileNameFromPath(localFilePath)); const value = String(href || '').trim(); if (!value || isExternalOrSpecialUrl(value)) return ''; if (value.startsWith('../') || value.includes('/../')) return ''; return attachmentClassForFileName(fileNameFromPath(value)); }; const attachmentRefsFromContext = (context) => { if (Array.isArray(context?.attachmentRefs)) return context.attachmentRefs; if (Array.isArray(context?.attachment_refs)) return context.attachment_refs; if (Array.isArray(context?.body?.attachmentRefs)) return context.body.attachmentRefs; if (Array.isArray(context?.body?.attachment_refs)) return context.body.attachment_refs; if (Array.isArray(context?.latestAggregate?.body?.attachmentRefs)) return context.latestAggregate.body.attachmentRefs; if (Array.isArray(context?.latestAggregate?.body?.attachment_refs)) return context.latestAggregate.body.attachment_refs; if (Array.isArray(context?.aggregate?.body?.attachmentRefs)) return context.aggregate.body.attachmentRefs; if (Array.isArray(context?.aggregate?.body?.attachment_refs)) return context.aggregate.body.attachment_refs; return []; }; const attachmentRefForTiptapHref = (href, context) => { const value = String(href || '').trim(); if (!value) return null; return attachmentRefsFromContext(context).find((ref) => ( ref && typeof ref === 'object' && ( String(ref.rawHref || '') === value || String(ref.normalizedHref || '') === value || String(ref.resolvedUri || '') === value ) )) || null; }; const withAttachmentProjectionAttrs = (attrs, attachmentRef) => { if (!attachmentRef || typeof attachmentRef !== 'object') return attrs; const next = { ...attrs }; if (attachmentRef.exists === false) { next.class = mergeClassNames(next.class, 'mnote-uploaded-attachment-missing'); next['data-mnote-attachment-missing'] = 'true'; next['aria-label'] = `${String(attachmentRef.label || '附件')}(文件不存在)`; } if (attachmentRef.authorized === false) { next.class = mergeClassNames(next.class, 'mnote-uploaded-attachment-unauthorized'); next['data-mnote-attachment-unauthorized'] = 'true'; next['aria-label'] = `${String(attachmentRef.label || '附件')}(无权访问)`; } return next; }; export const localizeTiptapAssetUrls = (node, context) => { if (!node || typeof node !== 'object') return node; if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') { const originalSrc = String(node.attrs.src || '').trim(); node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(originalSrc, context), mnoteMarkdownSrc: node.attrs.mnoteMarkdownSrc || originalSrc, }; } if (Array.isArray(node.marks)) { node.marks = node.marks.map((mark) => { if (!mark || mark.type !== 'link' || !mark.attrs || typeof mark.attrs.href !== 'string') return mark; const href = String(mark.attrs.href || '').trim(); const attachmentClass = localAttachmentClassForTiptapHref(href); const attachmentRef = attachmentRefForTiptapHref(href, context); const attrs = attachmentClass ? withAttachmentProjectionAttrs({ ...mark.attrs, href, class: mergeClassNames(mark.attrs.class, attachmentClass), target: mark.attrs.target || '_blank', rel: mark.attrs.rel || 'noopener noreferrer nofollow', }, attachmentRef) : { ...mark.attrs, href }; return { ...mark, attrs }; }); } if (Array.isArray(node.content)) { node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context)); } return node; }; export const pageBodyTiptapDocumentSource = (body, fallbackText = '') => { const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null; if (blockDocument) return 'page_aggregate.block_document'; if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) { return 'local_markdown.content'; } if (body?.content) return 'compat.legacy_content'; return fallbackText ? 'degraded.fallback_text' : 'empty'; }; export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => { const projectionContext = { ...(context || {}), body }; const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null; if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), projectionContext); return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), projectionContext); }; export const inlineTextNodes = (node) => { if (!node || typeof node !== 'object') return []; if (Array.isArray(node.content)) { return node.content.flatMap((child) => { if (child?.type === 'text') { const text = typeof child.text === 'string' ? child.text : ''; if (!text) return []; const styles = {}; const marks = []; for (const mark of Array.isArray(child.marks) ? child.marks : []) { if (mark?.type === 'bold') { styles.bold = true; marks.push('bold'); } if (mark?.type === 'italic') { styles.italic = true; marks.push('italic'); } if (mark?.type === 'underline') { styles.underline = true; marks.push('underline'); } if (mark?.type === 'strike') { styles.strike = true; marks.push('strike'); } if (mark?.type === 'code') { styles.code = true; marks.push('code'); } if (mark?.type === 'link') { const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : ''; if (href) styles.link = href; } } return [{ payload: { type: 'text', text, ...(marks.length ? { marks } : {}) }, attrs: Object.keys(styles).length ? { styles } : {}, type: 'text', text, ...(Object.keys(styles).length ? { styles } : {}), }]; } if (child?.type === 'hardBreak') return [{ payload: { type: 'hard_break' }, attrs: {}, type: 'text', text: '\n' }]; return inlineTextNodes(child); }); } return []; }; export const firstChild = (node) => Array.isArray(node?.content) ? node.content[0] : null; export const blockIdOf = (node, index) => { const raw = typeof node?.attrs?.blockId === 'string' ? node.attrs.blockId.trim() : ''; return raw || `block-${index + 1}`; }; export const mindmapPropsFromAttrs = (attrs, fallbackMindmapId) => { const data = attrs?.data && typeof attrs.data === 'object' ? attrs.data : {}; const mindmapId = firstNonEmptyText( attrs?.mindmapId, attrs?.mindmap_id, attrs?.sourcePath, attrs?.source_path, data?.mindmapId, data?.mindmap_id, data?.id, fallbackMindmapId ); const rootNodeId = firstNonEmptyText(attrs?.rootNodeId, attrs?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root'; const projectionVersion = Number(attrs?.projectionVersion ?? attrs?.projection_version); const mindmapWidth = normalizeMindmapDimension(attrs?.mindmapWidth ?? attrs?.mindmap_width ?? data?.mindmapWidth ?? data?.mindmap_width, 'width'); const mindmapHeight = normalizeMindmapDimension(attrs?.mindmapHeight ?? attrs?.mindmap_height ?? data?.mindmapHeight ?? data?.mindmap_height, 'height'); return { mindmapId, rootNodeId, ...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}), ...(mindmapWidth !== null ? { mindmapWidth } : {}), ...(mindmapHeight !== null ? { mindmapHeight } : {}), }; }; export const tiptapNodeToEditorBlock = (node, index) => { const blockId = blockIdOf(node, index); if (node?.type === 'paragraph' && node?.attrs?.mnoteBlockType === 'mindmap') { const mindmapId = firstNonEmptyText( node?.attrs?.mindmapId, node?.attrs?.mindmap_id, node?.attrs?.sourcePath, node?.attrs?.source_path, blockId ); return { blockId, blockType: 'mindmap', props: { ...mindmapPropsFromAttrs(node?.attrs, blockId), sourcePath: mindmapId, }, contentNodes: [], childBlockIds: [], }; } if (node?.type === 'paragraph') return { blockId, blockType: 'paragraph', props: {}, contentNodes: inlineTextNodes(node), childBlockIds: [] }; if (node?.type === 'heading') { const level = Math.max(1, Math.min(6, Number(node?.attrs?.level || 1) || 1)); return { blockId, blockType: 'heading', props: { headingLevel: level }, contentNodes: inlineTextNodes(node), childBlockIds: [] }; } if (node?.type === 'bulletList') return { blockId, blockType: 'bullet_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] }; if (node?.type === 'orderedList') return { blockId, blockType: 'numbered_list_item', props: {}, contentNodes: inlineTextNodes(firstChild(firstChild(node))), childBlockIds: [] }; if (node?.type === 'taskList') { const taskItem = firstChild(node); return { blockId, blockType: 'todo', props: { checked: Boolean(taskItem?.attrs?.checked) }, contentNodes: inlineTextNodes(firstChild(taskItem)), childBlockIds: [] }; } if (node?.type === 'blockquote') return { blockId, blockType: 'quote', props: {}, contentNodes: inlineTextNodes(firstChild(node)), childBlockIds: [] }; if (node?.type === 'codeBlock') return { blockId, blockType: 'code_block', props: { language: typeof node?.attrs?.language === 'string' ? node.attrs.language : null }, contentNodes: inlineTextNodes(node), childBlockIds: [] }; if (node?.type === 'horizontalRule') return { blockId, blockType: 'divider', props: {}, contentNodes: [], childBlockIds: [] }; if (node?.type === 'image') return { blockId, blockType: 'image', props: { src: node?.attrs?.mnoteMarkdownSrc || node?.attrs?.src || '', alt: node?.attrs?.alt || null, title: node?.attrs?.title || null, tiptapImage: node }, contentNodes: [], childBlockIds: [] }; if (node?.type === 'tocNode') return { blockId, blockType: 'toc', props: { tiptapTocNode: node }, contentNodes: [], childBlockIds: [] }; if (node?.type === 'table') return { blockId, blockType: 'table', props: { tiptapTable: node }, contentNodes: inlineTextNodes(node), childBlockIds: [] }; return null; }; export const editorDocumentFromTiptapDocument = (bootstrap, tiptapDocument) => { const content = Array.isArray(tiptapDocument?.content) ? tiptapDocument.content : []; const blocks = content.map(tiptapNodeToEditorBlock).filter(Boolean); return { documentId: bootstrap.documentId, rootBlockIds: blocks.map((block) => block.blockId), blocks, }; }; export const legacyBlocksFromEditorDocument = (editorDocument) => ( Array.isArray(editorDocument?.blocks) ? editorDocument.blocks : [] ).map((block) => ({ id: block.blockId, type: block.blockType, props: block.blockType === 'heading' ? { level: block.props?.headingLevel || 1 } : block.blockType === 'todo' ? { checked: Boolean(block.props?.checked) } : block.blockType === 'code_block' ? { language: block.props?.language || null } : block.blockType === 'mindmap' ? mindmapPropsFromAttrs(block.props || {}, block.blockId) : block.blockType === 'image' ? { ...(block.props || {}) } : block.blockType === 'toc' ? { ...(block.props || {}) } : block.blockType === 'table' ? { ...(block.props || {}) } : undefined, content: block.blockType === 'mindmap' ? '' : Array.isArray(block.contentNodes) ? block.contentNodes.map((node) => { if (!node || typeof node !== 'object') return null; const payload = node.payload && typeof node.payload === 'object' ? node.payload : {}; const text = typeof payload.text === 'string' ? payload.text : payload.type === 'hard_break' ? '\n' : typeof node.text === 'string' ? node.text : ''; if (!text) return null; const attrs = node.attrs && typeof node.attrs === 'object' ? node.attrs : {}; const styles = attrs.styles && typeof attrs.styles === 'object' ? attrs.styles : node.styles && typeof node.styles === 'object' ? node.styles : null; const marks = Array.isArray(payload.marks) ? payload.marks : []; return { type: 'text', text, ...(styles ? { styles } : {}), ...(marks.length ? { marks } : {}), }; }).filter(Boolean) : '', })); export const conflictDetectionKeyFromBody = (body) => typeof body?.conflictDetectionKey === 'string' ? body.conflictDetectionKey : typeof body?.conflict_detection_key === 'string' ? body.conflict_detection_key : typeof body?.fileVersion === 'string' ? body.fileVersion : typeof body?.file_version === 'string' ? body.file_version : null; export const conflictDetectionKeyBelongsToSession = (session, key) => { if (!session || session.sourceKind !== 'local_folder') return true; const value = String(key || '').trim(); if (!value) return false; return value.startsWith(`local-md:${session.documentId}:`); }; export const revisionFromConflictKey = (value) => { const match = String(value || '').match(/:(\d+)$/); return match ? Number(match[1]) : null; };