fix local markdown attachment regressions

This commit is contained in:
lix-2026
2026-05-29 11:13:05 +08:00
parent 1109e3c0d8
commit cbe789e034
63 changed files with 3249 additions and 1502 deletions
@@ -363,27 +363,77 @@ export const mergeClassNames = (...values) => {
export const localAttachmentClassForTiptapHref = (href) => {
const localFilePath = localFileOpenPathFromTiptapHref(href);
return localFilePath ? attachmentClassForFileName(fileNameFromPath(localFilePath)) : '';
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') {
node.attrs = { ...node.attrs, src: localFileOpenUrlForTiptap(node.attrs.src, context) };
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 = localFileOpenUrlForTiptap(mark.attrs.href, context);
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 };
});
@@ -405,12 +455,13 @@ export const pageBodyTiptapDocumentSource = (body, fallbackText = '') => {
};
export const pageBodyTiptapDocument = (body, fallbackText = '', context = null) => {
const projectionContext = { ...(context || {}), body };
if (pageBodyTiptapDocumentSource(body, fallbackText) === 'local_markdown.content') {
return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), context);
return localizeTiptapAssetUrls(toTiptapDocument(body.content, fallbackText), projectionContext);
}
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);
if (blockDocument) return localizeTiptapAssetUrls(toTiptapDocument(blockDocument, fallbackText), projectionContext);
return localizeTiptapAssetUrls(toTiptapDocument(body?.content, fallbackText), projectionContext);
};
export const inlineTextNodes = (node) => {
@@ -524,7 +575,7 @@ export const tiptapNodeToEditorBlock = (node, index) => {
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 === '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;