@@ -483,11 +483,7 @@ const updateMarkdownAssetLinks = ({
483483 app : App ;
484484 originalNodePath ?: string ;
485485} ) : string => {
486- if ( oldPathToNewPath . size === 0 ) {
487- return content ;
488- }
489-
490- // Create a set of all new paths for quick lookup (used by findImportedAssetFile)
486+ // Create a set of all new paths for quick lookup (used by findImportedAssetFile when pathMapping has entries)
491487 const newPaths = new Set ( oldPathToNewPath . values ( ) ) ;
492488
493489 let updatedContent = content ;
@@ -496,6 +492,13 @@ const updateMarkdownAssetLinks = ({
496492 ? targetFile . path . replace ( / \/ [ ^ / ] * $ / , "" )
497493 : "" ;
498494
495+ // 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).
496+ const pathParts = targetFile . path . split ( "/" ) ;
497+ const importFolder =
498+ pathParts [ 0 ] === "import" && pathParts . length >= 2
499+ ? pathParts . slice ( 0 , 2 ) . join ( "/" )
500+ : null ;
501+
499502 /** Path of targetFile relative to the current note, for use in links. Obsidian resolves relative links from the note's directory. */
500503 const getRelativeLinkPath = ( assetPath : string ) : string => {
501504 const noteParts = noteDir ? noteDir . split ( "/" ) . filter ( Boolean ) : [ ] ;
@@ -599,48 +602,100 @@ const updateMarkdownAssetLinks = ({
599602 return null ;
600603 } ;
601604
605+ const processLink = ( linkPath : string ) : string => {
606+ // Skip external URLs
607+ if ( linkPath . startsWith ( "http://" ) || linkPath . startsWith ( "https://" ) ) {
608+ return linkPath ;
609+ }
610+
611+ // First, try to find if this link resolves to one of our imported assets
612+ const importedAssetFile = findImportedAssetFile ( linkPath ) ;
613+ if ( importedAssetFile ) {
614+ return getRelativeLinkPath ( importedAssetFile . path ) ;
615+ }
616+
617+ // Direct lookup from pathMapping (record built when we downloaded each asset)
618+ const newPath = getNewPathForLink ( linkPath ) ;
619+ if ( newPath ) {
620+ const newFile = app . metadataCache . getFirstLinkpathDest (
621+ newPath ,
622+ targetFile . path ,
623+ ) ;
624+ if ( newFile ) {
625+ return getRelativeLinkPath ( newFile . path ) ;
626+ }
627+ }
628+
629+ // Only resolve to files under import/{spaceName}/ so we don't point at the wrong vault's files
630+ const resolvedFile = app . metadataCache . getFirstLinkpathDest (
631+ linkPath ,
632+ targetFile . path ,
633+ ) ;
634+ const isInImportFolder =
635+ importFolder &&
636+ resolvedFile &&
637+ resolvedFile . path . startsWith ( importFolder + "/" ) ;
638+ if ( isInImportFolder && resolvedFile ) {
639+ return getRelativeLinkPath ( resolvedFile . path ) ;
640+ }
641+
642+ // 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
643+ if ( importFolder && originalNodePath && ! resolvedFile ) {
644+ // 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
645+ const canonicalSourcePath =
646+ linkPath . includes ( "/" ) &&
647+ ! linkPath . startsWith ( "." ) &&
648+ ! linkPath . startsWith ( "/" )
649+ ? normalizePathForLookup ( linkPath )
650+ : ( getCanonicalFromOriginalNote ( linkPath ) ??
651+ normalizePathForLookup ( linkPath ) ) ;
652+ return `${ importFolder } /${ canonicalSourcePath } ` ;
653+ }
654+
655+ return linkPath ;
656+ } ;
657+
602658 // Match wiki links: [[path]] or [[path|alias]]
603659 const wikiLinkRegex = / \[ \[ ( [ ^ \] ] + ) \] \] / g;
604660 updatedContent = updatedContent . replace (
605661 wikiLinkRegex ,
606- ( match , linkContent ) => {
662+ ( match , linkContent : string ) => {
607663 // Extract path and optional alias
608664 const [ linkPath , alias ] = linkContent
609665 . split ( "|" )
610666 . map ( ( s : string ) => s . trim ( ) ) ;
611-
612- // Skip external URLs
613- if ( linkPath . startsWith ( "http://" ) || linkPath . startsWith ( "https://" ) ) {
614- return match ;
615- }
616-
617- // First, try to find if this link resolves to one of our imported assets
618- const importedAssetFile = findImportedAssetFile ( linkPath ) ;
619- if ( importedAssetFile ) {
620- const linkText = getRelativeLinkPath ( importedAssetFile . path ) ;
621- if ( alias ) {
622- return `[[${ linkText } |${ alias } ]]` ;
623- }
624- return `[[${ linkText } ]]` ;
667+ if ( ! linkPath ) return match ;
668+ let processedPath = processLink ( linkPath ) ;
669+ if ( processedPath . endsWith ( ".md" ) && ! linkPath . endsWith ( ".md" ) )
670+ processedPath = processedPath . substring ( 0 , processedPath . length - 3 ) ;
671+ if ( alias ) {
672+ return `[[${ processedPath } |${ alias } ]]` ;
625673 }
674+ return `[[${ processedPath } |${ linkPath } ]]` ;
675+ } ,
676+ ) ;
626677
627- // Direct lookup from pathMapping (record built when we downloaded each asset)
628- const newPath = getNewPathForLink ( linkPath ) ;
629- if ( newPath ) {
630- const newFile = app . metadataCache . getFirstLinkpathDest (
631- newPath ,
632- targetFile . path ,
633- ) ;
634- if ( newFile ) {
635- const linkText = getRelativeLinkPath ( newFile . path ) ;
636- if ( alias ) {
637- return `[[${ linkText } |${ alias } ]]` ;
678+ // Match markdown links (non-image): [text](path) — internal paths resolved like wikilinks, href kept URL-encoded
679+ const markdownLinkRegex = / (?< ! ! ) \[ ( [ ^ \] ] * ) \] \( ( [ ^ ) ] + ) \) / g;
680+ updatedContent = updatedContent . replace (
681+ markdownLinkRegex ,
682+ ( match , linkText : string , linkPath : string ) => {
683+ if ( ! linkPath ) return match ;
684+ linkPath = linkPath
685+ . split ( "/" )
686+ . map ( ( segment ) => {
687+ try {
688+ return decodeURIComponent ( segment ) ;
689+ } catch {
690+ return segment ;
638691 }
639- return `[[${ linkText } ]]` ;
640- }
692+ } )
693+ . join ( "/" ) ;
694+ if ( linkPath . startsWith ( "http://" ) || linkPath . startsWith ( "https://" ) ) {
695+ return match ;
641696 }
642-
643- return match ;
697+ const processedPath = encodePathForMarkdownLink ( processLink ( linkPath ) ) ;
698+ return `[ ${ linkText } ]( ${ processedPath } )` ;
644699 } ,
645700 ) ;
646701
@@ -911,6 +966,15 @@ const sanitizeFileName = (fileName: string): string => {
911966 . trim ( ) ;
912967} ;
913968
969+ /** Sanitize each path segment for use under import folder (preserves source vault folder structure). */
970+ const sanitizePathForImport = ( path : string ) : string => {
971+ return path
972+ . split ( "/" )
973+ . map ( ( segment ) => sanitizeFileName ( segment ) )
974+ . filter ( Boolean )
975+ . join ( "/" ) ;
976+ } ;
977+
914978type ParsedFrontmatter = {
915979 nodeTypeId ?: string ;
916980 nodeInstanceId ?: string ;
@@ -1210,11 +1274,13 @@ export const importSelectedNodes = async ({
12101274 content,
12111275 createdAt : contentCreatedAt ,
12121276 modifiedAt : contentModifiedAt ,
1213- filePath,
1277+ filePath : contentFilePath ,
12141278 } = nodeContent ;
12151279 const createdAt = node . createdAt ?? contentCreatedAt ;
12161280 const modifiedAt = node . modifiedAt ?? contentModifiedAt ;
1217- const originalNodePath : string | undefined = node . filePath ;
1281+ // Use source vault path from Content direct variant metadata for wikilink rewriting and asset placement
1282+ const originalNodePath : string | undefined =
1283+ contentFilePath ?? node . filePath ;
12181284
12191285 // Sanitize file name
12201286 const sanitizedFileName = sanitizeFileName ( fileName ) ;
@@ -1224,14 +1290,20 @@ export const importSelectedNodes = async ({
12241290 // Update existing file - use its current path
12251291 finalFilePath = existingFile . path ;
12261292 } else {
1227- // Create new file in the import folder
1228- finalFilePath = `${ importFolderPath } /${ sanitizedFileName } .md` ;
1229-
1230- // Check if file path already exists (edge case: same title but different nodeInstanceId)
1231- let counter = 1 ;
1232- while ( await plugin . app . vault . adapter . exists ( finalFilePath ) ) {
1233- finalFilePath = `${ importFolderPath } /${ sanitizedFileName } (${ counter } ).md` ;
1234- counter ++ ;
1293+ // Preserve source vault folder structure under import/{vaultName} when we have filePath from Content
1294+ const pathUnderImport =
1295+ contentFilePath && contentFilePath . includes ( "/" )
1296+ ? sanitizePathForImport ( contentFilePath )
1297+ : `${ sanitizedFileName } .md` ;
1298+ finalFilePath = `${ importFolderPath } /${ pathUnderImport } ` ;
1299+
1300+ // Ensure all parent folders exist (e.g. import/VaultName/Discourse Nodes/SubFolder)
1301+ const dirParts = finalFilePath . split ( "/" ) ;
1302+ for ( let i = 1 ; i < dirParts . length - 1 ; i ++ ) {
1303+ const folderPath = dirParts . slice ( 0 , i + 1 ) . join ( "/" ) ;
1304+ if ( ! ( await plugin . app . vault . adapter . exists ( folderPath ) ) ) {
1305+ await plugin . app . vault . createFolder ( folderPath ) ;
1306+ }
12351307 }
12361308 }
12371309
@@ -1243,7 +1315,7 @@ export const importSelectedNodes = async ({
12431315 sourceSpaceId : spaceId ,
12441316 sourceSpaceUri : spaceUri ,
12451317 rawContent : content ,
1246- originalFilePath : filePath ,
1318+ originalFilePath : contentFilePath ,
12471319 filePath : finalFilePath ,
12481320 importedCreatedAt : createdAt ,
12491321 importedModifiedAt : modifiedAt ,
@@ -1274,30 +1346,31 @@ export const importSelectedNodes = async ({
12741346 originalNodePath,
12751347 } ) ;
12761348
1277- // Update markdown content with new asset paths if assets were imported
1278- if ( assetImportResult . pathMapping . size > 0 ) {
1279- const currentContent = await plugin . app . vault . read ( processedFile ) ;
1280- const updatedContent = updateMarkdownAssetLinks ( {
1281- content : currentContent ,
1282- oldPathToNewPath : assetImportResult . pathMapping ,
1283- targetFile : processedFile ,
1284- app : plugin . app ,
1285- originalNodePath,
1286- } ) ;
1287-
1288- // Only update if content changed
1289- if ( updatedContent !== currentContent ) {
1290- await plugin . app . vault . process ( processedFile , ( ) => updatedContent ) ;
1291- }
1349+ // Update markdown content: rewrite asset paths from pathMapping and normalize all wiki links to relative paths
1350+ const currentContent = await plugin . app . vault . read ( processedFile ) ;
1351+ const updatedContent = updateMarkdownAssetLinks ( {
1352+ content : currentContent ,
1353+ oldPathToNewPath : assetImportResult . pathMapping ,
1354+ targetFile : processedFile ,
1355+ app : plugin . app ,
1356+ originalNodePath,
1357+ } ) ;
1358+
1359+ // Only update if content changed
1360+ if ( updatedContent !== currentContent ) {
1361+ await plugin . app . vault . process ( processedFile , ( ) => updatedContent ) ;
12921362 }
12931363
12941364 // If title changed and file exists, rename it to match the new title
12951365 if ( existingFile && processedFile . basename !== sanitizedFileName ) {
1296- const newPath = `${ importFolderPath } /${ sanitizedFileName } .md` ;
1366+ const currentDir = processedFile . path . includes ( "/" )
1367+ ? processedFile . path . replace ( / \/ [ ^ / ] * $ / , "" )
1368+ : importFolderPath ;
1369+ const newPath = `${ currentDir } /${ sanitizedFileName } .md` ;
12971370 let targetPath = newPath ;
12981371 let counter = 1 ;
12991372 while ( await plugin . app . vault . adapter . exists ( targetPath ) ) {
1300- targetPath = `${ importFolderPath } /${ sanitizedFileName } (${ counter } ).md` ;
1373+ targetPath = `${ currentDir } /${ sanitizedFileName } (${ counter } ).md` ;
13011374 counter ++ ;
13021375 }
13031376 await plugin . app . fileManager . renameFile ( processedFile , targetPath ) ;
@@ -1442,3 +1515,11 @@ export const refreshAllImportedFiles = async (
14421515
14431516 return { success : successCount , failed : failedCount , errors } ;
14441517} ;
1518+
1519+ const encodePathForMarkdownLink = ( linkPath : string ) : string => {
1520+ // Input is already decoded; encode each segment (spaces → %20) but keep / as separator
1521+ return linkPath
1522+ . split ( "/" )
1523+ . map ( ( segment ) => encodeURIComponent ( segment ) )
1524+ . join ( "/" ) ;
1525+ } ;
0 commit comments