From 7b52dbe2b6d74b65397496fbded1c0371d0c17ff Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 20 Feb 2026 16:25:06 -0500 Subject: [PATCH 01/10] wip --- apps/obsidian/src/utils/importNodes.ts | 59 +++++++++++++++++--------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 2ebe0ff5e..de68f502e 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -487,11 +487,7 @@ const updateMarkdownAssetLinks = ({ app: App; originalNodePath?: string; }): string => { - if (oldPathToNewPath.size === 0) { - return content; - } - - // Create a set of all new paths for quick lookup (used by findImportedAssetFile) + // Create a set of all new paths for quick lookup (used by findImportedAssetFile when pathMapping has entries) const newPaths = new Set(oldPathToNewPath.values()); let updatedContent = content; @@ -500,6 +496,13 @@ const updateMarkdownAssetLinks = ({ ? targetFile.path.replace(/\/[^/]*$/, "") : ""; + // When the note is under import/{spaceName}/, only treat wiki links as resolved if the target is in this folder (not some other vault file). + const pathParts = targetFile.path.split("/"); + const importFolder = + pathParts[0] === "import" && pathParts.length >= 2 + ? pathParts.slice(0, 2).join("/") + : null; + /** Path of targetFile relative to the current note, for use in links. Obsidian resolves relative links from the note's directory. */ const getRelativeLinkPath = (assetPath: string): string => { const noteParts = noteDir ? noteDir.split("/").filter(Boolean) : []; @@ -644,6 +647,25 @@ const updateMarkdownAssetLinks = ({ } } + // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files; leave other links unchanged so they resolve from this folder when the target is created + const resolvedFile = app.metadataCache.getFirstLinkpathDest( + linkPath, + targetFile.path, + ); + const isInImportFolder = + importFolder && + resolvedFile && + (resolvedFile.path === importFolder || + resolvedFile.path.startsWith(importFolder + "/")); + if (isInImportFolder && resolvedFile) { + const linkText = getRelativeLinkPath(resolvedFile.path); + if (alias) { + return `[[${linkText}|${alias}]]`; + } + return `[[${linkText}]]`; + } + + // No resolved file in import folder: keep link as-is. Using ./ prefix breaks Obsidian (getParentPrefix null, "Folder already exists"); where new notes are created is controlled by app settings. return match; }, ); @@ -1250,21 +1272,18 @@ export const importSelectedNodes = async ({ originalNodePath, }); - // Update markdown content with new asset paths if assets were imported - if (assetImportResult.pathMapping.size > 0) { - const currentContent = await plugin.app.vault.read(processedFile); - const updatedContent = updateMarkdownAssetLinks({ - content: currentContent, - oldPathToNewPath: assetImportResult.pathMapping, - targetFile: processedFile, - app: plugin.app, - originalNodePath, - }); - - // Only update if content changed - if (updatedContent !== currentContent) { - await plugin.app.vault.modify(processedFile, updatedContent); - } + // Update markdown content: rewrite asset paths from pathMapping and normalize all wiki links to relative paths + const currentContent = await plugin.app.vault.read(processedFile); + const updatedContent = updateMarkdownAssetLinks({ + content: currentContent, + oldPathToNewPath: assetImportResult.pathMapping, + targetFile: processedFile, + app: plugin.app, + originalNodePath, + }); + + if (updatedContent !== currentContent) { + await plugin.app.vault.modify(processedFile, updatedContent); } // Log asset import errors if any From 4d30501550f2d176e8d62ee6d9fb2c3a6e73b4d8 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Fri, 20 Feb 2026 21:26:45 -0500 Subject: [PATCH 02/10] make sure the import has filePath now --- apps/obsidian/src/utils/importNodes.ts | 57 ++++++++++++++++++++++---- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index de68f502e..8682ba30d 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -647,7 +647,7 @@ const updateMarkdownAssetLinks = ({ } } - // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files; leave other links unchanged so they resolve from this folder when the target is created + // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files const resolvedFile = app.metadataCache.getFirstLinkpathDest( linkPath, targetFile.path, @@ -665,7 +665,21 @@ const updateMarkdownAssetLinks = ({ return `[[${linkText}]]`; } - // No resolved file in import folder: keep link as-is. Using ./ prefix breaks Obsidian (getParentPrefix null, "Folder already exists"); where new notes are created is controlled by app settings. + // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault + if (importFolder && originalNodePath) { + // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir + const canonicalSourcePath = + linkPath.includes("/") && !linkPath.startsWith(".") && !linkPath.startsWith("/") + ? normalizePathForLookup(linkPath) + : (getCanonicalFromOriginalNote(linkPath) ?? + normalizePathForLookup(linkPath)); + const linkUnderImport = `${importFolder}/${canonicalSourcePath}`; + if (alias) { + return `[[${linkUnderImport}|${alias}]]`; + } + return `[[${linkUnderImport}]]`; + } + return match; }, ); @@ -939,6 +953,15 @@ const sanitizeFileName = (fileName: string): string => { .trim(); }; +/** Sanitize each path segment for use under import folder (preserves source vault folder structure). */ +const sanitizePathForImport = (path: string): string => { + return path + .split("/") + .map((segment) => sanitizeFileName(segment)) + .filter(Boolean) + .join("/"); +}; + type ParsedFrontmatter = { nodeTypeId?: string; nodeInstanceId?: string; @@ -1209,11 +1232,13 @@ export const importSelectedNodes = async ({ content, createdAt: contentCreatedAt, modifiedAt: contentModifiedAt, - filePath, + filePath: contentFilePath, } = nodeContent; const createdAt = node.createdAt ?? contentCreatedAt; const modifiedAt = node.modifiedAt ?? contentModifiedAt; - const originalNodePath: string | undefined = node.filePath; + // Use source vault path from Content direct variant metadata for wikilink rewriting and asset placement + const originalNodePath: string | undefined = + contentFilePath ?? node.filePath; // Sanitize file name const sanitizedFileName = sanitizeFileName(fileName); @@ -1223,13 +1248,29 @@ export const importSelectedNodes = async ({ // Update existing file - use its current path finalFilePath = existingFile.path; } else { - // Create new file in the import folder - finalFilePath = `${importFolderPath}/${sanitizedFileName}.md`; + // Preserve source vault folder structure under import/{vaultName} when we have filePath from Content + const pathUnderImport = + contentFilePath && contentFilePath.includes("/") + ? sanitizePathForImport(contentFilePath) + : `${sanitizedFileName}.md`; + finalFilePath = `${importFolderPath}/${pathUnderImport}`; + + // Ensure parent folder exists (e.g. import/VaultName/Discourse Nodes) + const parentDir = finalFilePath.replace(/\/[^/]*$/, ""); + if (parentDir !== importFolderPath) { + const folderExists = + await plugin.app.vault.adapter.exists(parentDir); + if (!folderExists) { + await plugin.app.vault.createFolder(parentDir); + } + } // Check if file path already exists (edge case: same title but different nodeInstanceId) let counter = 1; while (await plugin.app.vault.adapter.exists(finalFilePath)) { - finalFilePath = `${importFolderPath}/${sanitizedFileName} (${counter}).md`; + const baseWithoutExt = finalFilePath.replace(/\.md$/i, ""); + const base = baseWithoutExt.replace(/\s*\(\d+\)$/, ""); + finalFilePath = `${base} (${counter}).md`; counter++; } } @@ -1242,7 +1283,7 @@ export const importSelectedNodes = async ({ sourceSpaceId: spaceId, sourceSpaceUri: spaceUri, rawContent: content, - originalFilePath: filePath, + originalFilePath: contentFilePath, filePath: finalFilePath, importedCreatedAt: createdAt, importedModifiedAt: modifiedAt, From af15ea273332b16286d71c8584fe45ebbb0ea634 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Feb 2026 10:21:57 -0500 Subject: [PATCH 03/10] address PR comments --- apps/obsidian/src/utils/importNodes.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 8682ba30d..1ce4cba3e 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -610,14 +610,15 @@ const updateMarkdownAssetLinks = ({ const wikiLinkRegex = /\[\[([^\]]+)\]\]/g; updatedContent = updatedContent.replace( wikiLinkRegex, - (match, linkContent) => { + (match, linkContent: string) => { // Extract path and optional alias const [linkPath, alias] = linkContent .split("|") .map((s: string) => s.trim()); + if (!linkPath) return match; // Skip external URLs - if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) { + if ((linkPath.startsWith("http://") || linkPath.startsWith("https://"))) { return match; } @@ -655,8 +656,7 @@ const updateMarkdownAssetLinks = ({ const isInImportFolder = importFolder && resolvedFile && - (resolvedFile.path === importFolder || - resolvedFile.path.startsWith(importFolder + "/")); + resolvedFile.path.startsWith(importFolder + "/"); if (isInImportFolder && resolvedFile) { const linkText = getRelativeLinkPath(resolvedFile.path); if (alias) { @@ -666,13 +666,15 @@ const updateMarkdownAssetLinks = ({ } // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault - if (importFolder && originalNodePath) { + if (importFolder && originalNodePath && !resolvedFile) { // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir const canonicalSourcePath = - linkPath.includes("/") && !linkPath.startsWith(".") && !linkPath.startsWith("/") + linkPath.includes("/") && + !linkPath.startsWith(".") && + !linkPath.startsWith("/") ? normalizePathForLookup(linkPath) : (getCanonicalFromOriginalNote(linkPath) ?? - normalizePathForLookup(linkPath)); + normalizePathForLookup(linkPath)); const linkUnderImport = `${importFolder}/${canonicalSourcePath}`; if (alias) { return `[[${linkUnderImport}|${alias}]]`; @@ -1273,6 +1275,12 @@ export const importSelectedNodes = async ({ finalFilePath = `${base} (${counter}).md`; counter++; } + + console.log( + "[DG import] original file path (source vault):", + originalNodePath ?? "(none)", + ); + console.log("[DG import] imported file path:", finalFilePath); } // Process the file content (maps nodeTypeId, handles frontmatter, stores import timestamps) From e558d43602920d0d959eb80da3c75026dd8146dc Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Feb 2026 10:37:24 -0500 Subject: [PATCH 04/10] make sure it rewrites filePath --- apps/obsidian/src/utils/importNodes.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 1ce4cba3e..b0fe0223b 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -1266,16 +1266,6 @@ export const importSelectedNodes = async ({ await plugin.app.vault.createFolder(parentDir); } } - - // Check if file path already exists (edge case: same title but different nodeInstanceId) - let counter = 1; - while (await plugin.app.vault.adapter.exists(finalFilePath)) { - const baseWithoutExt = finalFilePath.replace(/\.md$/i, ""); - const base = baseWithoutExt.replace(/\s*\(\d+\)$/, ""); - finalFilePath = `${base} (${counter}).md`; - counter++; - } - console.log( "[DG import] original file path (source vault):", originalNodePath ?? "(none)", From 91c97886c54019e5c3cffde664ff148d17c0ac66 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Feb 2026 10:38:22 -0500 Subject: [PATCH 05/10] cleanup log --- apps/obsidian/src/utils/importNodes.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index b0fe0223b..193348dad 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -932,7 +932,6 @@ const importAssetsForNode = async ({ // Track path mapping (raw + normalized key so updateMarkdownAssetLinks can lookup by link text) setPathMapping(filepath, targetPath); - console.log(`Imported asset: ${filepath} -> ${targetPath}`); } catch (error) { const errorMsg = `Error importing asset ${fileRef.filepath}: ${error}`; errors.push(errorMsg); @@ -1266,11 +1265,6 @@ export const importSelectedNodes = async ({ await plugin.app.vault.createFolder(parentDir); } } - console.log( - "[DG import] original file path (source vault):", - originalNodePath ?? "(none)", - ); - console.log("[DG import] imported file path:", finalFilePath); } // Process the file content (maps nodeTypeId, handles frontmatter, stores import timestamps) From d384f40792ed393b3967237166d9b4aeeed2eb29 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Mon, 23 Feb 2026 10:44:06 -0500 Subject: [PATCH 06/10] prettier --- apps/obsidian/src/utils/importNodes.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 193348dad..babf1dfac 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -296,9 +296,7 @@ export const fetchNodeContentWithMetadata = async ({ return { content: data.text, - createdAt: data.created - ? new Date(data.created + "Z").valueOf() - : 0, + createdAt: data.created ? new Date(data.created + "Z").valueOf() : 0, modifiedAt: data.last_modified ? new Date(data.last_modified + "Z").valueOf() : 0, @@ -618,7 +616,7 @@ const updateMarkdownAssetLinks = ({ if (!linkPath) return match; // Skip external URLs - if ((linkPath.startsWith("http://") || linkPath.startsWith("https://"))) { + if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) { return match; } From 1b8a82a6d499828fbaa3ccebe7c4e21f1f9478cd Mon Sep 17 00:00:00 2001 From: Trang Doan <44855874+trangdoan982@users.noreply.github.com> Date: Mon, 23 Feb 2026 08:06:09 -0800 Subject: [PATCH 07/10] Update apps/obsidian/src/utils/importNodes.ts Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- apps/obsidian/src/utils/importNodes.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index babf1dfac..658067a4c 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -1254,13 +1254,12 @@ export const importSelectedNodes = async ({ : `${sanitizedFileName}.md`; finalFilePath = `${importFolderPath}/${pathUnderImport}`; - // Ensure parent folder exists (e.g. import/VaultName/Discourse Nodes) - const parentDir = finalFilePath.replace(/\/[^/]*$/, ""); - if (parentDir !== importFolderPath) { - const folderExists = - await plugin.app.vault.adapter.exists(parentDir); - if (!folderExists) { - await plugin.app.vault.createFolder(parentDir); + // Ensure all parent folders exist (e.g. import/VaultName/Discourse Nodes/SubFolder) + const dirParts = finalFilePath.split("/"); + for (let i = 1; i < dirParts.length - 1; i++) { + const folderPath = dirParts.slice(0, i + 1).join("/"); + if (!(await plugin.app.vault.adapter.exists(folderPath))) { + await plugin.app.vault.createFolder(folderPath); } } } From b2277485c120485045c43566567ff8ca00e7f460 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Tue, 24 Feb 2026 15:53:43 -0500 Subject: [PATCH 08/10] address the [markdown link]() --- apps/obsidian/src/utils/importNodes.ts | 83 ++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 658067a4c..dc9716bd7 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -684,6 +684,75 @@ const updateMarkdownAssetLinks = ({ }, ); + // Match markdown links (non-image): [text](path) — internal paths resolved like wikilinks, href kept URL-encoded + const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g; + updatedContent = updatedContent.replace( + markdownLinkRegex, + (match, linkText: string, linkPath: string) => { + // First, try to find if this link resolves to one of our imported assets + const importedAssetFile = findImportedAssetFile(linkPath); + if (importedAssetFile) { + const linkPath = getRelativeLinkPath(importedAssetFile.path); + if (linkText) { + return `[${linkText}](${encodePathForMarkdownLink(linkPath)})`; + } + return `[${linkPath}](${encodePathForMarkdownLink(linkPath)})`; + } + + // Direct lookup from pathMapping (record built when we downloaded each asset) + const newPath = getNewPathForLink(linkPath); + if (newPath) { + const newFile = app.metadataCache.getFirstLinkpathDest( + newPath, + targetFile.path, + ); + if (newFile) { + const linkPath = getRelativeLinkPath(newFile.path); + if (linkText) { + return `[${linkText}](${encodePathForMarkdownLink(linkPath)})`; + } + return `[${linkPath}](${encodePathForMarkdownLink(linkPath)})`; + } + } + + // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files + const resolvedFile = app.metadataCache.getFirstLinkpathDest( + linkPath, + targetFile.path, + ); + const isInImportFolder = + importFolder && + resolvedFile && + resolvedFile.path.startsWith(importFolder + "/"); + if (isInImportFolder && resolvedFile) { + const linkText = getRelativeLinkPath(resolvedFile.path); + if (linkText) { + return `[${linkText}](${encodePathForMarkdownLink(linkText)})`; + } + return `[[${linkText}]]`; + } + + // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault + if (importFolder && originalNodePath && !resolvedFile) { + // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir + const canonicalSourcePath = + linkPath.includes("/") && + !linkPath.startsWith(".") && + !linkPath.startsWith("/") + ? normalizePathForLookup(linkPath) + : (getCanonicalFromOriginalNote(linkPath) ?? + normalizePathForLookup(linkPath)); + const linkUnderImport = `${importFolder}/${canonicalSourcePath}`; + if (linkText) { + return `[${linkText}](${encodePathForMarkdownLink(linkUnderImport)})`; + } + return `[${linkUnderImport}](${encodePathForMarkdownLink(linkUnderImport)})`; + } + + return match; + }, + ); + // Match markdown image links: ![alt](path) or ![alt](path "title") const markdownImageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g; updatedContent = updatedContent.replace( @@ -1475,3 +1544,17 @@ export const refreshAllImportedFiles = async ( return { success: successCount, failed: failedCount, errors }; }; + +const encodePathForMarkdownLink = (linkPath: string): string => { + // Decode the full path first so %2F becomes / and we split into real segments; then encode each segment (spaces → %20) but keep / as separator so we never emit %2F + let decoded: string; + try { + decoded = decodeURIComponent(linkPath); + } catch { + decoded = linkPath; + } + return decoded + .split("/") + .map((segment) => encodeURIComponent(segment)) + .join("/"); +}; From e20fa27909c445b57ee2399fabc1c3b0437c9e90 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Tue, 24 Feb 2026 20:35:46 -0500 Subject: [PATCH 09/10] Factor out common code. Also avoid adding spurious file extension. Also disguise relative links. --- apps/obsidian/src/utils/importNodes.ts | 189 ++++++++----------------- 1 file changed, 62 insertions(+), 127 deletions(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index dc9716bd7..bd7154bf0 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -604,6 +604,59 @@ const updateMarkdownAssetLinks = ({ return null; }; + const processLink = (linkPath: string): string => { + // Skip external URLs + if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) { + return linkPath; + } + + // First, try to find if this link resolves to one of our imported assets + const importedAssetFile = findImportedAssetFile(linkPath); + if (importedAssetFile) { + return getRelativeLinkPath(importedAssetFile.path); + } + + // Direct lookup from pathMapping (record built when we downloaded each asset) + const newPath = getNewPathForLink(linkPath); + if (newPath) { + const newFile = app.metadataCache.getFirstLinkpathDest( + newPath, + targetFile.path, + ); + if (newFile) { + return getRelativeLinkPath(newFile.path); + } + } + + // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files + const resolvedFile = app.metadataCache.getFirstLinkpathDest( + linkPath, + targetFile.path, + ); + const isInImportFolder = + importFolder && + resolvedFile && + resolvedFile.path.startsWith(importFolder + "/"); + if (isInImportFolder && resolvedFile) { + return getRelativeLinkPath(resolvedFile.path); + } + + // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault + if (importFolder && originalNodePath && !resolvedFile) { + // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir + const canonicalSourcePath = + linkPath.includes("/") && + !linkPath.startsWith(".") && + !linkPath.startsWith("/") + ? normalizePathForLookup(linkPath) + : (getCanonicalFromOriginalNote(linkPath) ?? + normalizePathForLookup(linkPath)); + return `${importFolder}/${canonicalSourcePath}`; + } + + return linkPath; + }; + // Match wiki links: [[path]] or [[path|alias]] const wikiLinkRegex = /\[\[([^\]]+)\]\]/g; updatedContent = updatedContent.replace( @@ -614,73 +667,13 @@ const updateMarkdownAssetLinks = ({ .split("|") .map((s: string) => s.trim()); if (!linkPath) return match; - - // Skip external URLs - if (linkPath.startsWith("http://") || linkPath.startsWith("https://")) { - return match; - } - - // First, try to find if this link resolves to one of our imported assets - const importedAssetFile = findImportedAssetFile(linkPath); - if (importedAssetFile) { - const linkText = getRelativeLinkPath(importedAssetFile.path); - if (alias) { - return `[[${linkText}|${alias}]]`; - } - return `[[${linkText}]]`; - } - - // Direct lookup from pathMapping (record built when we downloaded each asset) - const newPath = getNewPathForLink(linkPath); - if (newPath) { - const newFile = app.metadataCache.getFirstLinkpathDest( - newPath, - targetFile.path, - ); - if (newFile) { - const linkText = getRelativeLinkPath(newFile.path); - if (alias) { - return `[[${linkText}|${alias}]]`; - } - return `[[${linkText}]]`; - } - } - - // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files - const resolvedFile = app.metadataCache.getFirstLinkpathDest( - linkPath, - targetFile.path, - ); - const isInImportFolder = - importFolder && - resolvedFile && - resolvedFile.path.startsWith(importFolder + "/"); - if (isInImportFolder && resolvedFile) { - const linkText = getRelativeLinkPath(resolvedFile.path); - if (alias) { - return `[[${linkText}|${alias}]]`; - } - return `[[${linkText}]]`; - } - - // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault - if (importFolder && originalNodePath && !resolvedFile) { - // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir - const canonicalSourcePath = - linkPath.includes("/") && - !linkPath.startsWith(".") && - !linkPath.startsWith("/") - ? normalizePathForLookup(linkPath) - : (getCanonicalFromOriginalNote(linkPath) ?? - normalizePathForLookup(linkPath)); - const linkUnderImport = `${importFolder}/${canonicalSourcePath}`; - if (alias) { - return `[[${linkUnderImport}|${alias}]]`; - } - return `[[${linkUnderImport}]]`; + let processedPath = processLink(linkPath); + if (processedPath.endsWith(".md") && !linkPath.endsWith(".md")) + processedPath = processedPath.substring(0, processedPath.length - 3); + if (alias) { + return `[[${processedPath}|${alias}]]`; } - - return match; + return `[[${processedPath}|${linkPath}]]`; }, ); @@ -689,67 +682,9 @@ const updateMarkdownAssetLinks = ({ updatedContent = updatedContent.replace( markdownLinkRegex, (match, linkText: string, linkPath: string) => { - // First, try to find if this link resolves to one of our imported assets - const importedAssetFile = findImportedAssetFile(linkPath); - if (importedAssetFile) { - const linkPath = getRelativeLinkPath(importedAssetFile.path); - if (linkText) { - return `[${linkText}](${encodePathForMarkdownLink(linkPath)})`; - } - return `[${linkPath}](${encodePathForMarkdownLink(linkPath)})`; - } - - // Direct lookup from pathMapping (record built when we downloaded each asset) - const newPath = getNewPathForLink(linkPath); - if (newPath) { - const newFile = app.metadataCache.getFirstLinkpathDest( - newPath, - targetFile.path, - ); - if (newFile) { - const linkPath = getRelativeLinkPath(newFile.path); - if (linkText) { - return `[${linkText}](${encodePathForMarkdownLink(linkPath)})`; - } - return `[${linkPath}](${encodePathForMarkdownLink(linkPath)})`; - } - } - - // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files - const resolvedFile = app.metadataCache.getFirstLinkpathDest( - linkPath, - targetFile.path, - ); - const isInImportFolder = - importFolder && - resolvedFile && - resolvedFile.path.startsWith(importFolder + "/"); - if (isInImportFolder && resolvedFile) { - const linkText = getRelativeLinkPath(resolvedFile.path); - if (linkText) { - return `[${linkText}](${encodePathForMarkdownLink(linkText)})`; - } - return `[[${linkText}]]`; - } - - // Unresolved (dead) link from another vault: rewrite so that when the user creates the file from this link, it is created under import/{vaultName}/ in the same relative position as in the source vault - if (importFolder && originalNodePath && !resolvedFile) { - // Vault-relative link (e.g. "Discourse Nodes/EVD - no relation testing") -> use as-is. Path-from-current-file (e.g. "EVD - no relation testing") -> resolve relative to source note dir - const canonicalSourcePath = - linkPath.includes("/") && - !linkPath.startsWith(".") && - !linkPath.startsWith("/") - ? normalizePathForLookup(linkPath) - : (getCanonicalFromOriginalNote(linkPath) ?? - normalizePathForLookup(linkPath)); - const linkUnderImport = `${importFolder}/${canonicalSourcePath}`; - if (linkText) { - return `[${linkText}](${encodePathForMarkdownLink(linkUnderImport)})`; - } - return `[${linkUnderImport}](${encodePathForMarkdownLink(linkUnderImport)})`; - } - - return match; + if (!linkPath) return match; + const processedPath = encodePathForMarkdownLink(processLink(linkPath)); + return `[${linkText}](${processedPath})`; }, ); From 93fba8ce8bc047295232c676805596893ab62738 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 26 Feb 2026 23:17:41 -0500 Subject: [PATCH 10/10] Devin correction; and decode linkPath --- apps/obsidian/src/utils/importNodes.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index bd7154bf0..abeab33f5 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -678,11 +678,12 @@ const updateMarkdownAssetLinks = ({ ); // Match markdown links (non-image): [text](path) — internal paths resolved like wikilinks, href kept URL-encoded - const markdownLinkRegex = /\[([^\]]*)\]\(([^)]+)\)/g; + const markdownLinkRegex = /(? { if (!linkPath) return match; + linkPath = decodeURI(linkPath); const processedPath = encodePathForMarkdownLink(processLink(linkPath)); return `[${linkText}](${processedPath})`; },