From 1ec11dbedbe6c7f745071d00bb105c64b5f938a0 Mon Sep 17 00:00:00 2001 From: c14sama Date: Mon, 20 Oct 2025 20:30:50 +0300 Subject: [PATCH 1/2] recursiveExport --- src/config.ts | 4 + src/main.ts | 16 +++ src/utils.ts | 301 +++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 270 insertions(+), 51 deletions(-) diff --git a/src/config.ts b/src/config.ts index 0d8fef8..c484da1 100644 --- a/src/config.ts +++ b/src/config.ts @@ -31,6 +31,8 @@ export interface MarkdownExportPluginSettings { relAttachPath: boolean; convertWikiLinksToMarkdown: boolean; removeYamlHeader: boolean; + // Recursive export settings + recursiveExport: boolean; // Text export settings textExportBulletPointMap: Record; textExportCheckboxUnchecked: string; @@ -50,6 +52,8 @@ export const DEFAULT_SETTINGS: MarkdownExportPluginSettings = { relAttachPath: true, convertWikiLinksToMarkdown: false, removeYamlHeader: false, + // Recursive export settings + recursiveExport: false, // Text export settings textExportBulletPointMap: { 0: "●", diff --git a/src/main.ts b/src/main.ts index a35fd33..4e57df3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -307,6 +307,22 @@ class MarkdownExportSettingTab extends PluginSettingTab { }) ); + containerEl.createEl("h3", { text: "Advanced Export Settings" }); + + new Setting(containerEl) + .setName("Recursive Export") + .setDesc( + "If enabled, all linked markdown files will be exported recursively. This will follow internal links and export referenced files to avoid broken links. Circular references are automatically detected and prevented." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.recursiveExport) + .onChange(async (value: boolean) => { + this.plugin.settings.recursiveExport = value; + await this.plugin.saveSettings(); + }) + ); + containerEl.createEl("h3", { text: "Export Text Setting" }); // Bullet point mapping settings diff --git a/src/utils.ts b/src/utils.ts index 8ddd305..f41169f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,11 @@ type CopyMarkdownOptions = { outputSubPath: string; }; +export async function getOutgoingLinks(markdown: string) { + const outgoingLinks = markdown.matchAll(OUTGOING_LINK_REGEXP); + return Array.from(outgoingLinks); +} + export async function getImageLinks(markdown: string) { const imageLinks = markdown.matchAll(ATTACHMENT_URL_REGEXP); const markdownImageLinks = markdown.matchAll( @@ -97,8 +102,27 @@ export async function tryRun( try { const params = allMarkdownParams(file, [], outputFormat); - for (const param of params) { - await tryCopyMarkdownByRead(plugin, param); + + if (plugin.settings.recursiveExport) { + // Use Set to track processed files and avoid circular references + const processedFiles = new Set(); + const allFilesToProcess = new Set(); + + // Get the root file (first file being exported) to determine base output path + const rootFile = params.length > 0 ? params[0].file : file; + + // Add initial files to the processing queue + for (const param of params) { + allFilesToProcess.add(param); + } + + // Process files recursively with root file context + await processFilesRecursively(plugin, allFilesToProcess, processedFiles, outputFormat, rootFile); + } else { + // Original behavior for non-recursive export + for (const param of params) { + await tryCopyMarkdownByRead(plugin, param); + } } } catch (error) { if (!error.message.contains("file already exists")) { @@ -107,6 +131,84 @@ export async function tryRun( } } +/** + * Process files recursively, following internal links and avoiding circular references + */ +async function processFilesRecursively( + plugin: MarkdownExportPlugin, + filesToProcess: Set, + processedFiles: Set, + outputFormat: string, + rootFile: TAbstractFile +) { + const processQueue = Array.from(filesToProcess); + + for (const fileParam of processQueue) { + const filePath = fileParam.file.path; + + // Skip if already processed to avoid circular references + if (processedFiles.has(filePath)) { + continue; + } + + // Mark as processed before processing to prevent infinite loops + processedFiles.add(filePath); + + // Process the current file with root file context for recursive exports + await tryCopyMarkdownByRead(plugin, fileParam, rootFile); + + // Only process markdown files for recursive link detection + if ((fileParam.file).extension === 'md') { + try { + // Read file content to find internal links + const content = await plugin.app.vault.adapter.read(filePath); + const outgoingLinks = await getOutgoingLinks(content); + + // Process each outgoing link + for (const linkMatch of outgoingLinks) { + const linkText = linkMatch[1]; + + // Skip if link is empty or already processed + if (!linkText || processedFiles.has(linkText)) { + continue; + } + + // Resolve the link to an actual file + const linkedFile = plugin.app.metadataCache.getFirstLinkpathDest( + linkText, + filePath + ); + + // If the linked file exists and is a markdown file, add it to the processing queue + if (linkedFile && linkedFile instanceof TFile && linkedFile.extension === 'md') { + const linkedFilePath = linkedFile.path; + + // Skip if already processed + if (!processedFiles.has(linkedFilePath)) { + const linkedFileParam: CopyMarkdownOptions = { + file: linkedFile, + outputFormat: outputFormat, + outputSubPath: "." // All linked files go to the root export directory + }; + + // Recursively process the linked file with root file context + await processFilesRecursively( + plugin, + new Set([linkedFileParam]), + processedFiles, + outputFormat, + rootFile + ); + } + } + } + } catch (error) { + console.warn(`Failed to process recursive links for ${filePath}: ${error.message}`); + } + } + } +} + export function getResourceOsPath( plugin: MarkdownExportPlugin, resouorce: TFile | null @@ -200,7 +302,8 @@ export async function tryCreate( export async function tryCopyImage( plugin: MarkdownExportPlugin, filename: string, - contentPath: string + contentPath: string, + rootFile?: TAbstractFile ) { try { await plugin.app.vault.adapter @@ -240,20 +343,41 @@ export async function tryCopyImage( continue; } - const targetPath = path - .join( - plugin.settings.relAttachPath - ? plugin.settings.output - : plugin.settings.attachment, - plugin.settings.includeFileName - ? filename.replace(".md", "") - : "", - plugin.settings.relAttachPath - ? plugin.settings.attachment - : "", - imageLinkMd5.concat(imageExt) - ) - .replace(/\\/g, "/"); + let targetPath: string; + if (plugin.settings.recursiveExport && rootFile) { + // In recursive mode, use root file for target path + const rootFileName = rootFile instanceof TFile ? rootFile.name : "export"; + targetPath = path + .join( + plugin.settings.relAttachPath + ? plugin.settings.output + : plugin.settings.attachment, + plugin.settings.includeFileName + ? rootFileName.replace(".md", "") + : "", + plugin.settings.relAttachPath + ? plugin.settings.attachment + : "", + imageLinkMd5.concat(imageExt) + ) + .replace(/\\/g, "/"); + } else { + // Original behavior for non-recursive export + targetPath = path + .join( + plugin.settings.relAttachPath + ? plugin.settings.output + : plugin.settings.attachment, + plugin.settings.includeFileName + ? filename.replace(".md", "") + : "", + plugin.settings.relAttachPath + ? plugin.settings.attachment + : "", + imageLinkMd5.concat(imageExt) + ) + .replace(/\\/g, "/"); + } try { if (!fileExists(targetPath)) { @@ -438,15 +562,31 @@ export function convertMarkdownToText( export async function tryCopyMarkdownByRead( plugin: MarkdownExportPlugin, - { file, outputFormat, outputSubPath = "." }: CopyMarkdownOptions + { file, outputFormat, outputSubPath = "." }: CopyMarkdownOptions, + rootFile?: TAbstractFile ) { try { await plugin.app.vault.adapter.read(file.path).then(async (content) => { const imageLinks = await getImageLinks(content); if (imageLinks.length > 0) { - await tryCreateFolder( - plugin, - path.join( + let attachmentDir: string; + if (plugin.settings.recursiveExport && rootFile) { + // In recursive mode, use root file for attachment directory + const rootFileName = rootFile instanceof TFile ? rootFile.name : "export"; + attachmentDir = path.join( + plugin.settings.relAttachPath + ? plugin.settings.output + : plugin.settings.attachment, + plugin.settings.includeFileName + ? rootFileName.replace(".md", "") + : "", + plugin.settings.relAttachPath + ? plugin.settings.attachment + : "" + ); + } else { + // Original behavior for non-recursive export + attachmentDir = path.join( plugin.settings.relAttachPath ? plugin.settings.output : plugin.settings.attachment, @@ -456,8 +596,9 @@ export async function tryCopyMarkdownByRead( plugin.settings.relAttachPath ? plugin.settings.attachment : "" - ) - ); + ); + } + await tryCreateFolder(plugin, attachmentDir); } for (const index in imageLinks) { @@ -485,22 +626,45 @@ export async function tryCopyMarkdownByRead( const clickSubRoute = getClickSubRoute(outputSubPath); - const hashLink = path - .join( - clickSubRoute, - plugin.settings.relAttachPath - ? plugin.settings.attachment - : path.join( - plugin.settings.customAttachPath - ? plugin.settings.customAttachPath - : plugin.settings.attachment, - plugin.settings.includeFileName - ? file.name.replace(".md", "") - : "" - ), - imageLinkMd5.concat(imageExt) - ) - .replace(/\\/g, "/"); + let hashLink: string; + if (plugin.settings.recursiveExport && rootFile) { + // In recursive mode, all images point to root file's attachment folder + const rootFileName = rootFile instanceof TFile ? rootFile.name : "export"; + hashLink = path + .join( + clickSubRoute, + plugin.settings.relAttachPath + ? plugin.settings.attachment + : path.join( + plugin.settings.customAttachPath + ? plugin.settings.customAttachPath + : plugin.settings.attachment, + plugin.settings.includeFileName + ? rootFileName.replace(".md", "") + : "" + ), + imageLinkMd5.concat(imageExt) + ) + .replace(/\\/g, "/"); + } else { + // Original behavior for non-recursive export + hashLink = path + .join( + clickSubRoute, + plugin.settings.relAttachPath + ? plugin.settings.attachment + : path.join( + plugin.settings.customAttachPath + ? plugin.settings.customAttachPath + : plugin.settings.attachment, + plugin.settings.includeFileName + ? file.name.replace(".md", "") + : "" + ), + imageLinkMd5.concat(imageExt) + ) + .replace(/\\/g, "/"); + } // filter markdown link eg: http://xxx.png if (urlEncodedImageLink.startsWith("http")) { @@ -541,8 +705,26 @@ export async function tryCopyMarkdownByRead( content = content.replace( /\[\[(.*?)\]\]/g, (match, linkText) => { - const encodedLink = encodeURIComponent(linkText); - return `[${linkText}](${encodedLink})`; + if (plugin.settings.recursiveExport) { + // In recursive mode, adjust links to point to exported files + // Remove any path separators and just use the filename + const cleanLinkText = linkText.split('/').pop() || linkText; + const encodedLink = encodeURIComponent(cleanLinkText); + return `[${linkText}](${encodedLink}.md)`; + } else { + const encodedLink = encodeURIComponent(linkText); + return `[${linkText}](${encodedLink})`; + } + } + ); + } else if (plugin.settings.recursiveExport) { + // Even if not converting to markdown links, we need to handle wikilinks in recursive mode + content = content.replace( + /\[\[(.*?)\]\]/g, + (match, linkText) => { + // In recursive mode, keep the link but adjust the path if needed + const cleanLinkText = linkText.split('/').pop() || linkText; + return `[[${cleanLinkText}]]`; } ); } @@ -560,19 +742,36 @@ export async function tryCopyMarkdownByRead( } } - await tryCopyImage(plugin, file.name, file.path); + await tryCopyImage(plugin, file.name, file.path, rootFile); // If the user has a custom filename set, we enforce subdirectories to // prevent rendered files from overwriting each other - const outDir = path.join( - plugin.settings.output, - plugin.settings.customFileName != "" || - (plugin.settings.includeFileName && - plugin.settings.relAttachPath) - ? file.name.replace(".md", "") - : "", - outputSubPath - ); + let outDir: string; + + if (plugin.settings.recursiveExport && rootFile) { + // In recursive mode, put all files in the root file's output directory + // Use the root file name for directory structure, not the current file + const rootFileName = rootFile instanceof TFile ? rootFile.name : "export"; + outDir = path.join( + plugin.settings.output, + plugin.settings.customFileName != "" || + (plugin.settings.includeFileName && + plugin.settings.relAttachPath) + ? rootFileName.replace(".md", "") + : "" + ); + } else { + // Original behavior for non-recursive export + outDir = path.join( + plugin.settings.output, + plugin.settings.customFileName != "" || + (plugin.settings.includeFileName && + plugin.settings.relAttachPath) + ? file.name.replace(".md", "") + : "", + outputSubPath + ); + } await tryCreateFolder(plugin, outDir); From 372628db1879e45ec75808b105b04fafdfb30ad3 Mon Sep 17 00:00:00 2001 From: c14sama Date: Mon, 20 Oct 2025 20:45:43 +0300 Subject: [PATCH 2/2] support all attachments --- RECURSIVE_EXPORT_FEATURE.md | 186 ++++++++++++++++++++++++++++++++++++ src/config.ts | 4 + src/main.ts | 14 +++ src/utils.ts | 145 +++++++++++++++++++++++++++- 4 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 RECURSIVE_EXPORT_FEATURE.md diff --git a/RECURSIVE_EXPORT_FEATURE.md b/RECURSIVE_EXPORT_FEATURE.md new file mode 100644 index 0000000..4f9db8f --- /dev/null +++ b/RECURSIVE_EXPORT_FEATURE.md @@ -0,0 +1,186 @@ +# Advanced Export Features + +This document explains the newly added advanced export functionalities for the Obsidian Markdown Export Plugin. + +## Overview + +The recursive export feature allows the plugin to automatically follow internal links in markdown files and export all referenced files together. This ensures that the exported content maintains its link structure and prevents broken links in the exported files. + +## Key Features + +### Recursive Export Feature + +#### 1. Recursive Link Following +- When enabled, the plugin scans each exported markdown file for internal links (`[[link]]` format) +- It automatically identifies and exports all linked markdown files +- The process continues recursively through all discovered links + +#### 2. Circular Reference Prevention +- Uses a `Set` to track processed files and avoid infinite loops +- Each file path is marked as processed before being exported +- Prevents re-processing of already exported files + +#### 3. Clean Export Structure +- All recursively exported files are placed in the main export directory +- Attachments are consolidated in the attachment folder within the main export directory +- Links are adjusted to point to the correct file locations in the export + +#### 4. Link Path Adjustment +- In recursive mode, wikilinks are adjusted to remove path separators +- Links point to the filename only (since all files are in the same export directory) +- Maintains compatibility with both wikilink and markdown link formats + +### Export All Attachments Feature + +#### 1. Comprehensive File Export +- Exports all linked files, not just images +- Includes PDFs, documents, archives, and other file types +- Works with both `[[file.pdf]]` and `![[file.pdf]]` syntax + +#### 2. Smart File Type Detection +- Automatically detects and handles different file extensions +- Excludes already-processed images (handled by existing image export) +- Excludes markdown files (handled by recursive export if enabled) +- Skips HTTP/external links + +#### 3. Unified Attachment Management +- All attachment types are placed in the same attachment directory +- Uses consistent file naming (MD5 hash or original name based on settings) +- Maintains file extension integrity + +## Configuration + +### New Settings in Advanced Export Settings + +#### 1. Recursive Export +- **Type**: Toggle (boolean) +- **Default**: `false` (disabled) +- **Description**: "If enabled, all linked markdown files will be exported recursively. This will follow internal links and export referenced files to avoid broken links. Circular references are automatically detected and prevented." + +#### 2. Export All Attachments +- **Type**: Toggle (boolean) +- **Default**: `false` (disabled) +- **Description**: "If enabled, all linked files will be exported, including non-image attachments like PDFs, documents, etc. This applies to both [[file.pdf]] and ![[file.pdf]] syntax." + +## Technical Implementation + +### Core Algorithm +1. **Root File Context**: The first selected file determines the base export directory name +2. **Unified Output**: All recursively found files are placed in the root file's export directory +3. **Attachment Consolidation**: All attachments from all files are moved to a single attachment folder within the root directory +4. **Link Resolution**: Internal links are adjusted to work with the flattened structure + +### New Functions +1. `getOutgoingLinks(markdown: string)` - Extracts all outgoing wikilinks from markdown content +2. `getAllFileLinks(markdown: string)` - Extracts all file links (both `![[]]` and `[[]]`) for any file type +3. `processFilesRecursively()` - Main recursive processing function that handles: + - File processing queue management + - Circular reference detection + - Link resolution and file discovery + - **Root file context preservation** +4. `tryCopyAllAttachments()` - Handles export of all attachment types (PDFs, documents, etc.) + +### Modified Functions +1. `tryRun()` - Enhanced to support both recursive and non-recursive export modes + - Passes root file context to recursive processing +2. `tryCopyMarkdownByRead()` - Updated to handle output directory structure for recursive exports + - Uses root file name for directory structure instead of current file name + - Adjusts attachment paths to point to consolidated attachment folder + - **Added support for all attachment types when enabled** +3. `tryCopyImage()` - Modified to consolidate all attachments in root file's attachment folder +4. Link processing logic - Adjusted to handle path resolution in recursive mode + +### Root File Context Flow +``` +User selects file/folder → tryRun() + ↓ +Identifies root file (first file in selection) + ↓ +processFilesRecursively(rootFile) + ↓ +All subsequent files use rootFile for: + - Output directory naming + - Attachment folder location + - Link path calculation +``` + +## Usage + +### Basic Usage +1. Open the plugin settings in Obsidian +2. Navigate to "Advanced Export Settings" +3. Configure the desired features: + - **Recursive Export**: Enable to follow markdown links recursively + - **Export All Attachments**: Enable to export all file types (PDFs, docs, etc.) +4. Export any markdown file or folder as usual +5. All configured content will be automatically included in the export + +### Export All Attachments Examples + +**Without Export All Attachments (Default):** +- `![[image.png]]` → ✅ Exported (image) +- `[[document.pdf]]` → ❌ Not exported (non-image file) +- `![[presentation.pptx]]` → ❌ Not exported (non-image file) + +**With Export All Attachments Enabled:** +- `![[image.png]]` → ✅ Exported (image) +- `[[document.pdf]]` → ✅ Exported (PDF document) +- `![[presentation.pptx]]` → ✅ Exported (PowerPoint file) +- `[[data.xlsx]]` → ✅ Exported (Excel file) +- `![[archive.zip]]` → ✅ Exported (archive file) + +## Benefits + +- **Complete Export**: Ensures all referenced content is included +- **No Broken Links**: Maintains link integrity in the exported files +- **Clean Organization**: All files are consolidated in a single export directory +- **Safe Processing**: Prevents infinite loops from circular references +- **Flexible**: Can be enabled/disabled based on user needs + +## File Structure Example + +**Before Export (Vault Structure):** +``` +vault/ +├── folder1/ +│ ├── noteA.md (links to noteB.md, noteC.md, and contains ![[report.pdf]]) +│ └── attachments/ +│ ├── imageA.png +│ ├── imageA2.jpg +│ └── report.pdf +├── folder2/ +│ ├── noteB.md (links to noteC.md and contains [[data.xlsx]]) +│ ├── attachments/ +│ │ ├── imageB.png +│ │ └── data.xlsx +│ └── noteC.md (contains ![[presentation.pptx]]) +│ └── attachments/ +│ ├── imageC.gif +│ └── presentation.pptx +└── folder3/ + └── noteD.md (not linked) +``` + +**After Export with Both Features Enabled (when exporting noteA.md):** +``` +output/ +├── noteA/ # Root folder named after the first selected file +│ ├── noteA.md # Original file +│ ├── noteB.md # Recursively exported file +│ ├── noteC.md # Recursively exported file +│ └── attachment/ # All attachments in one folder +│ ├── imageA.png # Images (always exported) +│ ├── imageA2.jpg # Images (always exported) +│ ├── imageB.png # Images (always exported) +│ ├── imageC.gif # Images (always exported) +│ ├── report.pdf # PDF (exported due to Export All Attachments) +│ ├── data.xlsx # Excel (exported due to Export All Attachments) +│ └── presentation.pptx # PowerPoint (exported due to Export All Attachments) +``` + +**Key Points:** +- All markdown files go into the folder named after the **first selected file** (noteA) +- All attachments from all files are consolidated into **one attachment folder** +- Links in exported files are adjusted to point to the correct locations +- `noteD.md` is not included because it's not linked from the exported files +- The structure is clean and self-contained \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c484da1..297582a 100644 --- a/src/config.ts +++ b/src/config.ts @@ -33,6 +33,8 @@ export interface MarkdownExportPluginSettings { removeYamlHeader: boolean; // Recursive export settings recursiveExport: boolean; + // Export all attachments (including non-image files like PDFs) + exportAllAttachments: boolean; // Text export settings textExportBulletPointMap: Record; textExportCheckboxUnchecked: string; @@ -54,6 +56,8 @@ export const DEFAULT_SETTINGS: MarkdownExportPluginSettings = { removeYamlHeader: false, // Recursive export settings recursiveExport: false, + // Export all attachments (including non-image files like PDFs) + exportAllAttachments: false, // Text export settings textExportBulletPointMap: { 0: "●", diff --git a/src/main.ts b/src/main.ts index 4e57df3..ac9ff98 100644 --- a/src/main.ts +++ b/src/main.ts @@ -323,6 +323,20 @@ class MarkdownExportSettingTab extends PluginSettingTab { }) ); + new Setting(containerEl) + .setName("Export All Attachments") + .setDesc( + "If enabled, all linked files will be exported, including non-image attachments like PDFs, documents, etc. This applies to both [[file.pdf]] and ![[file.pdf]] syntax." + ) + .addToggle((toggle) => + toggle + .setValue(this.plugin.settings.exportAllAttachments) + .onChange(async (value: boolean) => { + this.plugin.settings.exportAllAttachments = value; + await this.plugin.saveSettings(); + }) + ); + containerEl.createEl("h3", { text: "Export Text Setting" }); // Bullet point mapping settings diff --git a/src/utils.ts b/src/utils.ts index f41169f..e9ec360 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -26,6 +26,13 @@ export async function getOutgoingLinks(markdown: string) { return Array.from(outgoingLinks); } +export async function getAllFileLinks(markdown: string) { + // Match both ![[file]] (embedded) and [[file]] (linked) patterns for all file types + const allFileLinksRegexp = /\!?\[\[((.*?)\.([\w]+))(?:\s*\|\s*.*?)?\]\]/g; + const allFileLinks = markdown.matchAll(allFileLinksRegexp); + return Array.from(allFileLinks); +} + export async function getImageLinks(markdown: string) { const imageLinks = markdown.matchAll(ATTACHMENT_URL_REGEXP); const markdownImageLinks = markdown.matchAll( @@ -411,6 +418,131 @@ export async function tryCopyImage( } } +export async function tryCopyAllAttachments( + plugin: MarkdownExportPlugin, + filename: string, + contentPath: string, + rootFile?: TAbstractFile +) { + try { + await plugin.app.vault.adapter + .read(contentPath) + .then(async (content) => { + const allFileLinks = await getAllFileLinks(content); + for (const index in allFileLinks) { + const rawFileLink = allFileLinks[index][0]; + const fullFileName = allFileLinks[index][1]; // e.g., "document.pdf" + const fileNameWithoutExt = allFileLinks[index][2]; // e.g., "document" + const fileExt = allFileLinks[index][3]; // e.g., "pdf" + + // Skip if it's an image (already handled by tryCopyImage) + const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg', 'webp', 'tiff']; + if (imageExtensions.includes(fileExt.toLowerCase())) { + continue; + } + + // Skip markdown files (handled by recursive export) + if (fileExt.toLowerCase() === 'md') { + continue; + } + + // Skip HTTP links + if (fullFileName.startsWith("http")) { + continue; + } + + // Clean the file link (remove display text if any) + let cleanFileName = fullFileName; + if (cleanFileName.contains("|")) { + cleanFileName = cleanFileName.split("|")[0]; + } + + const baseFileName = path.parse(path.basename(cleanFileName)).name; + const fileLinkMd5 = plugin.settings.fileNameEncode + ? md5(cleanFileName) + : baseFileName; + const cleanFileExt = path.extname(cleanFileName); + + // Resolve the file path + const linkedFile = plugin.app.metadataCache.getFirstLinkpathDest( + cleanFileName, + contentPath + ); + + const sourcePath = + linkedFile !== null + ? linkedFile.path + : path.join(path.dirname(contentPath), cleanFileName); + + // Calculate target path + let targetPath: string; + if (plugin.settings.recursiveExport && rootFile) { + // In recursive mode, use root file for target path + const rootFileName = rootFile instanceof TFile ? rootFile.name : "export"; + targetPath = path + .join( + plugin.settings.relAttachPath + ? plugin.settings.output + : plugin.settings.attachment, + plugin.settings.includeFileName + ? rootFileName.replace(".md", "") + : "", + plugin.settings.relAttachPath + ? plugin.settings.attachment + : "", + fileLinkMd5.concat(cleanFileExt) + ) + .replace(/\\/g, "/"); + } else { + // Original behavior for non-recursive export + targetPath = path + .join( + plugin.settings.relAttachPath + ? plugin.settings.output + : plugin.settings.attachment, + plugin.settings.includeFileName + ? filename.replace(".md", "") + : "", + plugin.settings.relAttachPath + ? plugin.settings.attachment + : "", + fileLinkMd5.concat(cleanFileExt) + ) + .replace(/\\/g, "/"); + } + + try { + if (!fileExists(targetPath)) { + if ( + plugin.settings.output.startsWith("/") || + path.win32.isAbsolute(plugin.settings.output) + ) { + const resourceOsPath = getResourceOsPath( + plugin, + linkedFile + ); + fs.copyFileSync(resourceOsPath, targetPath); + } else { + await plugin.app.vault.adapter.copy( + sourcePath, + targetPath + ); + } + } + } catch (error) { + console.error( + `Failed to copy attachment from ${sourcePath} to ${targetPath}: ${error.message}` + ); + } + } + }); + } catch (error) { + if (!error.message.contains("file already exists")) { + throw error; + } + } +} + export async function tryCopyMarkdown( plugin: MarkdownExportPlugin, contentPath: string, @@ -567,8 +699,14 @@ export async function tryCopyMarkdownByRead( ) { try { await plugin.app.vault.adapter.read(file.path).then(async (content) => { + // Get both image links and all file links based on settings const imageLinks = await getImageLinks(content); - if (imageLinks.length > 0) { + const allFileLinks = plugin.settings.exportAllAttachments ? await getAllFileLinks(content) : []; + + // Combine links for attachment directory creation check + const totalAttachmentLinks = imageLinks.length + allFileLinks.length; + + if (totalAttachmentLinks > 0) { let attachmentDir: string; if (plugin.settings.recursiveExport && rootFile) { // In recursive mode, use root file for attachment directory @@ -744,6 +882,11 @@ export async function tryCopyMarkdownByRead( await tryCopyImage(plugin, file.name, file.path, rootFile); + // Process all file attachments if exportAllAttachments is enabled + if (plugin.settings.exportAllAttachments) { + await tryCopyAllAttachments(plugin, file.name, file.path, rootFile); + } + // If the user has a custom filename set, we enforce subdirectories to // prevent rendered files from overwriting each other let outDir: string;