Skip to content

Commit fe1184c

Browse files
bingryanclaude
andcommitted
fix: add inline block embeds support to fix #114
- Add new setting "Inline Block Embeds" to replace block refs with content - Fix execution order: process embeds before WikiLinks conversion - Add getBlockContent() to extract block content using Obsidian API - Add parseEmbedLink() to parse file path and block reference ID - Prevent "undefined" output when embed content is not found Fixes #114 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 35a1029 commit fe1184c

3 files changed

Lines changed: 163 additions & 16 deletions

File tree

src/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ export interface MarkdownExportPluginSettings {
3131
relAttachPath: boolean;
3232
convertWikiLinksToMarkdown: boolean;
3333
removeYamlHeader: boolean;
34+
// Block embed settings
35+
inlineBlockEmbeds: boolean;
3436
// Text export settings
3537
textExportBulletPointMap: Record<number, string>;
3638
textExportCheckboxUnchecked: string;
@@ -50,6 +52,8 @@ export const DEFAULT_SETTINGS: MarkdownExportPluginSettings = {
5052
relAttachPath: true,
5153
convertWikiLinksToMarkdown: false,
5254
removeYamlHeader: false,
55+
// Block embed settings
56+
inlineBlockEmbeds: false,
5357
// Text export settings
5458
textExportBulletPointMap: {
5559
0: "●",

src/main.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,20 @@ class MarkdownExportSettingTab extends PluginSettingTab {
438438
await this.plugin.saveSettings();
439439
})
440440
);
441+
new Setting(containerEl)
442+
.setName("Inline Block Embeds")
443+
.setDesc(
444+
"If enabled, block embeds like ![[#^blockid]] will be replaced with the actual block content. " +
445+
"If disabled, block embeds will be preserved as-is (may not work in exported markdown)."
446+
)
447+
.addToggle((toggle) =>
448+
toggle
449+
.setValue(this.plugin.settings.inlineBlockEmbeds)
450+
.onChange(async (value: boolean) => {
451+
this.plugin.settings.inlineBlockEmbeds = value;
452+
await this.plugin.saveSettings();
453+
})
454+
);
441455
new Setting(containerEl)
442456
.setName("Remove YAML Metadata Header")
443457
.setDesc(

src/utils.ts

Lines changed: 145 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,114 @@ export async function getEmbedMap(
479479
return embedMap;
480480
}
481481

482+
/**
483+
* Extract block content from a file using block reference ID
484+
*/
485+
async function getBlockContent(
486+
plugin: MarkdownExportPlugin,
487+
filePath: string,
488+
blockId: string
489+
): Promise<string | null> {
490+
try {
491+
// Get the file metadata
492+
const file = plugin.app.vault.getAbstractFileByPath(filePath);
493+
if (!(file instanceof TFile)) {
494+
return null;
495+
}
496+
497+
// Read file content and get metadata
498+
const content = await plugin.app.vault.cachedRead(file);
499+
const metadata = plugin.app.metadataCache.getFileCache(file);
500+
501+
if (!metadata || !metadata.blocks) {
502+
return null;
503+
}
504+
505+
// Find the block by ID
506+
const block = metadata.blocks[blockId];
507+
if (!block) {
508+
return null;
509+
}
510+
511+
// Extract the block content from the file
512+
const lines = content.split("\n");
513+
const startLine = block.position.start.line;
514+
const endLine = block.position.end.line;
515+
516+
if (startLine >= lines.length) {
517+
return null;
518+
}
519+
520+
const blockLines = lines.slice(startLine, endLine + 1);
521+
let blockContent = blockLines.join("\n");
522+
523+
// Remove the block ID from the content if present at the end
524+
blockContent = blockContent.replace(/\s*\^([a-zA-Z0-9]+)\s*$/, "");
525+
526+
return blockContent;
527+
} catch (error) {
528+
console.error("Error getting block content:", error);
529+
return null;
530+
}
531+
}
532+
533+
/**
534+
* Parse embed link to extract file path and block reference
535+
* Examples:
536+
* - "My Note#^abc123" -> { filePath: "My Note.md", blockId: "abc123" }
537+
* - "#^abc123" -> { filePath: null, blockId: "abc123" }
538+
* - "My Note" -> { filePath: "My Note.md", blockId: null }
539+
*/
540+
function parseEmbedLink(embedLink: string, currentPath: string): {
541+
filePath: string | null;
542+
blockId: string | null;
543+
heading: string | null;
544+
} {
545+
const blockMatch = embedLink.match(/^(.*?)#?\^([a-zA-Z0-9]+)$/);
546+
if (blockMatch) {
547+
const [, filePart, blockId] = blockMatch;
548+
let filePath = filePart;
549+
if (filePart && filePart.trim()) {
550+
// Resolve relative path
551+
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
552+
filePath = currentDir ? `${currentDir}/${filePart}` : filePart;
553+
if (!filePath.endsWith(".md")) {
554+
filePath += ".md";
555+
}
556+
} else {
557+
filePath = currentPath;
558+
}
559+
return { filePath, blockId, heading: null };
560+
}
561+
562+
// Check for heading link
563+
const headingMatch = embedLink.match(/^(.*?)#([^#]+)$/);
564+
if (headingMatch) {
565+
const [, filePart, heading] = headingMatch;
566+
let filePath = filePart || currentPath;
567+
if (filePart && filePart.trim()) {
568+
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
569+
filePath = currentDir ? `${currentDir}/${filePart}` : filePart;
570+
if (!filePath.endsWith(".md")) {
571+
filePath += ".md";
572+
}
573+
}
574+
return { filePath, blockId: null, heading };
575+
}
576+
577+
// Regular file link
578+
if (embedLink.trim()) {
579+
const currentDir = currentPath.substring(0, currentPath.lastIndexOf("/"));
580+
let filePath = currentDir ? `${currentDir}/${embedLink}` : embedLink;
581+
if (!filePath.endsWith(".md")) {
582+
filePath += ".md";
583+
}
584+
return { filePath, blockId: null, heading: null };
585+
}
586+
587+
return { filePath: null, blockId: null, heading: null };
588+
}
589+
482590
// Convert Markdown to plain text with specific formatting
483591
export function convertMarkdownToText(
484592
plugin: MarkdownExportPlugin,
@@ -690,32 +798,53 @@ export async function tryCopyMarkdownByRead(
690798
content = content.replaceAll(OUTGOING_LINK_REGEXP, "$1");
691799
}
692800

693-
if (plugin.settings.convertWikiLinksToMarkdown) {
694-
content = content.replace(
695-
/\[\[(.*?)\]\]/g,
696-
(match, linkText) => {
697-
const encodedLink = encodeURIComponent(linkText);
698-
return `[${linkText}](${encodedLink})`;
699-
}
700-
);
701-
}
702-
801+
// Process embeds BEFORE converting WikiLinks to Markdown
802+
// This ensures block embeds like ![[#^blockid]] are handled correctly
703803
const cfile = plugin.app.workspace.getActiveFile();
704804
if (cfile != undefined) {
705805
const embedMap = await getEmbedMap(plugin, content, cfile.path);
706806
const embeds = await getEmbeds(content);
707807
for (const index in embeds) {
708-
const url = embeds[index][1];
709-
const replacement = embedMap.get(url);
808+
const embedMatch = embeds[index];
809+
const fullMatch = embedMatch[0];
810+
const embedLink = embedMatch[1];
811+
812+
let replacement = embedMap.get(embedLink);
813+
814+
// If not in embedMap and inlineBlockEmbeds is enabled, try to extract block content
815+
if (replacement === undefined && plugin.settings.inlineBlockEmbeds) {
816+
const parsed = parseEmbedLink(embedLink, cfile.path);
817+
if (parsed.blockId && parsed.filePath) {
818+
const blockContent = await getBlockContent(
819+
plugin,
820+
parsed.filePath,
821+
parsed.blockId
822+
);
823+
if (blockContent !== null) {
824+
// Format block content as quote block
825+
replacement = "> " + blockContent.replace(/\n/g, "\n> ");
826+
}
827+
}
828+
}
829+
830+
// Only replace if we found a replacement
831+
// This prevents replacing with "undefined"
710832
if (replacement !== undefined) {
711-
content = content.replace(
712-
embeds[index][0],
713-
replacement
714-
);
833+
content = content.replace(fullMatch, replacement);
715834
}
716835
}
717836
}
718837

838+
if (plugin.settings.convertWikiLinksToMarkdown) {
839+
content = content.replace(
840+
/\[\[(.*?)\]\]/g,
841+
(match, linkText) => {
842+
const encodedLink = encodeURIComponent(linkText);
843+
return `[${linkText}](${encodedLink})`;
844+
}
845+
);
846+
}
847+
719848
await tryCopyImage(plugin, file.name, file.path);
720849

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

0 commit comments

Comments
 (0)