Checkpoint current workspace

This commit is contained in:
lix-2026
2026-05-02 06:25:26 +08:00
parent 015ad5bedd
commit 98b6360595
88 changed files with 40217 additions and 178 deletions
+125 -11
View File
@@ -311,26 +311,93 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const type = typeof block?.type === 'string' ? block.type : 'paragraph';
const text = flattenText(block?.content);
const content = text ? [{ type: 'text', text }] : [];
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(legacyBlockToTiptap).filter(Boolean)
: [];
const withListChildren = (itemType, listType, attrs = {}) => ({
type: listType,
content: [{
type: itemType,
attrs,
content: [
{ type: 'paragraph', content },
...nestedChildren,
],
}],
});
if (type === 'heading') {
const level = Number(block?.props?.level || block?.level || 1) || 1;
return { type: 'heading', attrs: { level: Math.max(1, Math.min(6, level)) }, content };
const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
return { type: 'heading', attrs: withTextAlign({ level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
}
if (type === 'bulletListItem' || type === 'bullet_list_item') {
return { type: 'bulletList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] };
return withListChildren('listItem', 'bulletList');
}
if (type === 'numberedListItem' || type === 'numbered_list_item') {
return { type: 'orderedList', content: [{ type: 'listItem', content: [{ type: 'paragraph', content }] }] };
return withListChildren('listItem', 'orderedList');
}
if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
return { type: 'taskList', content: [{ type: 'taskItem', attrs: { checked: Boolean(block?.props?.checked) }, content: [{ type: 'paragraph', content }] }] };
return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
}
if (type === 'quote' || type === 'blockquote') {
return { type: 'blockquote', content: [{ type: 'paragraph', content }] };
return { type: 'blockquote', attrs: withTextAlign(), content: [{ type: 'paragraph', attrs: withTextAlign(), content }] };
}
if (type === 'codeBlock') {
return { type: 'codeBlock', attrs: { language: block?.props?.language || null }, content };
if (type === 'codeBlock' || type === 'code_block') {
return { type: 'codeBlock', attrs: withTextAlign({ language: block?.props?.language || null }), content };
}
return { type: 'paragraph', content };
if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') {
return { type: 'horizontalRule' };
}
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 } : null;
}
return { type: 'paragraph', attrs: withTextAlign(), content };
};
const textToTiptapDocument = (text) => ({
@@ -371,9 +438,20 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const pageBody = aggregate.body || {};
const permissions = aggregate.head?.permissions || {};
const conflictDetectionKey = typeof pageBody.conflictDetectionKey === 'string'
? pageBody.conflictDetectionKey
: typeof pageBody.conflict_detection_key === 'string'
? pageBody.conflict_detection_key
: null;
const revisionFromConflictKey = (value) => {
const match = String(value || '').match(/:(\d+)$/);
return match ? Number(match[1]) : null;
};
const pageBodyRevision = Number.isInteger(pageBody.revision) ? pageBody.revision : null;
const keyRevision = revisionFromConflictKey(conflictDetectionKey);
const editorMeta = {
revision: Number.isInteger(pageBody.revision) ? pageBody.revision : null,
conflictDetectionKey: typeof pageBody.conflictDetectionKey === 'string' ? pageBody.conflictDetectionKey : null,
revision: pageBodyRevision && pageBodyRevision > 0 ? pageBodyRevision : keyRevision,
conflictDetectionKey,
};
const mountOptions = {
documentId: bootstrap.documentId,
@@ -389,8 +467,26 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
let saveTimer = 0;
let lastSavedSerialized = '';
const normalizeBridgeValue = (value) => {
if (value instanceof Map) {
const out = {};
for (const [key, item] of value.entries()) {
out[key] = normalizeBridgeValue(item);
}
return out;
}
if (Array.isArray(value)) {
return value.map(normalizeBridgeValue);
}
if (value && typeof value === 'object') {
return Object.fromEntries(
Object.entries(value).map(([key, item]) => [key, normalizeBridgeValue(item)]),
);
}
return value;
};
const normalizeEnvelopePayload = (event) => {
const detail = event?.detail;
const detail = normalizeBridgeValue(event?.detail);
if (!detail || typeof detail !== 'object') return null;
const payload = detail.payload && typeof detail.payload === 'object' ? detail.payload : detail;
return payload && typeof payload === 'object' ? payload : null;
@@ -512,6 +608,24 @@ fn runtime_asset_content_type(asset_path: &str) -> &'static str {
}
}
pub async fn editor_image_placeholder_asset() -> Response {
const SVG: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" width="640" height="360" viewBox="0 0 640 360" role="img" aria-label="E24 image placeholder">
<rect width="640" height="360" rx="18" fill="#f3f4f6"/>
<rect x="42" y="42" width="556" height="276" rx="14" fill="#ffffff" stroke="#d1d5db" stroke-width="2"/>
<circle cx="168" cy="132" r="34" fill="#93c5fd"/>
<path d="M98 278 246 174 340 242 410 192 542 278Z" fill="#86efac"/>
<path d="M98 278 246 174 340 242 410 192 542 278" fill="none" stroke="#16a34a" stroke-width="8" stroke-linejoin="round"/>
<text x="320" y="322" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" fill="#374151">E24 Image</text>
</svg>"##;
Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "image/svg+xml; charset=utf-8")
.header(header::CACHE_CONTROL, "no-store")
.header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
.body(Body::from(SVG))
.unwrap_or_else(|_| Response::new(Body::empty()))
}
pub async fn leptos_tiptap_manifest() -> Response {
let manifest = json!({
"entryAssetPath": "mnote-leptos-tiptap-spike-island.js",