diff --git a/main.js b/main.js index c0c48bd..4b5e5c3 100644 --- a/main.js +++ b/main.js @@ -1,23 +1,20 @@ -const { Plugin, MarkdownView, WorkspaceLeaf, Component } = require('obsidian'); +const { Plugin, MarkdownView, WorkspaceLeaf, Component, debounce } = require('obsidian'); module.exports = class SyncEmbedPlugin extends Plugin { async onload() { console.log('Loading Sync Embeds Plugin'); - // Add the command to insert a sync block. this.addCommand({ id: 'insert-synced-embed', name: 'Insert synced embed', editorCallback: (editor, view) => { const selection = editor.getSelection(); - // If the user has selected text, use it as the note name. - const noteName = selection || ' '; + const noteName = selection || 'Note Name'; const textToInsert = `\`\`\`sync\n![[${noteName}]]\n\`\`\``; editor.replaceSelection(textToInsert); } }); - // Register the main code block processor. this.registerMarkdownCodeBlockProcessor('sync', (source, el, ctx) => { this.processSyncBlock(source, el, ctx); }); @@ -30,29 +27,30 @@ module.exports = class SyncEmbedPlugin extends Plugin { async processSyncBlock(source, el, ctx) { el.empty(); const syncContainer = el.createDiv('sync-container'); - - const embedLines = source.split('\n') - .map(line => line.trim()) - .filter(line => line.startsWith('![[') && line.endsWith(']]')); - + const embedLines = source.split('\n').map(line => line.trim()).filter(line => line.startsWith('![[') && line.endsWith(']]')); if (embedLines.length === 0) { syncContainer.createDiv('sync-empty').setText('No embeds found in sync block'); return; } - for (let i = 0; i < embedLines.length; i++) { - const embedLine = embedLines[i]; - await this.processEmbed(embedLine, syncContainer, ctx, i > 0); + await this.processEmbed(embedLines[i], syncContainer, ctx, i > 0); } } - async processEmbed(embedLine, container, ctx, addGap = false) { + async processEmbed(embedLine, container, ctx, addGap) { try { const match = embedLine.match(/!\[\[([^\]]+)\]\]/); if (!match) return; const linkText = match[1]; - const notePath = linkText.split('#')[0]; + let notePath = linkText; + let section = null; + + if (linkText.includes('#')) { + const parts = linkText.split('#'); + notePath = parts[0]; + section = parts.slice(1).join('#'); + } const file = this.app.metadataCache.getFirstLinkpathDest(notePath, ctx.sourcePath); if (!file) { @@ -65,16 +63,10 @@ module.exports = class SyncEmbedPlugin extends Plugin { const component = new Component(); const leaf = new WorkspaceLeaf(this.app); - component.load(); - component.addChild(new (class extends Component { - async onunload() { - leaf.detach(); - } - })()); + component.addChild(new (class extends Component { async onunload() { leaf.detach(); } })()); await leaf.openFile(file, { state: { mode: "source" } }); - const view = leaf.view; if (!(view instanceof MarkdownView)) { this.renderError(embedContainer, 'Failed to load a markdown view.', addGap); @@ -82,9 +74,39 @@ module.exports = class SyncEmbedPlugin extends Plugin { return; } + // --- SECTION LOGIC --- + if (section) { + let fullContent = await this.app.vault.read(file); + const sectionContent = this.extractSection(fullContent, section); + view.editor.setValue(sectionContent); + + // --- THE FIX --- + // Disconnect the view from the file to prevent Obsidian's native + // auto-save from overwriting the entire note with just the section. + view.file = null; + // --------------- + + const debouncedSave = debounce(async () => { + const newSectionText = view.editor.getValue(); + // We must re-read the file right before saving to get the latest version. + const currentFullContent = await this.app.vault.read(file); + const newFullContent = this.replaceSection(currentFullContent, section, newSectionText); + + if (newFullContent !== currentFullContent) { + await this.app.vault.modify(file, newFullContent); + } + }, 500, true); + + // Register the save function on editor changes for this specific view + component.registerEvent(this.app.workspace.on('editor-change', (editor) => { + if (editor === view.editor) { + debouncedSave(); + } + })); + } + embedContainer.appendChild(view.containerEl); embedContainer.style.height = 'auto'; - ctx.addChild(component); } catch (error) { @@ -98,4 +120,74 @@ module.exports = class SyncEmbedPlugin extends Plugin { if (addGap) errorDiv.addClass('sync-embed-gap'); errorDiv.setText(message); } + + escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + extractSection(content, sectionName) { + const lines = content.split('\n'); + let inSection = false; + let sectionLines = []; + const headerRegex = new RegExp(`^#{1,6}\\s+${this.escapeRegExp(sectionName)}\\s*$`); + let sectionLevel = 0; + + for (const line of lines) { + if (headerRegex.test(line)) { + inSection = true; + sectionLevel = (line.match(/^#+/)?.[0] || '').length; + sectionLines.push(line); + continue; + } + + if (inSection) { + const currentLevelMatch = line.match(/^#+/); + if (currentLevelMatch) { + const currentLevel = currentLevelMatch[0].length; + if (currentLevel <= sectionLevel) { + break; + } + } + sectionLines.push(line); + } + } + return inSection ? sectionLines.join('\n') : `# ${sectionName}\n\n*Section not found.*`; + } + + replaceSection(fullContent, sectionName, newSectionText) { + const lines = fullContent.split('\n'); + const headerRegex = new RegExp(`^#{1,6}\\s+${this.escapeRegExp(sectionName)}\\s*$`); + let sectionLevel = 0; + let startIdx = -1; + let endIdx = lines.length; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (startIdx === -1 && headerRegex.test(line)) { + startIdx = i; + sectionLevel = (line.match(/^#+/)?.[0] || '').length; + continue; + } + + if (startIdx !== -1) { + const currentLevelMatch = line.match(/^#+/); + if (currentLevelMatch) { + const currentLevel = currentLevelMatch[0].length; + if (currentLevel <= sectionLevel) { + endIdx = i; + break; + } + } + } + } + + if (startIdx === -1) { + return `${fullContent}\n\n${newSectionText}`; + } + + const before = lines.slice(0, startIdx); + const after = lines.slice(endIdx); + const newSectionLines = newSectionText.split('\n'); + return [...before, ...newSectionLines, ...after].join('\n'); + } }; \ No newline at end of file diff --git a/manifest.json b/manifest.json index d322e56..d25912b 100644 --- a/manifest.json +++ b/manifest.json @@ -1,10 +1,10 @@ { "id": "sync-embeds", "name": "Sync Embeds", - "version": "1.0.0", + "version": "1.1.0", "minAppVersion": "0.15.0", "description": "Create fully editable, live-synced note embeds.", "author": "uthvah", - "authorUrl": "https://github.com/uthvah", + "authorUrl": "", "isDesktopOnly": false } \ No newline at end of file diff --git a/styles.css b/styles.css index 75cc77e..2a583bd 100644 --- a/styles.css +++ b/styles.css @@ -1,4 +1,11 @@ -/* Main container for the sync block */ +/* + * styles.css for Sync Embeds Plugin + * + * This file handles the visual presentation of the synced embed blocks, + * aiming for a seamless and native look and feel. + */ + +/* Main container for a ```sync``` code block */ .sync-container { border: 1px solid var(--background-modifier-border); border-radius: 8px; @@ -7,11 +14,12 @@ margin: 12px 0; display: flex; flex-direction: column; - gap: 16px; + gap: 16px; /* Provides consistent spacing between multiple embeds */ } -/* Individual embed container */ +/* Wrapper for a single `![[...]]` embed inside the container */ .sync-embed { + /* Reset any default styling that might interfere */ border: none !important; margin: 0 !important; padding: 0 !important; @@ -19,14 +27,15 @@ box-shadow: none !important; } -/* --- SEAMLESS STYLING FOR EMBEDDED EDITOR --- */ +/* --- SEAMLESS STYLING FOR THE EMBEDDED EDITOR --- */ +/* This is the most critical section for the "native" feel. */ -/* Hide the view's header (title bar, icons) completely */ +/* Hide the view's header (which contains the title, icons, etc.) */ .sync-embed .view-header { display: none !important; } -/* Make the main content area and editor background transparent */ +/* Make the main content area and editor background transparent to blend in. */ .sync-embed .view-content, .sync-embed .markdown-source-view, .sync-embed .cm-editor, @@ -36,38 +45,36 @@ margin: 0 !important; } -/* Ensure the editor itself takes up the full width */ +/* Ensure the editor itself takes up the full width of its container */ .sync-embed .markdown-source-view.mod-cm6 .cm-editor { - width: 100%; + width: 100%; } -/* Remove padding from the view content area */ +/* + * Remove the default padding from the view content area. + * This aligns the text inside the embed with the text of the parent note. + */ .sync-embed .view-content { padding: 0 var(--file-margins) !important; } -/* Remove extra vertical padding from CodeMirror */ +/* Adjust vertical padding inside the editor */ .sync-embed .cm-s-obsidian .cm-gutters, .sync-embed .cm-s-obsidian .cm-content { - padding-top: 0 !important; - padding-bottom: 0 !important; + padding-top: 0 !important; + /* --- NEW --- Add a space at the bottom of the embed for better visual separation */ + padding-bottom: 1em !important; } -/* Optional: remove line numbers */ -.sync-embed .cm-gutters { - display: none; -} -.sync-embed .cm-content { - padding-left: 0 !important; -} - -/* --- UTILITY & ERROR STYLING --- */ +/* --- UTILITY & ERROR STATE STYLING --- */ +/* Provides a visual separator line for multiple embeds if the 'gap' isn't enough */ .sync-embed-gap { padding-top: 16px; border-top: 1px solid var(--background-modifier-border-hover); } +/* Style for when a sync block is empty */ .sync-empty { padding: 20px; text-align: center; @@ -75,6 +82,7 @@ font-style: italic; } +/* Style for when an embed fails to load (e.g., note not found) */ .sync-embed-error { padding: 8px 12px; background: var(--background-modifier-error);