Compare commits

..

8 Commits

Author SHA1 Message Date
Philipinho 2fe2c0e6c1 don't zip sinple page exports 2026-03-29 02:02:53 +01:00
Philipinho 388572f689 fix extra paragraph in task list 2026-03-29 00:34:21 +00:00
Philipinho a8335475fd Merge branch 'main' into editor-972 2026-03-29 00:11:06 +00:00
Philipinho 6a90b318e5 cleanup two preceeding spaces in ordered lists item 2026-03-29 00:05:40 +00:00
Philipinho 8b4cc82e5a fix clipboardTextSerializer for single lines 2026-03-28 23:59:46 +00:00
Philipinho cda7cc9a57 return clipboardTextSerializer as markdown 2026-03-28 22:59:18 +00:00
Philipinho 59c5f25502 fix marked 2026-03-28 21:52:28 +00:00
Philipinho b9d58081b8 autojoiner 2026-03-28 21:52:04 +00:00
14 changed files with 66 additions and 433 deletions
@@ -733,5 +733,7 @@
"Publish": "Publish.", "Publish": "Publish.",
"Security": "Security.", "Security": "Security.",
"Enforce SSO": "Enforce SSO.", "Enforce SSO": "Enforce SSO.",
"Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password." "Once enforced, members will not be able to login with email and password.": "Once enforced, members will not be able to log in with email and password.",
"Uploading {{name}}": "Uploading {{name}}",
"Uploading file": "Uploading file"
} }
@@ -75,7 +75,7 @@ function CommentMenu({
{isResolved ? t("Re-open comment") : t("Resolve comment")} {isResolved ? t("Re-open comment") : t("Resolve comment")}
</Menu.Item> </Menu.Item>
) : ( ) : (
<Tooltip label={upgradeLabel} position="left" withinPortal={false}> <Tooltip label={upgradeLabel} position="left" withPortal={false}>
<Menu.Item disabled leftSection={<IconCircleCheck size={14} />}> <Menu.Item disabled leftSection={<IconCircleCheck size={14} />}>
{t("Resolve comment")} {t("Resolve comment")}
</Menu.Item> </Menu.Item>
@@ -10,7 +10,7 @@ import { useCallback } from "react";
export default function AttachmentView(props: NodeViewProps) { export default function AttachmentView(props: NodeViewProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { editor, node, getPos, selected } = props; const { editor, node, getPos, selected } = props;
const { url, name, size, mime, attachmentId, placeholder } = node.attrs; const { url, name, size, mime, attachmentId } = node.attrs;
const { hovered, ref } = useHover(); const { hovered, ref } = useHover();
const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf"); const isPdf = mime === "application/pdf" || name?.toLowerCase().endsWith(".pdf");
@@ -49,14 +49,14 @@ export default function AttachmentView(props: NodeViewProps) {
h={25} h={25}
> >
<Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}> <Group wrap="nowrap" gap="sm" style={{ minWidth: 0, flex: 1 }}>
{!url && placeholder ? ( {url ? (
<Loader size={20} style={{ flexShrink: 0 }} />
) : (
<IconPaperclip size={20} style={{ flexShrink: 0 }} /> <IconPaperclip size={20} style={{ flexShrink: 0 }} />
) : (
<Loader size={20} style={{ flexShrink: 0 }} />
)} )}
<Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}> <Text component="span" size="md" truncate="end" style={{ minWidth: 0 }}>
{!url && placeholder ? t("Uploading {{name}}", { name }) : name} {url ? name : t("Uploading {{name}}", { name })}
</Text> </Text>
<Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}> <Text component="span" size="sm" c="dimmed" style={{ flexShrink: 0 }}>
@@ -29,7 +29,7 @@ export default function AudioView(props: NodeViewProps) {
return ( return (
<NodeViewWrapper data-drag-handle> <NodeViewWrapper data-drag-handle>
<div className={`${classes.audioWrapper} ${!safeSrc && placeholder ? classes.skeleton : ''}`}> <div className={`${classes.audioWrapper} ${!safeSrc ? classes.skeleton : ''}`}>
{safeSrc && ( {safeSrc && (
<audio <audio
className={classes.audio} className={classes.audio}
@@ -49,7 +49,7 @@ export default function AudioView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} /> <Loader size={20} pos="absolute" top={6} right={6} />
</Group> </Group>
)} )}
{!safeSrc && !previewSrc && placeholder && ( {!safeSrc && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md" h={54}>
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -59,9 +59,6 @@ export default function AudioView(props: NodeViewProps) {
</Text> </Text>
</Group> </Group>
)} )}
{!safeSrc && !previewSrc && !placeholder && (
<audio className={classes.audio} controls />
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -33,7 +33,7 @@ export default function ImageView(props: NodeViewProps) {
className={clsx( className={clsx(
selected && "ProseMirror-selectednode", selected && "ProseMirror-selectednode",
classes.imageWrapper, classes.imageWrapper,
!src && placeholder && classes.skeleton, !src && classes.skeleton,
alignClass, alignClass,
)} )}
style={{ style={{
@@ -55,7 +55,7 @@ export default function ImageView(props: NodeViewProps) {
<Loader size={20} pos="absolute" bottom={6} right={6} /> <Loader size={20} pos="absolute" bottom={6} right={6} />
</Group> </Group>
)} )}
{!src && !previewSrc && placeholder && ( {!src && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -73,17 +73,15 @@ export default function PdfView(props: NodeViewProps) {
if (!src || !safeSrc) { if (!src || !safeSrc) {
return ( return (
<NodeViewWrapper data-drag-handle> <NodeViewWrapper data-drag-handle>
<div className={`${classes.pdfWrapper} ${placeholder ? classes.skeleton : ''}`} style={{ height: placeholder ? 600 : undefined }}> <div className={`${classes.pdfWrapper} ${classes.skeleton}`} style={{ height: 600 }}>
{placeholder && ( <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Loader size={20} style={{ flexShrink: 0 }} />
<Loader size={20} style={{ flexShrink: 0 }} /> <Text component="span" size="sm" truncate="end">
<Text component="span" size="sm" truncate="end"> {placeholder?.name
{placeholder?.name ? t("Uploading {{name}}", { name: placeholder.name })
? t("Uploading {{name}}", { name: placeholder.name }) : t("Uploading file")}
: t("Uploading file")} </Text>
</Text> </Group>
</Group>
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -33,7 +33,7 @@ export default function VideoView(props: NodeViewProps) {
className={clsx( className={clsx(
selected && "ProseMirror-selectednode", selected && "ProseMirror-selectednode",
classes.videoWrapper, classes.videoWrapper,
!src && placeholder && classes.skeleton, !src && classes.skeleton,
alignClass, alignClass,
)} )}
style={{ style={{
@@ -60,7 +60,7 @@ export default function VideoView(props: NodeViewProps) {
<Loader size={20} pos="absolute" top={6} right={6} /> <Loader size={20} pos="absolute" top={6} right={6} />
</Group> </Group>
)} )}
{!src && !previewSrc && placeholder && ( {!src && !previewSrc && (
<Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md"> <Group justify="center" wrap="nowrap" gap="xs" maw="100%" px="md">
<Loader size={20} style={{ flexShrink: 0 }} /> <Loader size={20} style={{ flexShrink: 0 }} />
<Text component="span" size="sm" truncate="end"> <Text component="span" size="sm" truncate="end">
@@ -70,9 +70,6 @@ export default function VideoView(props: NodeViewProps) {
</Text> </Text>
</Group> </Group>
)} )}
{!src && !previewSrc && !placeholder && (
<video className={classes.video} controls />
)}
</div> </div>
</NodeViewWrapper> </NodeViewWrapper>
); );
@@ -253,8 +253,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 24, minWidth: 80,
minHeight: 16, minHeight: 40,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createImageHandle, createCustomHandle: createImageHandle,
@@ -266,8 +266,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 24, minWidth: 80,
minHeight: 16, minHeight: 40,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -297,8 +297,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 24, minWidth: 80,
minHeight: 16, minHeight: 40,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -310,8 +310,8 @@ export const mainExtensions = [
resize: { resize: {
enabled: true, enabled: true,
directions: ["left", "right"], directions: ["left", "right"],
minWidth: 24, minWidth: 80,
minHeight: 16, minHeight: 40,
alwaysPreserveAspectRatio: true, alwaysPreserveAspectRatio: true,
//@ts-ignore //@ts-ignore
createCustomHandle: createResizeHandle, createCustomHandle: createResizeHandle,
@@ -1,6 +1,6 @@
// adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT // adapted from: https://github.com/aguingand/tiptap-markdown/blob/main/src/extensions/tiptap/clipboard.js - MIT
import { Extension } from "@tiptap/core"; import { Extension } from "@tiptap/core";
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state"; import { Plugin, PluginKey } from "@tiptap/pm/state";
import { DOMParser, DOMSerializer, Fragment, Slice } from "@tiptap/pm/model"; import { DOMParser, DOMSerializer, Fragment, Slice } from "@tiptap/pm/model";
import { find } from "linkifyjs"; import { find } from "linkifyjs";
import { markdownToHtml, htmlToMarkdown } from "@docmost/editor-ext"; import { markdownToHtml, htmlToMarkdown } from "@docmost/editor-ext";
@@ -50,46 +50,26 @@ export const MarkdownClipboard = Extension.create({
} }
const text = event.clipboardData.getData("text/plain"); const text = event.clipboardData.getData("text/plain");
const html = event.clipboardData.getData("text/html");
const vscode = event.clipboardData.getData("vscode-editor-data"); const vscode = event.clipboardData.getData("vscode-editor-data");
const vscodeData = vscode ? JSON.parse(vscode) : undefined; const vscodeData = vscode ? JSON.parse(vscode) : undefined;
const language = vscodeData?.mode; const language = vscodeData?.mode;
const isVscodeMarkdown = language === "markdown"; if (language !== "markdown") {
const isPlainTextOnly = !html && !vscode && !!text;
if (!isVscodeMarkdown && !isPlainTextOnly) {
return false; return false;
} }
if (isPlainTextOnly) {
if ((view as any).input?.shiftKey || !this.options.transformPastedText) {
return false;
}
const link = find(text, {
defaultProtocol: "http",
}).find((item) => item.isLink && item.value === text);
if (link) {
return false;
}
}
const { tr } = view.state; const { tr } = view.state;
const { from, to } = view.state.selection; const { from, to } = view.state.selection;
const parsed = markdownToHtml(text.replace(/\n+$/, "")); const html = markdownToHtml(text.replace(/\n+$/, ""));
const contentNodes = DOMParser.fromSchema( const contentNodes = DOMParser.fromSchema(
this.editor.schema, this.editor.schema,
).parseSlice(elementFromString(parsed), { ).parseSlice(elementFromString(html), {
preserveWhitespace: true, preserveWhitespace: true,
}); });
tr.replaceRange(from, to, contentNodes); tr.replaceRange(from, to, contentNodes);
const insertEnd = tr.mapping.map(from, 1);
tr.setSelection(TextSelection.near(tr.doc.resolve(Math.max(from, insertEnd - 2)), -1));
tr.setMeta('paste', true) tr.setMeta('paste', true)
view.dispatch(tr); view.dispatch(tr);
return true; return true;
@@ -125,6 +105,26 @@ export const MarkdownClipboard = Extension.create({
return slice; return slice;
}, },
clipboardTextParser: (text, context, plainText) => {
const link = find(text, {
defaultProtocol: "http",
}).find((item) => item.isLink && item.value === text);
if (plainText || !this.options.transformPastedText || link) {
// don't parse plaintext link to allow link paste handler to work
// pasting with shift key prevents formatting
return null;
}
const parsed = markdownToHtml(text.replace(/\n+$/, ""));
return DOMParser.fromSchema(this.editor.schema).parseSlice(
elementFromString(parsed),
{
preserveWhitespace: true,
context,
},
);
},
}, },
}), }),
]; ];
@@ -1,333 +0,0 @@
import { type Kysely, sql } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createIndex('idx_group_users_user_id')
.ifNotExists()
.on('group_users')
.column('user_id')
.execute();
await db.schema
.createIndex('idx_space_members_user_id')
.ifNotExists()
.on('space_members')
.column('user_id')
.execute();
await db.schema
.createIndex('idx_space_members_group_id')
.ifNotExists()
.on('space_members')
.column('group_id')
.execute();
// Page tree
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_parent_position
ON pages (space_id, parent_page_id, position COLLATE "C")
WHERE deleted_at IS NULL
`.execute(db);
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_parent_page_id
ON pages (parent_page_id)
WHERE deleted_at IS NULL
`.execute(db);
// Recent pages query
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_updated
ON pages (space_id, updated_at DESC)
WHERE deleted_at IS NULL
`.execute(db);
// Trash view
await sql`
CREATE INDEX IF NOT EXISTS idx_pages_space_deleted
ON pages (space_id, deleted_at DESC)
WHERE deleted_at IS NOT NULL
`.execute(db);
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_workspaces_hostname_lower
ON workspaces (LOWER(hostname))
`.execute(db);
await db.schema
.createIndex('idx_workspaces_created_at')
.ifNotExists()
.on('workspaces')
.column('created_at')
.execute();
await db.schema
.createIndex('idx_users_workspace_deleted')
.ifNotExists()
.on('users')
.columns(['workspace_id', 'deleted_at'])
.execute();
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_spaces_slug_lower_workspace
ON spaces (LOWER(slug), workspace_id)
`.execute(db);
await db.schema
.createIndex('idx_spaces_workspace_id')
.ifNotExists()
.on('spaces')
.column('workspace_id')
.execute();
await sql`
CREATE UNIQUE INDEX IF NOT EXISTS idx_groups_name_lower_workspace
ON groups (LOWER(name), workspace_id)
`.execute(db);
await db.schema
.createIndex('idx_groups_workspace_id')
.ifNotExists()
.on('groups')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_shares_page_id')
.ifNotExists()
.on('shares')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_attachments_page_id')
.ifNotExists()
.on('attachments')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_attachments_space_id')
.ifNotExists()
.on('attachments')
.column('space_id')
.execute();
await db.schema
.createIndex('idx_comments_page_id')
.ifNotExists()
.on('comments')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_comments_parent_comment_id')
.ifNotExists()
.on('comments')
.column('parent_comment_id')
.execute();
await sql`
CREATE INDEX IF NOT EXISTS idx_page_history_page_created
ON page_history (page_id, created_at DESC)
`.execute(db);
await db.schema
.createIndex('idx_attachments_workspace_id')
.ifNotExists()
.on('attachments')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_backlinks_target_page_id')
.ifNotExists()
.on('backlinks')
.column('target_page_id')
.execute();
await db.schema
.createIndex('idx_pages_workspace_id')
.ifNotExists()
.on('pages')
.column('workspace_id')
.execute();
await db.schema
.createIndex('idx_pages_creator_id')
.ifNotExists()
.on('pages')
.column('creator_id')
.execute();
// Notifications: FK cascade from pages, spaces, comments
await db.schema
.createIndex('idx_notifications_page_id')
.ifNotExists()
.on('notifications')
.column('page_id')
.execute();
await db.schema
.createIndex('idx_notifications_space_id')
.ifNotExists()
.on('notifications')
.column('space_id')
.execute();
await db.schema
.createIndex('idx_notifications_comment_id')
.ifNotExists()
.on('notifications')
.column('comment_id')
.execute();
// Watchers: cleanup queries and FK cascade
await db.schema
.createIndex('idx_watchers_user_workspace')
.ifNotExists()
.on('watchers')
.columns(['user_id', 'workspace_id'])
.execute();
await db.schema
.createIndex('idx_watchers_space_id')
.ifNotExists()
.on('watchers')
.column('space_id')
.execute();
// Auth providers: all queries filter by workspaceId
await db.schema
.createIndex('idx_auth_providers_workspace_id')
.ifNotExists()
.on('auth_providers')
.column('workspace_id')
.execute();
// Auth accounts: SSO login lookup by provider user
await db.schema
.createIndex('idx_auth_accounts_provider_user_id')
.ifNotExists()
.on('auth_accounts')
.columns(['provider_user_id', 'auth_provider_id'])
.execute();
// Workspace invitations: listing and SSO lookup
await db.schema
.createIndex('idx_workspace_invitations_workspace_id')
.ifNotExists()
.on('workspace_invitations')
.column('workspace_id')
.execute();
// API keys: query and FK cascade
await db.schema
.createIndex('idx_api_keys_workspace_id')
.ifNotExists()
.on('api_keys')
.column('workspace_id')
.execute();
// User sessions: delete queries and FK cascade on all session states
await db.schema
.createIndex('idx_user_sessions_user_workspace')
.ifNotExists()
.on('user_sessions')
.columns(['user_id', 'workspace_id'])
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropIndex('idx_group_users_user_id').ifExists().execute();
await db.schema.dropIndex('idx_space_members_user_id').ifExists().execute();
await db.schema.dropIndex('idx_space_members_group_id').ifExists().execute();
await db.schema
.dropIndex('idx_pages_space_parent_position')
.ifExists()
.execute();
await db.schema.dropIndex('idx_pages_parent_page_id').ifExists().execute();
await db.schema.dropIndex('idx_pages_space_updated').ifExists().execute();
await db.schema.dropIndex('idx_pages_space_deleted').ifExists().execute();
await db.schema
.dropIndex('idx_workspaces_hostname_lower')
.ifExists()
.execute();
await db.schema.dropIndex('idx_workspaces_created_at').ifExists().execute();
await db.schema
.dropIndex('idx_users_workspace_deleted')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_spaces_slug_lower_workspace')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_spaces_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_groups_name_lower_workspace')
.ifExists()
.execute();
await db.schema.dropIndex('idx_groups_workspace_id').ifExists().execute();
await db.schema.dropIndex('idx_shares_page_id').ifExists().execute();
await db.schema.dropIndex('idx_attachments_page_id').ifExists().execute();
await db.schema.dropIndex('idx_attachments_space_id').ifExists().execute();
await db.schema.dropIndex('idx_comments_page_id').ifExists().execute();
await db.schema
.dropIndex('idx_comments_parent_comment_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_page_history_page_created')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_attachments_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_backlinks_target_page_id')
.ifExists()
.execute();
await db.schema.dropIndex('idx_pages_workspace_id').ifExists().execute();
await db.schema.dropIndex('idx_pages_creator_id').ifExists().execute();
await db.schema
.dropIndex('idx_notifications_page_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_notifications_space_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_notifications_comment_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_watchers_user_workspace')
.ifExists()
.execute();
await db.schema.dropIndex('idx_watchers_space_id').ifExists().execute();
await db.schema
.dropIndex('idx_auth_providers_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_auth_accounts_provider_user_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_workspace_invitations_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_api_keys_workspace_id')
.ifExists()
.execute();
await db.schema
.dropIndex('idx_user_sessions_user_workspace')
.ifExists()
.execute();
}
@@ -193,8 +193,6 @@ export class ImportAttachmentService {
// Build a map from resolved archive path → real filename from Confluence // Build a map from resolved archive path → real filename from Confluence
// metadata. Confluence Server archives often store files under numeric IDs // metadata. Confluence Server archives often store files under numeric IDs
// (e.g. "attachments/65601/65602") instead of the original filename. // (e.g. "attachments/65601/65602") instead of the original filename.
// Also register aliases so HTML references using the original filename
// (e.g. "attachments/pageId/original.mp3") resolve to the numeric path.
const pageDir = path.dirname(pageRelativePath); const pageDir = path.dirname(pageRelativePath);
const attachmentNameByRelPath = new Map<string, string>(); const attachmentNameByRelPath = new Map<string, string>();
for (const attachment of pageAttachments) { for (const attachment of pageAttachments) {
@@ -205,13 +203,6 @@ export class ImportAttachmentService {
); );
if (relPath && attachment.fileName) { if (relPath && attachment.fileName) {
attachmentNameByRelPath.set(relPath, attachment.fileName); attachmentNameByRelPath.set(relPath, attachment.fileName);
const dir = path.posix.dirname(relPath);
const aliasKey = `${dir}/${attachment.fileName}`;
if (!attachmentCandidates.has(aliasKey)) {
attachmentCandidates.set(aliasKey, attachmentCandidates.get(relPath)!);
attachmentNameByRelPath.set(aliasKey, attachment.fileName);
}
} }
} }
@@ -571,31 +562,18 @@ export class ImportAttachmentService {
continue; continue;
} }
// Resolve the metadata href to the actual archive path // Check if already processed (was referenced in HTML)
const resolvedHref = resolveRelativeAttachmentPath( if (processed.has(href)) {
href, continue;
pageDir, }
attachmentCandidates,
);
if (!resolvedHref) continue;
// Check if already processed (was referenced in HTML). // Skip if the file doesn't exist
// Inline elements may have been processed under an alias key (original if (!attachmentCandidates.has(href)) {
// filename) rather than the numeric archive path, so also check whether
// the underlying absolute file path has already been uploaded.
const absPath = attachmentCandidates.get(resolvedHref);
const alreadyProcessed =
processed.has(resolvedHref) ||
(absPath &&
Array.from(processed.values()).some(
(entry) => entry.abs === absPath,
));
if (alreadyProcessed) {
continue; continue;
} }
// This attachment was in the list but not referenced in HTML - add it // This attachment was in the list but not referenced in HTML - add it
const { attachmentId, apiFilePath, abs } = processFile(resolvedHref); const { attachmentId, apiFilePath, abs } = processFile(href);
const mime = mimeType || getMimeType(abs); const mime = mimeType || getMimeType(abs);
// Add as attachment node at the end // Add as attachment node at the end
+1 -3
View File
@@ -9,7 +9,5 @@
"main": "dist/index.js", "main": "dist/index.js",
"module": "./src/index.ts", "module": "./src/index.ts",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
"dependencies": { "dependencies": {}
"marked": "17.0.5"
}
} }
+1 -5
View File
@@ -801,11 +801,7 @@ importers:
specifier: ^8.57.1 specifier: ^8.57.1
version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3) version: 8.57.1(eslint@9.39.4(jiti@2.4.2))(typescript@5.9.3)
packages/editor-ext: packages/editor-ext: {}
dependencies:
marked:
specifier: 17.0.5
version: 17.0.5
packages: packages: