This commit is contained in:
thecodexapp 2025-09-18 22:28:14 +01:00
parent 51ec340a75
commit bc0574d263
3 changed files with 146 additions and 46 deletions

138
main.js
View file

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

View file

@ -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
}

View file

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