Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface MarkdownExportPluginSettings {
relAttachPath: boolean;
convertWikiLinksToMarkdown: boolean;
removeYamlHeader: boolean;
// Block embed settings
inlineBlockEmbeds: boolean;
// Text export settings
textExportBulletPointMap: Record<number, string>;
textExportCheckboxUnchecked: string;
Expand All @@ -50,6 +52,8 @@ export const DEFAULT_SETTINGS: MarkdownExportPluginSettings = {
relAttachPath: true,
convertWikiLinksToMarkdown: false,
removeYamlHeader: false,
// Block embed settings
inlineBlockEmbeds: false,
// Text export settings
textExportBulletPointMap: {
0: "●",
Expand Down
14 changes: 14 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,20 @@ class MarkdownExportSettingTab extends PluginSettingTab {
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Inline Block Embeds")
.setDesc(
"If enabled, block embeds like ![[#^blockid]] will be replaced with the actual block content. " +
"If disabled, block embeds will be preserved as-is (may not work in exported markdown)."
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.inlineBlockEmbeds)
.onChange(async (value: boolean) => {
this.plugin.settings.inlineBlockEmbeds = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName("Remove YAML Metadata Header")
.setDesc(
Expand Down
161 changes: 145 additions & 16 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,114 @@ export async function getEmbedMap(
return embedMap;
}

/**
* Extract block content from a file using block reference ID
*/
async function getBlockContent(
plugin: MarkdownExportPlugin,
filePath: string,
blockId: string
): Promise<string | null> {
try {
// Get the file metadata
const file = plugin.app.vault.getAbstractFileByPath(filePath);
if (!(file instanceof TFile)) {
return null;
}

// Read file content and get metadata
const content = await plugin.app.vault.cachedRead(file);
const metadata = plugin.app.metadataCache.getFileCache(file);

if (!metadata || !metadata.blocks) {
return null;
}

// Find the block by ID
const block = metadata.blocks[blockId];
if (!block) {
return null;
}

// Extract the block content from the file
const lines = content.split("\n");
const startLine = block.position.start.line;
const endLine = block.position.end.line;

if (startLine >= lines.length) {
return null;
}

const blockLines = lines.slice(startLine, endLine + 1);
let blockContent = blockLines.join("\n");

// Remove the block ID from the content if present at the end
blockContent = blockContent.replace(/\s*\^([a-zA-Z0-9]+)\s*$/, "");

return blockContent;
} catch (error) {
console.error("Error getting block content:", error);
return null;
}
}

/**
* Parse embed link to extract file path and block reference
* Examples:
* - "My Note#^abc123" -> { filePath: "My Note.md", blockId: "abc123" }
* - "#^abc123" -> { filePath: null, blockId: "abc123" }
* - "My Note" -> { filePath: "My Note.md", blockId: null }
*/
function parseEmbedLink(embedLink: string, currentPath: string): {
filePath: string | null;
blockId: string | null;
heading: string | null;
} {
const blockMatch = embedLink.match(/^(.*?)#?\^([a-zA-Z0-9]+)$/);
if (blockMatch) {
const [, filePart, blockId] = blockMatch;
let filePath = filePart;
if (filePart && filePart.trim()) {
// Resolve relative path
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
filePath = currentDir ? `${currentDir}/${filePart}` : filePart;
if (!filePath.endsWith(".md")) {
filePath += ".md";
}
} else {
filePath = currentPath;
}
return { filePath, blockId, heading: null };
}

// Check for heading link
const headingMatch = embedLink.match(/^(.*?)#([^#]+)$/);
if (headingMatch) {
const [, filePart, heading] = headingMatch;
let filePath = filePart || currentPath;
if (filePart && filePart.trim()) {
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
filePath = currentDir ? `${currentDir}/${filePart}` : filePart;
if (!filePath.endsWith(".md")) {
filePath += ".md";
}
}
return { filePath, blockId: null, heading };
}

// Regular file link
if (embedLink.trim()) {
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
let filePath = currentDir ? `${currentDir}/${embedLink}` : embedLink;
if (!filePath.endsWith(".md")) {
filePath += ".md";
}
return { filePath, blockId: null, heading: null };
}

return { filePath: null, blockId: null, heading: null };
}

// Convert Markdown to plain text with specific formatting
export function convertMarkdownToText(
plugin: MarkdownExportPlugin,
Expand Down Expand Up @@ -690,32 +798,53 @@ export async function tryCopyMarkdownByRead(
content = content.replaceAll(OUTGOING_LINK_REGEXP, "$1");
}

if (plugin.settings.convertWikiLinksToMarkdown) {
content = content.replace(
/\[\[(.*?)\]\]/g,
(match, linkText) => {
const encodedLink = encodeURIComponent(linkText);
return `[${linkText}](${encodedLink})`;
}
);
}

// Process embeds BEFORE converting WikiLinks to Markdown
// This ensures block embeds like ![[#^blockid]] are handled correctly
const cfile = plugin.app.workspace.getActiveFile();
if (cfile != undefined) {
const embedMap = await getEmbedMap(plugin, content, cfile.path);
const embeds = await getEmbeds(content);
for (const index in embeds) {
const url = embeds[index][1];
const replacement = embedMap.get(url);
const embedMatch = embeds[index];
const fullMatch = embedMatch[0];
const embedLink = embedMatch[1];

let replacement = embedMap.get(embedLink);

// If not in embedMap and inlineBlockEmbeds is enabled, try to extract block content
if (replacement === undefined && plugin.settings.inlineBlockEmbeds) {
const parsed = parseEmbedLink(embedLink, cfile.path);
if (parsed.blockId && parsed.filePath) {
const blockContent = await getBlockContent(
plugin,
parsed.filePath,
parsed.blockId
);
if (blockContent !== null) {
// Format block content as quote block
replacement = "> " + blockContent.replace(/\n/g, "\n> ");
}
}
}

// Only replace if we found a replacement
// This prevents replacing with "undefined"
if (replacement !== undefined) {
content = content.replace(
embeds[index][0],
replacement
);
content = content.replace(fullMatch, replacement);
}
}
}

if (plugin.settings.convertWikiLinksToMarkdown) {
content = content.replace(
/\[\[(.*?)\]\]/g,
(match, linkText) => {
const encodedLink = encodeURIComponent(linkText);
return `[${linkText}](${encodedLink})`;
}
);
}

await tryCopyImage(plugin, file.name, file.path);

// If the user has a custom filename set, we enforce subdirectories to
Expand Down