Bug fixes

This commit is contained in:
thecodexapp 2025-09-22 13:26:55 +01:00
parent 5a7bbd0269
commit b5d810e686
2 changed files with 174 additions and 80 deletions

207
main.js
View file

@ -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');
}
};

View file

@ -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);