mirror of
https://github.com/uthvah/sync-embeds.git
synced 2026-07-22 16:30:22 +00:00
Keyboard shortcuts
This commit is contained in:
parent
b5d810e686
commit
3d2eccd5fa
2 changed files with 397 additions and 155 deletions
548
main.js
548
main.js
|
|
@ -1,8 +1,22 @@
|
|||
const { Plugin, MarkdownView, WorkspaceLeaf, Component, debounce } = require('obsidian');
|
||||
|
||||
module.exports = class SyncEmbedPlugin extends Plugin {
|
||||
constructor(app, manifest) {
|
||||
super(app, manifest);
|
||||
this.embedRegistry = new Map(); // Maps container elements to embed data
|
||||
this.originalExecuteCommand = null;
|
||||
this.originalGetActiveViewOfType = null;
|
||||
this.commandInterceptors = new Map(); // Maps command IDs to our handlers
|
||||
this.currentFocusedEmbed = null;
|
||||
}
|
||||
|
||||
async onload() {
|
||||
console.log('Loading Sync Embeds Plugin');
|
||||
console.log('Loading Sync Embeds Plugin - Command Interception Active');
|
||||
|
||||
this.hijackObsidianCommands();
|
||||
|
||||
// Setup command interceptors for common shortcuts
|
||||
this.setupCommandInterceptors();
|
||||
|
||||
this.addCommand({
|
||||
id: 'insert-synced-embed',
|
||||
|
|
@ -18,12 +32,268 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
this.registerMarkdownCodeBlockProcessor('sync', (source, el, ctx) => {
|
||||
this.processSyncBlock(source, el, ctx);
|
||||
});
|
||||
|
||||
// Global event listeners to track focus
|
||||
this.registerDomEvent(document, 'focusin', this.trackFocus.bind(this));
|
||||
this.registerDomEvent(document, 'focusout', this.trackFocusLoss.bind(this));
|
||||
}
|
||||
|
||||
onunload() {
|
||||
console.log('Unloading Sync Embeds Plugin');
|
||||
this.restoreObsidianCommands();
|
||||
this.embedRegistry.clear();
|
||||
this.currentFocusedEmbed = null;
|
||||
}
|
||||
|
||||
// HIJACK OBSIDIAN'S COMMAND SYSTEM
|
||||
hijackObsidianCommands() {
|
||||
this.originalExecuteCommand = this.app.commands.executeCommand;
|
||||
this.originalGetActiveViewOfType = this.app.workspace.getActiveViewOfType;
|
||||
|
||||
// Override executeCommand to route to our embeds when appropriate
|
||||
this.app.commands.executeCommand = (command, ...args) => {
|
||||
const focusedEmbed = this.getFocusedEmbed();
|
||||
|
||||
if (focusedEmbed && this.commandInterceptors.has(command.id)) {
|
||||
// Route to our custom handler
|
||||
const handler = this.commandInterceptors.get(command.id);
|
||||
return handler.call(this, focusedEmbed, ...args);
|
||||
}
|
||||
|
||||
// Otherwise, use the default Obsidian behavior
|
||||
return this.originalExecuteCommand.call(this.app.commands, command, ...args);
|
||||
};
|
||||
|
||||
// Override getActiveViewOfType to make Obsidian think our embed is the active view
|
||||
this.app.workspace.getActiveViewOfType = (type) => {
|
||||
const focusedEmbed = this.getFocusedEmbed();
|
||||
if (focusedEmbed && focusedEmbed.view instanceof type) {
|
||||
return focusedEmbed.view;
|
||||
}
|
||||
return this.originalGetActiveViewOfType.call(this.app.workspace, type);
|
||||
};
|
||||
}
|
||||
|
||||
restoreObsidianCommands() {
|
||||
if (this.originalExecuteCommand) {
|
||||
this.app.commands.executeCommand = this.originalExecuteCommand;
|
||||
}
|
||||
if (this.originalGetActiveViewOfType) {
|
||||
this.app.workspace.getActiveViewOfType = this.originalGetActiveViewOfType;
|
||||
}
|
||||
}
|
||||
|
||||
// SETUP COMMAND INTERCEPTORS
|
||||
setupCommandInterceptors() {
|
||||
const commandMappings = {
|
||||
'editor:toggle-checklist-status': this.toggleChecklistCommand,
|
||||
'editor:toggle-bold': this.toggleBoldCommand,
|
||||
'editor:toggle-italics': this.toggleItalicCommand,
|
||||
'editor:toggle-strikethrough': this.toggleStrikethroughCommand,
|
||||
'editor:toggle-code': this.toggleCodeCommand,
|
||||
'editor:insert-link': this.insertLinkCommand,
|
||||
'editor:toggle-bullet-list': this.toggleBulletListCommand,
|
||||
'editor:toggle-numbered-list': this.toggleNumberedListCommand,
|
||||
'editor:indent-list': this.indentListCommand,
|
||||
'editor:unindent-list': this.unindentListCommand,
|
||||
'editor:insert-tag': this.insertTagCommand,
|
||||
'editor:swap-line-up': this.swapLineUpCommand,
|
||||
'editor:swap-line-down': this.swapLineDownCommand,
|
||||
'editor:duplicate-line': this.duplicateLineCommand,
|
||||
'editor:delete-line': this.deleteLineCommand
|
||||
};
|
||||
|
||||
for (const [commandId, handler] of Object.entries(commandMappings)) {
|
||||
this.commandInterceptors.set(commandId, handler);
|
||||
}
|
||||
}
|
||||
|
||||
// FOCUS TRACKING METHODS
|
||||
trackFocus(event) {
|
||||
const embed = this.getEmbedFromElement(event.target);
|
||||
if (embed) {
|
||||
this.currentFocusedEmbed = embed;
|
||||
}
|
||||
}
|
||||
|
||||
trackFocusLoss(event) {
|
||||
const embed = this.getEmbedFromElement(event.relatedTarget);
|
||||
if (!embed) {
|
||||
this.currentFocusedEmbed = null;
|
||||
}
|
||||
}
|
||||
|
||||
// UTILITY METHODS
|
||||
getFocusedEmbed() {
|
||||
return this.currentFocusedEmbed;
|
||||
}
|
||||
|
||||
getEmbedFromElement(element) {
|
||||
if (!element) return null;
|
||||
let current = element;
|
||||
while (current && current !== document.body) {
|
||||
if (current.classList && current.classList.contains('sync-embed')) {
|
||||
return this.embedRegistry.get(current);
|
||||
}
|
||||
current = current.parentElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// COMMAND IMPLEMENTATIONS
|
||||
toggleChecklistCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
if (line.match(/^\s*- \[ \]/)) {
|
||||
editor.replaceRange(line.replace(/- \[ \]/, '- [x]'), {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
} else if (line.match(/^\s*- \[x\]/i)) {
|
||||
editor.replaceRange(line.replace(/- \[x\]/i, '- [ ]'), {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
} else {
|
||||
const indent = line.match(/^\s*/)[0];
|
||||
const content = line.substring(indent.length);
|
||||
editor.replaceRange(`${indent}- [ ] ${content}`, {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Helper for toggling markdown formatting
|
||||
toggleMarkdownFormatting(embedData, markdownChar) {
|
||||
const { editor } = embedData;
|
||||
const selection = editor.getSelection();
|
||||
const len = markdownChar.length;
|
||||
|
||||
if (selection && selection.startsWith(markdownChar) && selection.endsWith(markdownChar)) {
|
||||
editor.replaceSelection(selection.slice(len, -len));
|
||||
} else if (selection) {
|
||||
editor.replaceSelection(`${markdownChar}${selection}${markdownChar}`);
|
||||
} else {
|
||||
const cursor = editor.getCursor();
|
||||
editor.replaceRange(markdownChar + markdownChar, cursor);
|
||||
editor.setCursor({ line: cursor.line, ch: cursor.ch + len });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleBoldCommand(embedData) { return this.toggleMarkdownFormatting(embedData, '**'); }
|
||||
toggleItalicCommand(embedData) { return this.toggleMarkdownFormatting(embedData, '*'); }
|
||||
toggleStrikethroughCommand(embedData) { return this.toggleMarkdownFormatting(embedData, '~~'); }
|
||||
toggleCodeCommand(embedData) { return this.toggleMarkdownFormatting(embedData, '`'); }
|
||||
|
||||
insertLinkCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const selection = editor.getSelection();
|
||||
if (selection) {
|
||||
editor.replaceSelection(`[[${selection}]]`);
|
||||
} else {
|
||||
const cursor = editor.getCursor();
|
||||
editor.replaceRange('[[]]', cursor);
|
||||
editor.setCursor({ line: cursor.line, ch: cursor.ch + 2 });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleBulletListCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
if (line.match(/^\s*- /)) {
|
||||
editor.replaceRange(line.replace(/^\s*- /, ''), {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
} else {
|
||||
editor.replaceRange(`- ${line}`, {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
toggleNumberedListCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
|
||||
if (line.match(/^\s*\d+\. /)) {
|
||||
editor.replaceRange(line.replace(/^\s*\d+\. /, ''), {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
} else {
|
||||
editor.replaceRange(`1. ${line}`, {line: cursor.line, ch: 0}, {line: cursor.line, ch: line.length});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
indentListCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
editor.replaceRange('\t', {line: cursor.line, ch: 0});
|
||||
return true;
|
||||
}
|
||||
|
||||
unindentListCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
if (line.startsWith('\t')) {
|
||||
editor.replaceRange('', {line: cursor.line, ch: 0}, {line: cursor.line, ch: 1});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
insertTagCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
editor.replaceSelection('#');
|
||||
return true;
|
||||
}
|
||||
|
||||
swapLineUpCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
if (cursor.line > 0) {
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
const prevLine = editor.getLine(cursor.line - 1);
|
||||
editor.transaction({
|
||||
changes: [
|
||||
{ from: {line: cursor.line - 1, ch: 0}, to: {line: cursor.line - 1, ch: prevLine.length}, text: currentLine },
|
||||
{ from: {line: cursor.line, ch: 0}, to: {line: cursor.line, ch: currentLine.length}, text: prevLine }
|
||||
],
|
||||
selection: { from: {line: cursor.line - 1, ch: cursor.ch} }
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
swapLineDownCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
if (cursor.line < editor.lastLine()) {
|
||||
const currentLine = editor.getLine(cursor.line);
|
||||
const nextLine = editor.getLine(cursor.line + 1);
|
||||
editor.transaction({
|
||||
changes: [
|
||||
{ from: {line: cursor.line, ch: 0}, to: {line: cursor.line, ch: currentLine.length}, text: nextLine },
|
||||
{ from: {line: cursor.line + 1, ch: 0}, to: {line: cursor.line + 1, ch: nextLine.length}, text: currentLine }
|
||||
],
|
||||
selection: { from: {line: cursor.line + 1, ch: cursor.ch} }
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
duplicateLineCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const cursor = editor.getCursor();
|
||||
const line = editor.getLine(cursor.line);
|
||||
editor.replaceRange(`\n${line}`, {line: cursor.line, ch: line.length});
|
||||
return true;
|
||||
}
|
||||
|
||||
deleteLineCommand(embedData) {
|
||||
const { editor } = embedData;
|
||||
const { line } = editor.getCursor();
|
||||
editor.replaceRange('', {line: line, ch: 0}, {line: line + 1, ch: 0});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// MAIN PROCESSING METHODS
|
||||
async processSyncBlock(source, el, ctx) {
|
||||
el.empty();
|
||||
const syncContainer = el.createDiv('sync-container');
|
||||
|
|
@ -49,21 +319,11 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
let notePath = linkPath.split('#')[0];
|
||||
const section = linkPath.includes('#') ? linkPath.substring(linkPath.indexOf('#') + 1) : null;
|
||||
|
||||
if (!notePath) {
|
||||
notePath = ctx.sourcePath;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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');
|
||||
|
|
@ -71,7 +331,21 @@ 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 {
|
||||
constructor(plugin, containerEl) {
|
||||
super();
|
||||
this.plugin = plugin;
|
||||
this.containerEl = containerEl;
|
||||
}
|
||||
async onunload() {
|
||||
this.plugin.embedRegistry.delete(this.containerEl);
|
||||
if (this.plugin.currentFocusedEmbed && this.plugin.currentFocusedEmbed.containerEl === this.containerEl) {
|
||||
this.plugin.currentFocusedEmbed = null;
|
||||
}
|
||||
leaf.detach();
|
||||
}
|
||||
})(this, embedContainer));
|
||||
|
||||
await leaf.openFile(file, { state: { mode: "source" } });
|
||||
const view = leaf.view;
|
||||
|
|
@ -82,113 +356,11 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
return;
|
||||
}
|
||||
|
||||
const embedData = { view, editor: view.editor, containerEl: embedContainer, file, section, component };
|
||||
this.embedRegistry.set(embedContainer, embedData);
|
||||
|
||||
if (section) {
|
||||
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 () => {
|
||||
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 (newFileContent !== currentFileContent) {
|
||||
await this.app.vault.modify(file, newFileContent);
|
||||
}
|
||||
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 || 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();
|
||||
}));
|
||||
await this.setupSectionEmbed(embedData);
|
||||
}
|
||||
|
||||
embedContainer.appendChild(view.containerEl);
|
||||
|
|
@ -201,6 +373,93 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
}
|
||||
}
|
||||
|
||||
async setupSectionEmbed(embedData) {
|
||||
const { view, file, section, component } = embedData;
|
||||
|
||||
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 () => {
|
||||
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 (newFileContent !== currentFileContent) {
|
||||
await this.app.vault.modify(file, newFileContent);
|
||||
}
|
||||
isSaving = false;
|
||||
}, 750, true);
|
||||
|
||||
component.registerEvent(this.app.vault.on('modify', async (modifiedFile) => {
|
||||
if (modifiedFile.path === file.path && !isSaving) await loadSection();
|
||||
}));
|
||||
|
||||
let lastCorrectedLineNumber = -1;
|
||||
component.registerEvent(this.app.workspace.on('editor-change', (editor) => {
|
||||
if (editor !== view.editor || isProgrammaticUpdate) return;
|
||||
const cursorPos = editor.getCursor();
|
||||
const line = editor.getLine(cursorPos.line);
|
||||
const currentLineNumber = cursorPos.line;
|
||||
|
||||
if (currentLineNumber === 0 && line !== originalHeader) {
|
||||
isProgrammaticUpdate = true;
|
||||
editor.replaceRange(originalHeader, { line: 0, ch: 0 }, { line: 0, ch: line.length });
|
||||
isProgrammaticUpdate = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentLineNumber > 0 && line.trim().startsWith('#')) {
|
||||
const currentHashes = (line.match(/^#+/) || [''])[0];
|
||||
const currentLevel = currentHashes.length;
|
||||
const requiredLevel = headerLevel + 1;
|
||||
const requiredHashes = '#'.repeat(requiredLevel);
|
||||
|
||||
if (currentLineNumber === lastCorrectedLineNumber && currentLevel < requiredLevel) {
|
||||
isProgrammaticUpdate = true;
|
||||
editor.replaceRange("", { line: currentLineNumber, ch: 0 }, { line: currentLineNumber, ch: currentLevel });
|
||||
isProgrammaticUpdate = false;
|
||||
lastCorrectedLineNumber = -1;
|
||||
debouncedSave();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentLevel <= headerLevel) {
|
||||
isProgrammaticUpdate = true;
|
||||
editor.replaceRange(requiredHashes, { line: currentLineNumber, ch: 0 }, { line: currentLineNumber, ch: currentLevel });
|
||||
lastCorrectedLineNumber = currentLineNumber;
|
||||
isProgrammaticUpdate = false;
|
||||
} else { lastCorrectedLineNumber = -1; }
|
||||
} else { lastCorrectedLineNumber = -1; }
|
||||
|
||||
debouncedSave();
|
||||
}));
|
||||
}
|
||||
|
||||
// UTILITY AND SECTION HELPERS
|
||||
renderError(container, message, addGap) {
|
||||
const errorDiv = container.createDiv('sync-embed-error');
|
||||
if (addGap) errorDiv.addClass('sync-embed-gap');
|
||||
|
|
@ -214,8 +473,7 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
extractSection(content, sectionName) {
|
||||
const lines = content.split('\n');
|
||||
const headerRegex = new RegExp(`^#{1,6}\\s+${this.escapeRegExp(sectionName)}\\s*$`);
|
||||
let startIdx = -1;
|
||||
let sectionLevel = 0;
|
||||
let startIdx = -1, sectionLevel = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (headerRegex.test(lines[i])) {
|
||||
|
|
@ -224,31 +482,23 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIdx === -1) {
|
||||
return `# ${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;
|
||||
}
|
||||
const match = lines[i].match(/^#+/);
|
||||
if (match && match[0].length <= 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 startIdx = -1;
|
||||
let sectionLevel = 0;
|
||||
let startIdx = -1, sectionLevel = 0;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
if (headerRegex.test(lines[i])) {
|
||||
|
|
@ -257,27 +507,19 @@ module.exports = class SyncEmbedPlugin extends Plugin {
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (startIdx === -1) {
|
||||
return `${fullContent.trim()}\n\n${newSectionText}`.trim();
|
||||
}
|
||||
|
||||
if (startIdx === -1) 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 match = lines[i].match(/^#+/);
|
||||
if (match && match[0].length <= 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');
|
||||
return [...before, ...newSectionText.split('\n'), ...after].join('\n');
|
||||
}
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"id": "sync-embeds",
|
||||
"name": "Sync Embeds",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.2",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "Create fully editable, live-synced note embeds.",
|
||||
"author": "uthvah",
|
||||
"authorUrl": "",
|
||||
"authorUrl": "https://github.com/uthvah",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
Loading…
Reference in a new issue