refactor: split document tiptap conversion runtime
This commit is contained in:
+1
-1
@@ -120,7 +120,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tests::mno
|
|||||||
拆分项:
|
拆分项:
|
||||||
|
|
||||||
- [ ] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。
|
- [ ] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。
|
||||||
- [ ] B2. `document-tiptap-conversion-runtime.js`:legacy block / inline content / marks 转 Tiptap document。
|
- [x] B2. `document-tiptap-conversion-runtime.js`:legacy block / inline content / marks 转 Tiptap document。
|
||||||
- [ ] B3. `document-resource-tab-runtime.js`:resource tab registry、MRU、close guard、resource text/image/frame editor mount。
|
- [ ] B3. `document-resource-tab-runtime.js`:resource tab registry、MRU、close guard、resource text/image/frame editor mount。
|
||||||
- [ ] B4. `document-mindmap-host-runtime.js`:primary mindmap object shell、mindmap resource tab mount/unmount。
|
- [ ] B4. `document-mindmap-host-runtime.js`:primary mindmap object shell、mindmap resource tab mount/unmount。
|
||||||
- [ ] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。
|
- [ ] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
import {
|
||||||
|
conflictDetectionKeyBelongsToSession,
|
||||||
|
conflictDetectionKeyFromBody,
|
||||||
|
editorDocumentFromTiptapDocument,
|
||||||
|
flattenText,
|
||||||
|
hydrateMindmapAttrsFromDom,
|
||||||
|
legacyBlocksFromEditorDocument,
|
||||||
|
legacyInlineContentToTiptap,
|
||||||
|
pageBodyTiptapDocument,
|
||||||
|
revisionFromConflictKey,
|
||||||
|
toTiptapDocument,
|
||||||
|
} from './document-tiptap-conversion-runtime.js';
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
|
const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
|
||||||
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
||||||
@@ -200,555 +213,7 @@
|
|||||||
return window.__mnoteLeptosTiptapRuntimePromise;
|
return window.__mnoteLeptosTiptapRuntimePromise;
|
||||||
};
|
};
|
||||||
|
|
||||||
const flattenText = (value) => {
|
// Tiptap/legacy conversion helpers live in document-tiptap-conversion-runtime.js.
|
||||||
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 '';
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstNonEmptyText = (...values) => {
|
|
||||||
for (const value of values) {
|
|
||||||
const text = flattenText(value).trim();
|
|
||||||
if (text) return text;
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
};
|
|
||||||
|
|
||||||
// 过渡适配(TODO step-4):legacy→Tiptap inline marks 转换函数组。
|
|
||||||
// AST/block 迁移 complete 后,前端应直接消费 block document 中的
|
|
||||||
// tiptap 格式 marks(已由 Rust 侧 local_markdown_parser 输出),
|
|
||||||
// 不再需要此 JS 侧样式→marks 适配层。
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
};
|
|
||||||
|
|
||||||
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 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,
|
|
||||||
block?.mindmapId,
|
|
||||||
block?.mindmap_id,
|
|
||||||
data?.mindmapId,
|
|
||||||
data?.mindmap_id,
|
|
||||||
data?.id
|
|
||||||
);
|
|
||||||
const rootNodeId = firstNonEmptyText(block?.props?.rootNodeId, block?.props?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
|
|
||||||
return {
|
|
||||||
type: 'paragraph',
|
|
||||||
attrs: withTextAlign({
|
|
||||||
blockId,
|
|
||||||
mnoteBlockType: 'mindmap',
|
|
||||||
mindmapId,
|
|
||||||
rootNodeId,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
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 };
|
|
||||||
};
|
|
||||||
|
|
||||||
const textToTiptapDocument = (text) => ({
|
|
||||||
type: 'doc',
|
|
||||||
content: [{
|
|
||||||
type: 'paragraph',
|
|
||||||
content: text ? [{ type: 'text', text }] : [],
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
|
|
||||||
const isTiptapDocument = (content) => (
|
|
||||||
content &&
|
|
||||||
typeof content === 'object' &&
|
|
||||||
!Array.isArray(content) &&
|
|
||||||
content.type === 'doc'
|
|
||||||
);
|
|
||||||
|
|
||||||
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';
|
|
||||||
return [{ mindmapId, rootNodeId }];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return tiptapDocument;
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
|
||||||
};
|
|
||||||
|
|
||||||
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, ' ');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const localMarkdownRelativePathFromDocumentId = (documentId) => {
|
|
||||||
const raw = String(documentId || '').trim();
|
|
||||||
const segment = raw.startsWith('local-md:') ? raw.slice('local-md:'.length) : raw;
|
|
||||||
return decodeLocalIdSegment(segment).replace(/^\/+/, '');
|
|
||||||
};
|
|
||||||
|
|
||||||
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');
|
|
||||||
};
|
|
||||||
|
|
||||||
const localMarkdownDirectoryFromDocumentId = (documentId) => {
|
|
||||||
const relativePath = localMarkdownRelativePathFromDocumentId(documentId);
|
|
||||||
const slash = relativePath.lastIndexOf('/');
|
|
||||||
return slash >= 0 ? relativePath.slice(0, slash) : '';
|
|
||||||
};
|
|
||||||
|
|
||||||
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/');
|
|
||||||
};
|
|
||||||
|
|
||||||
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('/');
|
|
||||||
};
|
|
||||||
|
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
const localizeTiptapAssetUrls = (node, context) => {
|
|
||||||
if (!node || typeof node !== 'object') return node;
|
|
||||||
if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') {
|
|
||||||
node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) };
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
return { ...mark, attrs: { ...mark.attrs, href: localFileOpenUrlForTiptap(mark.attrs.href, context) } };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (Array.isArray(node.content)) {
|
|
||||||
node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context));
|
|
||||||
}
|
|
||||||
return node;
|
|
||||||
};
|
|
||||||
|
|
||||||
const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
|
|
||||||
if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) {
|
|
||||||
return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
|
|
||||||
}
|
|
||||||
const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
|
|
||||||
if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), context);
|
|
||||||
return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), context);
|
|
||||||
};
|
|
||||||
|
|
||||||
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 [];
|
|
||||||
};
|
|
||||||
|
|
||||||
const firstChild = (node) => Array.isArray(node?.content) ? node.content[0] : null;
|
|
||||||
const blockIdOf = (node, index) => {
|
|
||||||
const raw = typeof node?.attrs?.blockId === 'string' ? node.attrs.blockId.trim() : '';
|
|
||||||
return raw || `block-${index + 1}`;
|
|
||||||
};
|
|
||||||
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);
|
|
||||||
return {
|
|
||||||
mindmapId,
|
|
||||||
rootNodeId,
|
|
||||||
...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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?.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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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)
|
|
||||||
: '',
|
|
||||||
}));
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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}:`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const revisionFromConflictKey = (value) => {
|
|
||||||
const match = String(value || '').match(/:(\d+)$/);
|
|
||||||
return match ? Number(match[1]) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const normalizeBridgeValue = (value) => {
|
const normalizeBridgeValue = (value) => {
|
||||||
if (value instanceof Map) {
|
if (value instanceof Map) {
|
||||||
const out = {};
|
const out = {};
|
||||||
|
|||||||
@@ -0,0 +1,548 @@
|
|||||||
|
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 '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 过渡适配(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 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,
|
||||||
|
block?.mindmapId,
|
||||||
|
block?.mindmap_id,
|
||||||
|
data?.mindmapId,
|
||||||
|
data?.mindmap_id,
|
||||||
|
data?.id
|
||||||
|
);
|
||||||
|
const rootNodeId = firstNonEmptyText(block?.props?.rootNodeId, block?.props?.root_node_id, data?.rootNodeId, data?.root_node_id) || 'root';
|
||||||
|
return {
|
||||||
|
type: 'paragraph',
|
||||||
|
attrs: withTextAlign({
|
||||||
|
blockId,
|
||||||
|
mnoteBlockType: 'mindmap',
|
||||||
|
mindmapId,
|
||||||
|
rootNodeId,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
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';
|
||||||
|
return [{ mindmapId, rootNodeId }];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 localizeTiptapAssetUrls = (node, context) => {
|
||||||
|
if (!node || typeof node !== 'object') return node;
|
||||||
|
if (node.type === 'image' && node.attrs && typeof node.attrs.src === 'string') {
|
||||||
|
node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) };
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
return { ...mark, attrs: { ...mark.attrs, href: localFileOpenUrlForTiptap(mark.attrs.href, context) } };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Array.isArray(node.content)) {
|
||||||
|
node.content = node.content.map((child) => localizeTiptapAssetUrls(child, context));
|
||||||
|
}
|
||||||
|
return node;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
|
||||||
|
if (String(body?.projectionSource || body?.projection_source || '') === 'local_markdown.content' && body?.content) {
|
||||||
|
return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
|
||||||
|
}
|
||||||
|
const blockDocument = body?.blockDocument || body?.block_document || body?.editorDocument || body?.editor_document || null;
|
||||||
|
if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), context);
|
||||||
|
return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), context);
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
|
return {
|
||||||
|
mindmapId,
|
||||||
|
rootNodeId,
|
||||||
|
...(Number.isFinite(projectionVersion) ? { projectionVersion } : {}),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
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?.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;
|
||||||
|
};
|
||||||
@@ -142,6 +142,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||||
get(web_shell::document_conflict_panel_runtime_asset),
|
get(web_shell::document_conflict_panel_runtime_asset),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js",
|
||||||
|
get(web_shell::document_tiptap_conversion_runtime_asset),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
||||||
get(web_shell::document_editor_adapter_runtime_asset),
|
get(web_shell::document_editor_adapter_runtime_asset),
|
||||||
@@ -559,6 +563,7 @@ mod tests {
|
|||||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
||||||
] {
|
] {
|
||||||
let response = app(false)
|
let response = app(false)
|
||||||
|
|||||||
@@ -855,6 +855,20 @@ pub async fn document_conflict_panel_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn document_tiptap_conversion_runtime_asset() -> Response {
|
||||||
|
const JS: &str = include_str!("../../browser/document-tiptap-conversion-runtime.js");
|
||||||
|
Response::builder()
|
||||||
|
.status(StatusCode::OK)
|
||||||
|
.header(
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
"application/javascript; charset=utf-8",
|
||||||
|
)
|
||||||
|
.header(header::CACHE_CONTROL, "no-store")
|
||||||
|
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||||
|
.body(Body::from(JS))
|
||||||
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn document_editor_adapter_runtime_asset() -> Response {
|
pub async fn document_editor_adapter_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/document-editor-adapter-runtime.js");
|
const JS: &str = include_str!("../../browser/document-editor-adapter-runtime.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
@@ -1278,6 +1292,8 @@ mod tests {
|
|||||||
|
|
||||||
const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str =
|
const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str =
|
||||||
include_str!("../../browser/document-editor-adapter-runtime.js");
|
include_str!("../../browser/document-editor-adapter-runtime.js");
|
||||||
|
const DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS: &str =
|
||||||
|
include_str!("../../browser/document-tiptap-conversion-runtime.js");
|
||||||
|
|
||||||
fn app() -> axum::Router {
|
fn app() -> axum::Router {
|
||||||
build_app(AppState::new(AppConfig {
|
build_app(AppState::new(AppConfig {
|
||||||
@@ -1566,11 +1582,14 @@ mod tests {
|
|||||||
assert!(html.contains("/api/tree/events"));
|
assert!(html.contains("/api/tree/events"));
|
||||||
assert!(html.contains("/api/realtime/ws"));
|
assert!(html.contains("/api/realtime/ws"));
|
||||||
assert!(runtime.contains("syncPageAggregateScript(session, nextAggregate);"));
|
assert!(runtime.contains("syncPageAggregateScript(session, nextAggregate);"));
|
||||||
assert!(runtime.contains(
|
let conversion_runtime = DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS;
|
||||||
|
assert!(conversion_runtime.contains(
|
||||||
"const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {"
|
"const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {"
|
||||||
));
|
));
|
||||||
assert!(runtime.contains("body?.blockDocument || body?.block_document"));
|
assert!(conversion_runtime.contains("body?.blockDocument || body?.block_document"));
|
||||||
assert!(runtime.contains("localizeTiptapAssetUrls(toTiptapDocument(body.content"));
|
assert!(
|
||||||
|
conversion_runtime.contains("localizeTiptapAssetUrls(toTiptapDocument(body.content")
|
||||||
|
);
|
||||||
assert!(runtime
|
assert!(runtime
|
||||||
.contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);"));
|
.contains("const nextTiptapDocument = pageBodyTiptapDocument(nextBody, '', session);"));
|
||||||
assert!(runtime.contains(
|
assert!(runtime.contains(
|
||||||
@@ -2022,7 +2041,10 @@ mod tests {
|
|||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("PANES_BOOTSTRAP_ID"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("PANES_BOOTSTRAP_ID"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("ROOT_SELECTOR"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("ROOT_SELECTOR"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("BRIDGE_PROTOCOL"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("BRIDGE_PROTOCOL"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("legacyInlineContentToTiptap"));
|
assert!(
|
||||||
|
DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-tiptap-conversion-runtime.js")
|
||||||
|
);
|
||||||
|
assert!(DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS.contains("legacyInlineContentToTiptap"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("openSecondaryDocument"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("openSecondaryDocument"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("openPrimaryMindmap"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("openPrimaryMindmap"));
|
||||||
}
|
}
|
||||||
@@ -2166,7 +2188,7 @@ mod tests {
|
|||||||
.expect("body");
|
.expect("body");
|
||||||
let html = String::from_utf8(body.to_vec()).expect("html");
|
let html = String::from_utf8(body.to_vec()).expect("html");
|
||||||
assert!(html.contains(r#"<script type="module" src="/api/mnote-browser-runtime/document-editor-adapter-runtime.js"></script>"#));
|
assert!(html.contains(r#"<script type="module" src="/api/mnote-browser-runtime/document-editor-adapter-runtime.js"></script>"#));
|
||||||
let runtime = DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS;
|
let runtime = DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS;
|
||||||
assert!(runtime.contains("legacyInlineContentToTiptap"));
|
assert!(runtime.contains("legacyInlineContentToTiptap"));
|
||||||
assert!(runtime.contains("legacyStylesToTiptapMarks"));
|
assert!(runtime.contains("legacyStylesToTiptapMarks"));
|
||||||
assert!(runtime.contains("legacyMarkArrayToTiptapMarks"));
|
assert!(runtime.contains("legacyMarkArrayToTiptapMarks"));
|
||||||
@@ -2183,7 +2205,8 @@ mod tests {
|
|||||||
assert!(runtime.contains("typeof payload.text === 'string'"));
|
assert!(runtime.contains("typeof payload.text === 'string'"));
|
||||||
assert!(runtime.contains("payload.type === 'hard_break'"));
|
assert!(runtime.contains("payload.type === 'hard_break'"));
|
||||||
assert!(runtime.contains("typeof body?.fileVersion === 'string'"));
|
assert!(runtime.contains("typeof body?.fileVersion === 'string'"));
|
||||||
assert!(runtime.contains("expectedFileVersion: session.conflictDetectionKey"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS
|
||||||
|
.contains("expectedFileVersion: session.conflictDetectionKey"));
|
||||||
assert!(runtime.contains("node?.attrs?.mnoteBlockType === 'mindmap'"));
|
assert!(runtime.contains("node?.attrs?.mnoteBlockType === 'mindmap'"));
|
||||||
assert!(runtime.contains("blockType: 'mindmap'"));
|
assert!(runtime.contains("blockType: 'mindmap'"));
|
||||||
assert!(runtime.contains("...mindmapPropsFromAttrs(node?.attrs, blockId)"));
|
assert!(runtime.contains("...mindmapPropsFromAttrs(node?.attrs, blockId)"));
|
||||||
|
|||||||
Reference in New Issue
Block a user