From b5d810e686510940feeb5b44d357d45a57810cfb Mon Sep 17 00:00:00 2001 From: thecodexapp <113909005+uthvah@users.noreply.github.com> Date: Mon, 22 Sep 2025 13:26:55 +0100 Subject: [PATCH] Bug fixes --- main.js | 207 +++++++++++++++++++++++++++++++++++++++-------------- styles.css | 47 ++++++------ 2 files changed, 174 insertions(+), 80 deletions(-) diff --git a/main.js b/main.js index c5f9740..f8eb8db 100644 --- a/main.js +++ b/main.js @@ -28,10 +28,12 @@ module.exports = class SyncEmbedPlugin extends Plugin { el.empty(); const syncContainer = el.createDiv('sync-container'); 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++) { await this.processEmbed(embedLines[i], syncContainer, ctx, i > 0); } @@ -43,21 +45,26 @@ module.exports = class SyncEmbedPlugin extends Plugin { if (!match) return; const linkText = match[1]; - let notePath = linkText; - let section = null; + const linkPath = linkText.split('|')[0].trim(); + let notePath = linkPath.split('#')[0]; + const section = linkPath.includes('#') ? linkPath.substring(linkPath.indexOf('#') + 1) : null; - if (linkText.includes('#')) { - const parts = linkText.split('#'); - notePath = parts[0]; - section = parts.slice(1).join('#'); + if (!notePath) { + notePath = ctx.sourcePath; } const file = this.app.metadataCache.getFirstLinkpathDest(notePath, ctx.sourcePath); + if (!file) { this.renderError(container, `Note not found: ${notePath}`, addGap); return; } + if (file.path === ctx.sourcePath) { + this.renderError(container, "Cannot create a recursive embed of the same note.", addGap); + return; + } + const embedContainer = container.createDiv('sync-embed'); if (addGap) embedContainer.addClass('sync-embed-gap'); @@ -68,6 +75,7 @@ module.exports = class SyncEmbedPlugin extends Plugin { 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); leaf.detach(); @@ -75,26 +83,111 @@ module.exports = class SyncEmbedPlugin extends Plugin { } if (section) { - let fullContent = await this.app.vault.read(file); - const sectionContent = this.extractSection(fullContent, section); - view.editor.setValue(sectionContent); + let isProgrammaticUpdate = false; + let isSaving = false; + let originalHeader = ""; + let headerLevel = 0; + const updateHeaderState = (content) => { + originalHeader = content.split('\n')[0] || ''; + headerLevel = (originalHeader.match(/^#+/)?.[0] || '#').length; + }; + + const loadSection = async () => { + isProgrammaticUpdate = true; + const fileContent = await this.app.vault.read(file); + const currentSectionContent = this.extractSection(fileContent, section); + + updateHeaderState(currentSectionContent); + + const cursor = view.editor.getCursor(); + view.editor.setValue(currentSectionContent); + if (view.editor.getDoc().lineCount() > cursor.line && + view.editor.getLine(cursor.line).length >= cursor.ch) { + view.editor.setCursor(cursor); + } + + setTimeout(() => isProgrammaticUpdate = false, 50); + }; + + await loadSection(); view.file = null; const debouncedSave = debounce(async () => { - const newSectionText = view.editor.getValue(); - const currentFullContent = await this.app.vault.read(file); - const newFullContent = this.replaceSection(currentFullContent, section, newSectionText); + if (isSaving) return; + isSaving = true; + const newEmbedContent = view.editor.getValue(); + const currentFileContent = await this.app.vault.read(file); + const newFileContent = this.replaceSection(currentFileContent, section, newEmbedContent); - if (newFullContent !== currentFullContent) { - await this.app.vault.modify(file, newFullContent); + if (newFileContent !== currentFileContent) { + await this.app.vault.modify(file, newFileContent); } - }, 500, true); + isSaving = false; + }, 750, true); + + component.registerEvent(this.app.vault.on('modify', async (modifiedFile) => { + if (modifiedFile.path === file.path && !isSaving) { + await loadSection(); + } + })); + + // --- REFINED HEADER LOGIC WITH INTUITIVE BACKSPACE --- + let lastCorrectedLineNumber = -1; // Track which line we just auto-corrected component.registerEvent(this.app.workspace.on('editor-change', (editor) => { - if (editor === view.editor) { - debouncedSave(); + if (editor !== view.editor || isProgrammaticUpdate) { + return; } + + const cursorPos = editor.getCursor(); + const line = editor.getLine(cursorPos.line); + const currentLineNumber = cursorPos.line; + + // Rule 1: The main section header is not editable. + if (currentLineNumber === 0 && line !== originalHeader) { + isProgrammaticUpdate = true; + editor.replaceRange(originalHeader, { line: 0, ch: 0 }, { line: 0, ch: line.length }); + lastCorrectedLineNumber = -1; + isProgrammaticUpdate = false; + return; + } + + // Rule 2: Enforce sub-headers and handle cancellation. + if (currentLineNumber > 0 && line.trim().startsWith('#')) { + const currentHashes = (line.match(/^#+/) || [''])[0]; + const currentLevel = currentHashes.length; + const requiredLevel = headerLevel + 1; + const requiredHashes = '#'.repeat(requiredLevel); + + // --- Intuitive Backspace Cancellation --- + // If the user just backspaced on a line we *just* auto-corrected, + // interpret it as "cancel" and remove the hashes entirely. + if (currentLineNumber === lastCorrectedLineNumber && currentLevel < requiredLevel) { + isProgrammaticUpdate = true; + editor.replaceRange("", { line: currentLineNumber, ch: 0 }, { line: currentLineNumber, ch: currentLevel }); + isProgrammaticUpdate = false; + lastCorrectedLineNumber = -1; // Reset tracker + debouncedSave(); + return; + } + + // --- Standard Sub-Header Enforcement --- + if (currentLevel <= headerLevel) { + isProgrammaticUpdate = true; + editor.replaceRange(requiredHashes, { line: currentLineNumber, ch: 0 }, { line: currentLineNumber, ch: currentLevel }); + lastCorrectedLineNumber = currentLineNumber; // Track that we made a correction + isProgrammaticUpdate = false; + } else { + // If the line is valid, reset the tracker + lastCorrectedLineNumber = -1; + } + } else { + // If the line is no longer a header, reset the tracker + lastCorrectedLineNumber = -1; + } + + debouncedSave(); })); } @@ -120,67 +213,71 @@ module.exports = class SyncEmbedPlugin extends Plugin { 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 startIdx = -1; 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); + for (let i = 0; i < lines.length; i++) { + if (headerRegex.test(lines[i])) { + startIdx = i; + sectionLevel = (lines[i].match(/^#+/)?.[0] || '').length; + break; } } - return inSection ? sectionLines.join('\n') : `# ${sectionName}\n\n*Section not found.*`; + + if (startIdx === -1) { + return `# ${sectionName}\n\n*Section not found.*`; + } + + let endIdx = lines.length; + for (let i = startIdx + 1; i < lines.length; i++) { + const currentLevelMatch = lines[i].match(/^#+/); + if (currentLevelMatch) { + const currentLevel = currentLevelMatch[0].length; + if (currentLevel <= sectionLevel) { + endIdx = i; + break; + } + } + } + + return lines.slice(startIdx, endIdx).join('\n'); } 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; + let sectionLevel = 0; for (let i = 0; i < lines.length; i++) { - const line = lines[i]; - if (startIdx === -1 && headerRegex.test(line)) { + if (headerRegex.test(lines[i])) { 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; - } - } + sectionLevel = (lines[i].match(/^#+/)?.[0] || '').length; + break; } } if (startIdx === -1) { - return `${fullContent}\n\n${newSectionText}`; + return `${fullContent.trim()}\n\n${newSectionText}`.trim(); } + let endIdx = lines.length; + for (let i = startIdx + 1; i < lines.length; i++) { + const currentLevelMatch = lines[i].match(/^#+/); + if (currentLevelMatch) { + const currentLevel = currentLevelMatch[0].length; + if (currentLevel <= sectionLevel) { + endIdx = i; + break; + } + } + } + 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/styles.css b/styles.css index 2a583bd..850392f 100644 --- a/styles.css +++ b/styles.css @@ -1,10 +1,3 @@ -/* - * 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); @@ -14,12 +7,11 @@ margin: 12px 0; display: flex; flex-direction: column; - gap: 16px; /* Provides consistent spacing between multiple embeds */ + gap: 16px; } /* 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; @@ -28,14 +20,10 @@ } /* --- SEAMLESS STYLING FOR THE EMBEDDED EDITOR --- */ -/* This is the most critical section for the "native" feel. */ - -/* 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 to blend in. */ .sync-embed .view-content, .sync-embed .markdown-source-view, .sync-embed .cm-editor, @@ -45,36 +33,46 @@ margin: 0 !important; } -/* Ensure the editor itself takes up the full width of its container */ .sync-embed .markdown-source-view.mod-cm6 .cm-editor { width: 100%; } -/* - * 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; } -/* Adjust vertical padding inside the editor */ .sync-embed .cm-s-obsidian .cm-gutters, .sync-embed .cm-s-obsidian .cm-content { padding-top: 0 !important; - /* --- NEW --- Add a space at the bottom of the embed for better visual separation */ - padding-bottom: 1em !important; + padding-bottom: 1em !important; +} + +/* --- STAGE 2 FIXES --- */ +/* + * Make the inline title visible and not greyed out. + */ +.sync-embed .inline-title { + pointer-events: none; /* Disables all clicks, focus, and hover */ + user-select: none; /* Prevents the user from selecting the text */ + opacity: 1; /* Restores the default text color */ +} + +/* + * Make the header in a section embed uneditable. + */ +.uneditable-header { + pointer-events: none !important; + user-select: none !important; + opacity: 0.7 !important; + background-color: transparent !important; } /* --- 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; @@ -82,7 +80,6 @@ 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);