mirror of
https://github.com/uthvah/sync-embeds.git
synced 2026-07-22 06:43:40 +00:00
1708 lines
71 KiB
JavaScript
1708 lines
71 KiB
JavaScript
/*
|
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
|
if you want to view the source, please visit the github repository of this plugin
|
|
*/
|
|
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __commonJS = (cb, mod) => function __require() {
|
|
try {
|
|
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
} catch (e) {
|
|
throw mod = 0, e;
|
|
}
|
|
};
|
|
|
|
// node_modules/monkey-around/dist/index.cjs
|
|
var require_dist = __commonJS({
|
|
"node_modules/monkey-around/dist/index.cjs"(exports2) {
|
|
"use strict";
|
|
function around2(obj, factories) {
|
|
const removers = Object.keys(factories).map((key) => around1(obj, key, factories[key]));
|
|
return removers.length === 1 ? removers[0] : function() {
|
|
removers.forEach((r) => r());
|
|
};
|
|
}
|
|
function around1(obj, method, createWrapper) {
|
|
const inherited = obj[method], hadOwn = obj.hasOwnProperty(method), original = hadOwn ? inherited : function() {
|
|
return Object.getPrototypeOf(obj)[method].apply(this, arguments);
|
|
};
|
|
let current = createWrapper(original);
|
|
if (inherited)
|
|
Object.setPrototypeOf(current, inherited);
|
|
Object.setPrototypeOf(wrapper, current);
|
|
obj[method] = wrapper;
|
|
return remove;
|
|
function wrapper(...args) {
|
|
if (current === original && obj[method] === wrapper)
|
|
remove();
|
|
return current.apply(this, args);
|
|
}
|
|
function remove() {
|
|
if (obj[method] === wrapper) {
|
|
if (hadOwn)
|
|
obj[method] = original;
|
|
else
|
|
delete obj[method];
|
|
}
|
|
if (current === original)
|
|
return;
|
|
current = original;
|
|
Object.setPrototypeOf(wrapper, inherited || Function);
|
|
}
|
|
}
|
|
function dedupe(key, oldFn, newFn) {
|
|
check[key] = key;
|
|
return check;
|
|
function check(...args) {
|
|
return (oldFn[key] === key ? oldFn : newFn).apply(this, args);
|
|
}
|
|
}
|
|
function after(promise, cb) {
|
|
return promise.then(cb, cb);
|
|
}
|
|
function serialize(asyncFunction) {
|
|
let lastRun = Promise.resolve();
|
|
function wrapper(...args) {
|
|
return lastRun = new Promise((res, rej) => {
|
|
after(lastRun, () => {
|
|
asyncFunction.apply(this, args).then(res, rej);
|
|
});
|
|
});
|
|
}
|
|
wrapper.after = function() {
|
|
return lastRun = new Promise((res, rej) => {
|
|
after(lastRun, res);
|
|
});
|
|
};
|
|
return wrapper;
|
|
}
|
|
exports2.after = after;
|
|
exports2.around = around2;
|
|
exports2.dedupe = dedupe;
|
|
exports2.serialize = serialize;
|
|
}
|
|
});
|
|
|
|
// src/viewport-controller.js
|
|
var require_viewport_controller = __commonJS({
|
|
"src/viewport-controller.js"(exports2, module2) {
|
|
var { Notice } = require("obsidian");
|
|
var ViewportController = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
}
|
|
async setupSectionViewport(embedData) {
|
|
const { view, editor, file, section } = embedData;
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
const content = editor.getValue();
|
|
const sectionInfo = this.findSectionBounds(content, section);
|
|
if (sectionInfo.startLine === -1) {
|
|
console.warn("Sync Embeds: Section not found for viewport embedding:", section);
|
|
return;
|
|
}
|
|
embedData.sectionInfo = sectionInfo;
|
|
embedData.viewportActive = true;
|
|
this.applyViewportRestriction(embedData);
|
|
this.setupBoundaryProtection(embedData);
|
|
this.setupHeaderInputInterception(embedData);
|
|
this.setupContentConstraints(embedData);
|
|
this.scrollToSection(embedData);
|
|
}
|
|
applyViewportRestriction(embedData) {
|
|
const { view } = embedData;
|
|
const style = document.createElement("style");
|
|
style.className = "sync-viewport-style";
|
|
const embedId = "embed-" + Math.random().toString(36).substr(2, 9);
|
|
view.containerEl.setAttribute("data-embed-id", embedId);
|
|
embedData.embedId = embedId;
|
|
this.updateViewportCSS(embedData, style);
|
|
view.containerEl.appendChild(style);
|
|
embedData.viewportStyle = style;
|
|
}
|
|
updateViewportCSS(embedData, style) {
|
|
const { sectionInfo, embedId, file } = embedData;
|
|
const { startLine, endLine } = sectionInfo;
|
|
let domOffset = 0;
|
|
const fileCache = this.plugin.app.metadataCache.getFileCache(file);
|
|
if (fileCache && fileCache.frontmatterPosition) {
|
|
domOffset = fileCache.frontmatterPosition.end.line;
|
|
}
|
|
const domStartLine = Math.max(0, startLine - domOffset);
|
|
const domEndLine = Math.max(0, endLine - domOffset);
|
|
const css = `
|
|
/* Hide all lines BEFORE and INCLUDING the section header */
|
|
[data-embed-id="${embedId}"] .cm-line:nth-child(-n+${domStartLine + 1}) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Hide all lines AFTER the section */
|
|
[data-embed-id="${embedId}"] .cm-line:nth-child(n+${domEndLine + 1}) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Catch-all to prevent overlapping text in collapsed line numbers */
|
|
[data-embed-id="${embedId}"] .cm-gutterElement[style*="height: 0px"]:not([style*="visibility: hidden"]) {
|
|
display: none !important;
|
|
}
|
|
`;
|
|
style.textContent = css;
|
|
}
|
|
setupBoundaryProtection(embedData) {
|
|
const { view, editor, component } = embedData;
|
|
const setupHandlers = () => {
|
|
const cmEditor = view.containerEl.querySelector(".cm-content");
|
|
if (!cmEditor) {
|
|
setTimeout(setupHandlers, 50);
|
|
return;
|
|
}
|
|
const keydownHandler = (event) => {
|
|
var _a;
|
|
if (!embedData.viewportActive || !embedData.sectionInfo) return;
|
|
const { startLine, endLine } = embedData.sectionInfo;
|
|
const cursor = editor.getCursor();
|
|
const selection = editor.getSelection();
|
|
if (event.key === "Backspace") {
|
|
if (selection) {
|
|
const from = editor.getCursor("from");
|
|
if (from.line <= startLine) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
} else {
|
|
if (cursor.line === startLine + 1 && cursor.ch === 0) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (event.key === "Delete") {
|
|
if (selection) {
|
|
const to = editor.getCursor("to");
|
|
if (to.line >= endLine) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
} else {
|
|
const lastEditableLine = endLine - 1;
|
|
const lastLineLength = ((_a = editor.getLine(lastEditableLine)) == null ? void 0 : _a.length) || 0;
|
|
if (cursor.line === lastEditableLine && cursor.ch === lastLineLength) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
cmEditor.addEventListener("keydown", keydownHandler, true);
|
|
component.register(() => {
|
|
cmEditor.removeEventListener("keydown", keydownHandler, true);
|
|
});
|
|
};
|
|
setTimeout(setupHandlers, 100);
|
|
}
|
|
setupHeaderInputInterception(embedData) {
|
|
const { view, editor, component } = embedData;
|
|
const { headerLevel } = embedData.sectionInfo;
|
|
let lastNoticeTime = 0;
|
|
const noticeDebounce = 5e3;
|
|
const setupHandlers = () => {
|
|
const cmEditor = view.containerEl.querySelector(".cm-content");
|
|
if (!cmEditor) {
|
|
setTimeout(setupHandlers, 50);
|
|
return;
|
|
}
|
|
const inputHandler = (event) => {
|
|
if (event.inputType !== "insertText" && event.inputType !== "insertFromPaste") return;
|
|
if (event.data !== "#") return;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
const beforeHash = line.substring(0, cursor.ch - 1);
|
|
const isAtLineStart = /^\s*$/.test(beforeHash);
|
|
if (isAtLineStart && cursor.line > embedData.sectionInfo.startLine && cursor.line < embedData.sectionInfo.endLine) {
|
|
const currentLine = editor.getLine(cursor.line);
|
|
const newLine = currentLine.substring(0, cursor.ch - 1) + currentLine.substring(cursor.ch);
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: cursor.line, ch: 0 },
|
|
{ line: cursor.line, ch: currentLine.length }
|
|
);
|
|
editor.setCursor({ line: cursor.line, ch: cursor.ch - 1 });
|
|
const now = Date.now();
|
|
if (this.plugin.settings.showHeaderHints && now - lastNoticeTime > noticeDebounce) {
|
|
lastNoticeTime = now;
|
|
const availableLevels = [];
|
|
for (let i = headerLevel + 1; i <= 6; i++) {
|
|
availableLevels.push(`H${i} (Alt+${i})`);
|
|
}
|
|
new Notice(`\u26A0\uFE0F Cannot create H1-H${headerLevel} headers in this section.
|
|
Use: ${availableLevels.join(", ")}`, 5e3);
|
|
}
|
|
}
|
|
};
|
|
const pasteHandler = (event) => {
|
|
var _a;
|
|
const clipboardData = (_a = event.clipboardData) == null ? void 0 : _a.getData("text");
|
|
if (!clipboardData) return;
|
|
const cursor = editor.getCursor();
|
|
if (cursor.line > embedData.sectionInfo.startLine && cursor.line < embedData.sectionInfo.endLine) {
|
|
const lines = clipboardData.split("\n");
|
|
let hasInvalidHeaders = false;
|
|
const adjustedLines = lines.map((line) => {
|
|
const match = line.match(/^(#{1,6})\s+(.*)$/);
|
|
if (!match) return line;
|
|
const [, hashes, content] = match;
|
|
if (hashes.length <= headerLevel) {
|
|
hasInvalidHeaders = true;
|
|
return "#".repeat(headerLevel + 1) + " " + content;
|
|
}
|
|
return line;
|
|
});
|
|
if (hasInvalidHeaders) {
|
|
event.preventDefault();
|
|
if (this.plugin.settings.showHeaderHints) {
|
|
new Notice("Pasted headers adjusted to maintain section hierarchy", 4e3);
|
|
}
|
|
editor.replaceSelection(adjustedLines.join("\n"));
|
|
}
|
|
}
|
|
};
|
|
cmEditor.addEventListener("input", inputHandler, true);
|
|
cmEditor.addEventListener("paste", pasteHandler, true);
|
|
component.register(() => {
|
|
cmEditor.removeEventListener("input", inputHandler, true);
|
|
cmEditor.removeEventListener("paste", pasteHandler, true);
|
|
});
|
|
};
|
|
setTimeout(setupHandlers, 100);
|
|
}
|
|
setupContentConstraints(embedData) {
|
|
const { view, editor, component } = embedData;
|
|
let isProgrammaticUpdate = false;
|
|
const enforceCursorBounds = () => {
|
|
var _a;
|
|
if (isProgrammaticUpdate || !embedData.viewportActive || !embedData.sectionInfo) return;
|
|
const { startLine, endLine } = embedData.sectionInfo;
|
|
const cursor = editor.getCursor();
|
|
if (cursor.line <= startLine) {
|
|
isProgrammaticUpdate = true;
|
|
editor.setCursor({ line: startLine + 1, ch: 0 });
|
|
isProgrammaticUpdate = false;
|
|
} else if (cursor.line >= endLine) {
|
|
isProgrammaticUpdate = true;
|
|
const lastEditableLine = endLine - 1;
|
|
const lastLineLength = ((_a = editor.getLine(lastEditableLine)) == null ? void 0 : _a.length) || 0;
|
|
editor.setCursor({ line: lastEditableLine, ch: lastLineLength });
|
|
isProgrammaticUpdate = false;
|
|
}
|
|
};
|
|
const setupDOMListeners = () => {
|
|
const cmContent = view.containerEl.querySelector(".cm-content");
|
|
if (!cmContent) {
|
|
setTimeout(setupDOMListeners, 50);
|
|
return;
|
|
}
|
|
cmContent.addEventListener("mouseup", enforceCursorBounds);
|
|
cmContent.addEventListener("focusin", enforceCursorBounds);
|
|
cmContent.addEventListener("keyup", (e) => {
|
|
if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"].includes(e.key)) {
|
|
enforceCursorBounds();
|
|
}
|
|
});
|
|
component.register(() => {
|
|
cmContent.removeEventListener("mouseup", enforceCursorBounds);
|
|
cmContent.removeEventListener("focusin", enforceCursorBounds);
|
|
cmContent.removeEventListener("keyup", enforceCursorBounds);
|
|
});
|
|
};
|
|
setTimeout(setupDOMListeners, 100);
|
|
component.registerEvent(
|
|
this.plugin.app.workspace.on("editor-change", (changedEditor) => {
|
|
if (changedEditor === editor) {
|
|
this.updateViewportImmediately(embedData);
|
|
enforceCursorBounds();
|
|
}
|
|
})
|
|
);
|
|
const cmScroller = view.containerEl.querySelector(".cm-scroller");
|
|
if (cmScroller) {
|
|
const preventScroll = (e) => {
|
|
if (!embedData.viewportActive) return;
|
|
const scrollTop = cmScroller.scrollTop;
|
|
const lineHeight = editor.defaultTextHeight || 20;
|
|
const firstVisibleLine = Math.floor(scrollTop / lineHeight);
|
|
let domOffset = 0;
|
|
const fileCache = this.plugin.app.metadataCache.getFileCache(embedData.file);
|
|
if (fileCache && fileCache.frontmatterPosition) {
|
|
domOffset = fileCache.frontmatterPosition.end.line;
|
|
}
|
|
const domStartLine = Math.max(0, embedData.sectionInfo.startLine - domOffset);
|
|
const domEndLine = Math.max(0, embedData.sectionInfo.endLine - domOffset);
|
|
if (firstVisibleLine < Math.max(0, domStartLine - 2)) {
|
|
cmScroller.scrollTop = Math.max(0, domStartLine - 2) * lineHeight;
|
|
} else if (firstVisibleLine > domEndLine - 2) {
|
|
cmScroller.scrollTop = (domEndLine - 2) * lineHeight;
|
|
}
|
|
};
|
|
cmScroller.addEventListener("scroll", preventScroll);
|
|
component.register(() => {
|
|
cmScroller.removeEventListener("scroll", preventScroll);
|
|
});
|
|
}
|
|
}
|
|
updateViewportImmediately(embedData) {
|
|
if (!embedData.viewportActive) return;
|
|
const currentContent = embedData.editor.getValue();
|
|
const newSectionInfo = this.findSectionBounds(currentContent, embedData.section);
|
|
if (newSectionInfo.startLine !== -1) {
|
|
embedData.sectionInfo = newSectionInfo;
|
|
if (embedData.viewportStyle) {
|
|
this.updateViewportCSS(embedData, embedData.viewportStyle);
|
|
}
|
|
}
|
|
}
|
|
scrollToSection(embedData) {
|
|
const { editor, sectionInfo } = embedData;
|
|
const { startLine } = sectionInfo;
|
|
setTimeout(() => {
|
|
editor.scrollIntoView({ line: startLine + 1, ch: 0 }, true);
|
|
editor.setCursor({ line: startLine + 1, ch: 0 });
|
|
}, 150);
|
|
}
|
|
findSectionBounds(content, sectionName) {
|
|
var _a;
|
|
const lines = content.split("\n");
|
|
const escapedName = this.escapeRegExp(sectionName);
|
|
const headerRegex = new RegExp(`^#{1,6}\\s+${escapedName}\\s*$`);
|
|
let startLine = -1;
|
|
let headerLevel = 0;
|
|
for (let i = 0; i < lines.length; i++) {
|
|
if (headerRegex.test(lines[i])) {
|
|
startLine = i;
|
|
headerLevel = (((_a = lines[i].match(/^#+/)) == null ? void 0 : _a[0]) || "").length;
|
|
break;
|
|
}
|
|
}
|
|
if (startLine === -1) {
|
|
return { startLine: -1, endLine: -1, headerLevel: 0 };
|
|
}
|
|
let endLine = lines.length;
|
|
for (let i = startLine + 1; i < lines.length; i++) {
|
|
const match = lines[i].match(/^#+/);
|
|
if (match && match[0].length <= headerLevel) {
|
|
endLine = i;
|
|
break;
|
|
}
|
|
}
|
|
return { startLine, endLine, headerLevel };
|
|
}
|
|
escapeRegExp(string) {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
}
|
|
cleanupViewport(embedData) {
|
|
if (embedData.viewportStyle) {
|
|
embedData.viewportStyle.remove();
|
|
}
|
|
embedData.viewportActive = false;
|
|
}
|
|
};
|
|
module2.exports = ViewportController;
|
|
}
|
|
});
|
|
|
|
// src/dynamic-paths.js
|
|
var require_dynamic_paths = __commonJS({
|
|
"src/dynamic-paths.js"(exports2, module2) {
|
|
var { moment } = require("obsidian");
|
|
var DynamicPaths = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.pathCache = /* @__PURE__ */ new Map();
|
|
this.cacheCleanupInterval = setInterval(() => {
|
|
this.cleanupCache();
|
|
}, 6e4);
|
|
}
|
|
cleanup() {
|
|
if (this.cacheCleanupInterval) {
|
|
clearInterval(this.cacheCleanupInterval);
|
|
}
|
|
this.pathCache.clear();
|
|
}
|
|
cleanupCache() {
|
|
const now = Date.now();
|
|
const maxAge = 6e4;
|
|
for (const [key, cached] of this.pathCache.entries()) {
|
|
if (now - cached.timestamp > maxAge) {
|
|
this.pathCache.delete(key);
|
|
}
|
|
}
|
|
if (this.plugin.settings.debugMode) {
|
|
console.log("[Sync Embeds] Cache cleanup: removed stale entries, size now:", this.pathCache.size);
|
|
}
|
|
}
|
|
resolve(linkPath, ctx) {
|
|
let resolved = linkPath;
|
|
try {
|
|
resolved = this.resolveDateOffsetPatterns(resolved);
|
|
resolved = this.resolveDatePatterns(resolved);
|
|
resolved = this.resolveTimePatterns(resolved);
|
|
resolved = this.resolveTitlePattern(resolved, ctx);
|
|
if (this.plugin.settings.debugMode && resolved !== linkPath) {
|
|
console.log("[Sync Embeds] Dynamic path resolved:", linkPath, "\xE2\u2020\u2019", resolved);
|
|
}
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Error resolving dynamic path:", error);
|
|
return linkPath;
|
|
}
|
|
return resolved;
|
|
}
|
|
resolveDateOffsetPatterns(linkPath) {
|
|
const offsetPattern = /\{\{date([+-]\d+)([dwmy]):([^}]+)\}\}/g;
|
|
return linkPath.replace(offsetPattern, (match, offset, unit, format) => {
|
|
try {
|
|
const amount = parseInt(offset, 10);
|
|
const unitMap = {
|
|
"d": "days",
|
|
"w": "weeks",
|
|
"m": "months",
|
|
"y": "years"
|
|
};
|
|
if (!unitMap[unit]) {
|
|
console.error("Sync Embeds: Invalid date offset unit:", unit);
|
|
return match;
|
|
}
|
|
if (!this.isValidMomentFormat(format)) {
|
|
console.warn("Sync Embeds: Potentially invalid date format:", format);
|
|
}
|
|
const result = moment().add(amount, unitMap[unit]).format(format);
|
|
if (this.plugin.settings.debugMode) {
|
|
console.log("[Sync Embeds] Date offset resolved:", match, "\xE2\u2020\u2019", result);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Error in date offset pattern:", error);
|
|
return match;
|
|
}
|
|
});
|
|
}
|
|
resolveDatePatterns(linkPath) {
|
|
const datePattern = /\{\{date:([^}]+)\}\}/g;
|
|
return linkPath.replace(datePattern, (match, format) => {
|
|
try {
|
|
if (!this.isValidMomentFormat(format)) {
|
|
console.warn("Sync Embeds: Potentially invalid date format:", format);
|
|
}
|
|
const result = moment().format(format);
|
|
if (this.plugin.settings.debugMode) {
|
|
console.log("[Sync Embeds] Date resolved:", match, "\xE2\u2020\u2019", result);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Invalid date format:", format, error);
|
|
return match;
|
|
}
|
|
});
|
|
}
|
|
resolveTimePatterns(linkPath) {
|
|
const timePattern = /\{\{time:([^}]+)\}\}/g;
|
|
return linkPath.replace(timePattern, (match, format) => {
|
|
try {
|
|
if (!this.isValidMomentFormat(format)) {
|
|
console.warn("Sync Embeds: Potentially invalid time format:", format);
|
|
}
|
|
const result = moment().format(format);
|
|
if (this.plugin.settings.debugMode) {
|
|
console.log("[Sync Embeds] Time resolved:", match, "\xE2\u2020\u2019", result);
|
|
}
|
|
return result;
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Invalid time format:", format, error);
|
|
return match;
|
|
}
|
|
});
|
|
}
|
|
resolveTitlePattern(linkPath, ctx) {
|
|
const titlePattern = /\{\{title\}\}/g;
|
|
if (!ctx.sourcePath) {
|
|
return linkPath.replace(titlePattern, "");
|
|
}
|
|
try {
|
|
const currentFile = this.plugin.app.vault.getAbstractFileByPath(ctx.sourcePath);
|
|
const title = (currentFile == null ? void 0 : currentFile.basename) || "";
|
|
if (this.plugin.settings.debugMode && linkPath.includes("{{title}}")) {
|
|
console.log("[Sync Embeds] Title resolved:", "{{title}}", "\xE2\u2020\u2019", title);
|
|
}
|
|
return linkPath.replace(titlePattern, title);
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Error resolving title pattern:", error);
|
|
return linkPath.replace(titlePattern, "");
|
|
}
|
|
}
|
|
isValidMomentFormat(format) {
|
|
if (!format || format.trim() === "") return false;
|
|
const validTokens = /[YMDdHhmsSaAZzX]/;
|
|
return validTokens.test(format);
|
|
}
|
|
};
|
|
module2.exports = DynamicPaths;
|
|
}
|
|
});
|
|
|
|
// src/embed-manager.js
|
|
var require_embed_manager = __commonJS({
|
|
"src/embed-manager.js"(exports2, module2) {
|
|
var { Component, WorkspaceLeaf, MarkdownView: MarkdownView2, setIcon } = require("obsidian");
|
|
var ViewportController = require_viewport_controller();
|
|
var DynamicPaths = require_dynamic_paths();
|
|
var EmbedManager2 = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.embedRegistry = /* @__PURE__ */ new WeakMap();
|
|
this.activeEmbeds = /* @__PURE__ */ new Set();
|
|
this.viewportController = new ViewportController(plugin);
|
|
this.dynamicPaths = new DynamicPaths(plugin);
|
|
}
|
|
cleanup() {
|
|
this.activeEmbeds.forEach((embedData) => {
|
|
if (embedData.component) embedData.component.unload();
|
|
if (embedData.leaf) embedData.leaf.detach();
|
|
});
|
|
this.activeEmbeds.clear();
|
|
if (this.dynamicPaths) this.dynamicPaths.cleanup();
|
|
}
|
|
getEmbedFromElement(element) {
|
|
if (!element) return null;
|
|
let current = element;
|
|
while (current && current !== document.body) {
|
|
if (current.classList && current.classList.contains("sync-embed")) {
|
|
const embedData = this.embedRegistry.get(current);
|
|
if (embedData) return embedData;
|
|
}
|
|
current = current.parentElement;
|
|
}
|
|
return null;
|
|
}
|
|
async processSyncBlock(source, el, ctx) {
|
|
el.empty();
|
|
const syncContainer = el.createDiv("sync-container");
|
|
syncContainer.style.setProperty("--sync-embed-height", this.plugin.settings.embedHeight);
|
|
syncContainer.style.setProperty("--sync-max-height", this.plugin.settings.maxEmbedHeight);
|
|
syncContainer.style.setProperty("--sync-gap", this.plugin.settings.gapBetweenEmbeds);
|
|
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;
|
|
}
|
|
const estimatedHeight = embedLines.length * 200;
|
|
syncContainer.style.minHeight = `${estimatedHeight}px`;
|
|
for (let i = 0; i < embedLines.length; i++) {
|
|
await this.processEmbed(embedLines[i], syncContainer, ctx, i > 0);
|
|
}
|
|
setTimeout(() => {
|
|
syncContainer.style.minHeight = "";
|
|
}, 100);
|
|
}
|
|
parseEmbedOptions(line) {
|
|
const optionsMatch = line.match(/\{([^}]+)\}\]\]$/);
|
|
const options = {};
|
|
if (optionsMatch) {
|
|
const optionsStr = optionsMatch[1];
|
|
const pairs = optionsStr.split(",");
|
|
pairs.forEach((pair) => {
|
|
const [key, value] = pair.split(":").map((s) => s.trim());
|
|
if (key && value !== void 0) {
|
|
if (value === "true") options[key] = true;
|
|
else if (value === "false") options[key] = false;
|
|
else options[key] = value;
|
|
}
|
|
});
|
|
line = line.replace(/\{[^}]+\}\]\]$/, "]]");
|
|
}
|
|
return { line, options };
|
|
}
|
|
async processEmbed(embedLine, container, ctx, addGap) {
|
|
var _a;
|
|
try {
|
|
const { line: cleanedLine, options } = this.parseEmbedOptions(embedLine);
|
|
const match = cleanedLine.match(/!\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/);
|
|
if (!match) return;
|
|
let linkText = match[1];
|
|
let displayAlias = (_a = match[2]) == null ? void 0 : _a.trim();
|
|
const hasDynamicPattern = /\{\{(date|time|title)/.test(linkText);
|
|
if (hasDynamicPattern) {
|
|
const cacheKey = `${linkText}-${ctx.sourcePath}`;
|
|
const cached = this.dynamicPaths.pathCache.get(cacheKey);
|
|
const now = Date.now();
|
|
let resolvedText;
|
|
if (cached && now - cached.timestamp < 1e3) {
|
|
resolvedText = cached.value;
|
|
} else {
|
|
resolvedText = this.dynamicPaths.resolve(linkText, ctx);
|
|
this.dynamicPaths.pathCache.set(cacheKey, { value: resolvedText, timestamp: now });
|
|
}
|
|
if (!displayAlias) displayAlias = linkText;
|
|
linkText = resolvedText;
|
|
}
|
|
const linkPath = linkText.split("|")[0].trim();
|
|
let notePath = linkPath.split("#")[0];
|
|
const section = linkPath.includes("#") ? linkPath.substring(linkPath.indexOf("#") + 1) : null;
|
|
if (!notePath) notePath = ctx.sourcePath;
|
|
const file = this.plugin.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");
|
|
embedContainer.addClass("sync-embed-loading");
|
|
if (Object.keys(options).length > 0) {
|
|
embedContainer.dataset.customOptions = JSON.stringify(options);
|
|
}
|
|
const placeholderText = displayAlias || `${file.basename}${section ? "#" + section : ""}`;
|
|
const placeholder = embedContainer.createDiv("sync-embed-placeholder");
|
|
placeholder.setText(`Loading ${placeholderText}...`);
|
|
const renderAsCallout = options.callout !== void 0 ? options.callout : this.plugin.settings.renderAsCallout;
|
|
if (renderAsCallout) embedContainer.addClass("is-callout-style");
|
|
const observer = new IntersectionObserver((entries) => {
|
|
entries.forEach((entry) => {
|
|
if (entry.isIntersecting) {
|
|
observer.disconnect();
|
|
requestAnimationFrame(() => {
|
|
this.loadEmbed(embedContainer, file, section, displayAlias, ctx, placeholder, options);
|
|
});
|
|
}
|
|
});
|
|
}, {
|
|
rootMargin: this.plugin.settings.lazyLoadThreshold,
|
|
threshold: 0.01
|
|
});
|
|
observer.observe(embedContainer);
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Error processing embed:", error);
|
|
this.renderError(container, `Error loading: ${error.message}`, addGap);
|
|
}
|
|
}
|
|
async loadEmbed(embedContainer, file, section, alias, ctx, placeholder, customOptions = {}) {
|
|
try {
|
|
const component = new Component();
|
|
const leaf = new WorkspaceLeaf(this.plugin.app);
|
|
component.load();
|
|
const embedData = {
|
|
containerEl: embedContainer,
|
|
file,
|
|
section,
|
|
alias,
|
|
component,
|
|
leaf,
|
|
customOptions,
|
|
sourcePath: ctx.sourcePath
|
|
};
|
|
component.addChild(new class extends Component {
|
|
constructor(manager, data) {
|
|
super();
|
|
this.manager = manager;
|
|
this.embedData = data;
|
|
}
|
|
async onunload() {
|
|
var _a;
|
|
this.manager.activeEmbeds.delete(this.embedData);
|
|
if (((_a = this.manager.plugin.currentFocusedEmbed) == null ? void 0 : _a.containerEl) === this.embedData.containerEl) {
|
|
this.manager.plugin.currentFocusedEmbed = null;
|
|
}
|
|
if (this.embedData.leaf) this.embedData.leaf.detach();
|
|
}
|
|
}(this, embedData));
|
|
await leaf.openFile(file, { state: { mode: "source" } });
|
|
const view = leaf.view;
|
|
if (!(view instanceof MarkdownView2)) {
|
|
this.renderError(embedContainer.parentElement, "Failed to load a markdown view.", false);
|
|
leaf.detach();
|
|
return;
|
|
}
|
|
embedData.view = view;
|
|
embedData.editor = view.editor;
|
|
this.embedRegistry.set(embedContainer, embedData);
|
|
this.activeEmbeds.add(embedData);
|
|
if (customOptions.height) embedContainer.style.setProperty("--sync-embed-height", customOptions.height);
|
|
if (customOptions.maxHeight) embedContainer.style.setProperty("--sync-max-height", customOptions.maxHeight);
|
|
if (customOptions.collapse === true) embedContainer.addClass("is-collapsed");
|
|
const renderAsCallout = customOptions.callout !== void 0 ? customOptions.callout : this.plugin.settings.renderAsCallout;
|
|
const headerTitle = alias || (section ? `${file.basename} > ${section}` : file.basename);
|
|
if (section) {
|
|
const content = view.editor.getValue();
|
|
const sectionInfo = this.viewportController.findSectionBounds(content, section);
|
|
if (sectionInfo.startLine === -1) {
|
|
embedContainer.empty();
|
|
embedContainer.removeClass("sync-embed-loading");
|
|
embedContainer.style.height = "auto";
|
|
embedContainer.style.minHeight = "0";
|
|
this.renderError(embedContainer, `Section not found: ${section}`, false);
|
|
leaf.detach();
|
|
return;
|
|
}
|
|
await this.viewportController.setupSectionViewport(embedData);
|
|
}
|
|
const userWantsTitle = customOptions.title !== void 0 ? customOptions.title : this.plugin.settings.showInlineTitle;
|
|
if (renderAsCallout || userWantsTitle) {
|
|
this.setupHeaderUI(embedData, headerTitle, renderAsCallout, !!section);
|
|
}
|
|
if (section) {
|
|
this.hideProperties(embedData);
|
|
} else if (this.plugin.settings.collapsePropertiesByDefault) {
|
|
this.setupPropertiesCollapse(embedData);
|
|
}
|
|
placeholder.replaceWith(view.containerEl);
|
|
embedContainer.removeClass("sync-embed-loading");
|
|
ctx.addChild(component);
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Error loading embed:", error);
|
|
placeholder.setText(`Error: ${error.message}`);
|
|
placeholder.addClass("sync-embed-error");
|
|
}
|
|
}
|
|
setupHeaderUI(embedData, displayTitle, renderAsCallout, isSection) {
|
|
const { view, file, section, containerEl } = embedData;
|
|
requestAnimationFrame(() => {
|
|
setTimeout(() => {
|
|
const titleEl = view.containerEl.querySelector(".inline-title");
|
|
if (titleEl) titleEl.style.display = "none";
|
|
const viewContent = view.containerEl.querySelector(".view-content");
|
|
if (!viewContent) return;
|
|
if (view.containerEl.querySelector(".sync-embed-header")) return;
|
|
const headerUI = document.createElement("div");
|
|
headerUI.className = "sync-embed-header";
|
|
if (renderAsCallout) {
|
|
headerUI.classList.add("is-sticky");
|
|
const foldBtn = headerUI.createDiv("sync-embed-fold");
|
|
setIcon(foldBtn, "chevron-down");
|
|
const linkPath = section ? `${file.path}#${section}` : file.path;
|
|
headerUI.createEl("a", {
|
|
cls: "internal-link",
|
|
text: displayTitle,
|
|
attr: { "href": linkPath, "data-href": linkPath }
|
|
});
|
|
headerUI.addEventListener("click", (e) => {
|
|
if (e.target.closest("a")) return;
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
containerEl.classList.toggle("is-collapsed");
|
|
});
|
|
view.containerEl.insertBefore(headerUI, viewContent);
|
|
} else {
|
|
headerUI.textContent = displayTitle;
|
|
viewContent.insertBefore(headerUI, viewContent.firstChild);
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
hideProperties(embedData) {
|
|
const { view } = embedData;
|
|
requestAnimationFrame(() => {
|
|
setTimeout(() => {
|
|
const propertiesEl = view.containerEl.querySelector(".metadata-container");
|
|
if (propertiesEl) {
|
|
propertiesEl.style.display = "none";
|
|
}
|
|
}, 50);
|
|
});
|
|
}
|
|
setupPropertiesCollapse(embedData) {
|
|
const { view } = embedData;
|
|
requestAnimationFrame(() => {
|
|
setTimeout(() => {
|
|
const propertiesEl = view.containerEl.querySelector(".metadata-container");
|
|
if (!propertiesEl) return;
|
|
const heading = propertiesEl.querySelector(".metadata-properties-heading");
|
|
if (heading && !propertiesEl.classList.contains("is-collapsed")) {
|
|
heading.click();
|
|
}
|
|
}, 100);
|
|
});
|
|
}
|
|
renderError(container, message, addGap) {
|
|
const errorDiv = container.createDiv("sync-embed-error");
|
|
if (addGap) errorDiv.addClass("sync-embed-gap");
|
|
errorDiv.setText(message);
|
|
}
|
|
};
|
|
module2.exports = EmbedManager2;
|
|
}
|
|
});
|
|
|
|
// src/command-interceptor.js
|
|
var require_command_interceptor = __commonJS({
|
|
"src/command-interceptor.js"(exports2, module2) {
|
|
var { Notice } = require("obsidian");
|
|
var CommandInterceptor2 = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.handlers = /* @__PURE__ */ new Map();
|
|
this.setupHandlers();
|
|
}
|
|
setupHandlers() {
|
|
const mappings = {
|
|
"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,
|
|
"editor:toggle-highlight": this.toggleHighlightCommand,
|
|
"editor:insert-callout": this.insertCalloutCommand,
|
|
// NEW: Header commands (H2-H6 only, no H1)
|
|
"sync-embeds:insert-header-2": () => this.insertHeaderCommand(2),
|
|
"sync-embeds:insert-header-3": () => this.insertHeaderCommand(3),
|
|
"sync-embeds:insert-header-4": () => this.insertHeaderCommand(4),
|
|
"sync-embeds:insert-header-5": () => this.insertHeaderCommand(5),
|
|
"sync-embeds:insert-header-6": () => this.insertHeaderCommand(6)
|
|
};
|
|
for (const [commandId, handler] of Object.entries(mappings)) {
|
|
this.handlers.set(commandId, handler.bind(this));
|
|
}
|
|
}
|
|
hasHandler(commandId) {
|
|
return this.handlers.has(commandId);
|
|
}
|
|
handle(commandId, embedData, ...args) {
|
|
const handler = this.handlers.get(commandId);
|
|
if (handler) {
|
|
try {
|
|
return handler(embedData, ...args);
|
|
} catch (error) {
|
|
console.error(`Sync Embeds: Error handling command ${commandId}:`, error);
|
|
return false;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// === CHECKLIST ===
|
|
toggleChecklistCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
if (line.match(/^\s*- \[ \]/)) {
|
|
const newLine = line.replace(/- \[ \]/, "- [x]");
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: cursor.line, ch: 0 },
|
|
{ line: cursor.line, ch: line.length }
|
|
);
|
|
editor.setCursor({ line: cursor.line, ch: newLine.length });
|
|
} else if (line.match(/^\s*- \[x\]/i)) {
|
|
const newLine = line.replace(/- \[x\]/i, "- [ ]");
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: cursor.line, ch: 0 },
|
|
{ line: cursor.line, ch: line.length }
|
|
);
|
|
editor.setCursor({ line: cursor.line, ch: newLine.length });
|
|
} else {
|
|
const indent = line.match(/^\s*/)[0];
|
|
const content = line.substring(indent.length);
|
|
const newLine = `${indent}- [ ] ${content}`;
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: cursor.line, ch: 0 },
|
|
{ line: cursor.line, ch: line.length }
|
|
);
|
|
editor.setCursor({ line: cursor.line, ch: newLine.length });
|
|
}
|
|
return true;
|
|
}
|
|
// === FORMATTING ===
|
|
toggleMarkdownFormatting(embedData, markdownChar) {
|
|
const { editor } = embedData;
|
|
const selection = editor.getSelection();
|
|
const len = markdownChar.length;
|
|
if (selection && selection.startsWith(markdownChar) && selection.endsWith(markdownChar)) {
|
|
const newText = selection.slice(len, -len);
|
|
editor.replaceSelection(newText);
|
|
const cursor = editor.getCursor("from");
|
|
editor.setSelection(
|
|
cursor,
|
|
{ line: cursor.line, ch: cursor.ch + newText.length }
|
|
);
|
|
} else if (selection) {
|
|
editor.replaceSelection(`${markdownChar}${selection}${markdownChar}`);
|
|
const cursor = editor.getCursor("from");
|
|
editor.setSelection(
|
|
{ line: cursor.line, ch: cursor.ch + len },
|
|
{ line: cursor.line, ch: cursor.ch + len + selection.length }
|
|
);
|
|
} 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, "`");
|
|
}
|
|
toggleHighlightCommand(embedData) {
|
|
return this.toggleMarkdownFormatting(embedData, "==");
|
|
}
|
|
// === LINKS ===
|
|
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;
|
|
}
|
|
// === LISTS ===
|
|
toggleBulletListCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
if (line.match(/^\s*- /)) {
|
|
const newLine = line.replace(/^\s*- /, "");
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ 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;
|
|
}
|
|
toggleNumberedListCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const cursor = editor.getCursor();
|
|
const line = editor.getLine(cursor.line);
|
|
if (line.match(/^\s*\d+\. /)) {
|
|
const newLine = line.replace(/^\s*\d+\. /, "");
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ 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}1. ${content}`,
|
|
{ line: cursor.line, ch: 0 },
|
|
{ line: cursor.line, ch: line.length }
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
indentListCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const from = editor.getCursor("from").line;
|
|
const to = editor.getCursor("to").line;
|
|
for (let line = from; line <= to; line++) {
|
|
editor.replaceRange(" ", { line, ch: 0 });
|
|
}
|
|
return true;
|
|
}
|
|
unindentListCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const from = editor.getCursor("from").line;
|
|
const to = editor.getCursor("to").line;
|
|
for (let line = from; line <= to; line++) {
|
|
const lineText = editor.getLine(line);
|
|
if (lineText.startsWith(" ")) {
|
|
editor.replaceRange("", { line, ch: 0 }, { line, ch: 1 });
|
|
} else if (lineText.startsWith(" ")) {
|
|
editor.replaceRange("", { line, ch: 0 }, { line, ch: 4 });
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
// === TAGS ===
|
|
insertTagCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const cursor = editor.getCursor();
|
|
const selection = editor.getSelection();
|
|
if (selection) {
|
|
editor.replaceSelection(`#${selection}`);
|
|
} else {
|
|
editor.replaceRange("#", cursor);
|
|
}
|
|
return true;
|
|
}
|
|
// === CALLOUTS ===
|
|
insertCalloutCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const cursor = editor.getCursor();
|
|
const calloutText = "> [!note]\n> ";
|
|
editor.replaceRange(calloutText, cursor);
|
|
editor.setCursor({ line: cursor.line + 1, ch: 2 });
|
|
return true;
|
|
}
|
|
// === LINE OPERATIONS ===
|
|
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(`
|
|
${line}`, { line: cursor.line, ch: line.length });
|
|
editor.setCursor({ line: cursor.line + 1, ch: cursor.ch });
|
|
return true;
|
|
}
|
|
deleteLineCommand(embedData) {
|
|
const { editor } = embedData;
|
|
const { line } = editor.getCursor();
|
|
if (line === editor.lastLine()) {
|
|
const prevLineLength = line > 0 ? editor.getLine(line - 1).length : 0;
|
|
editor.replaceRange("", { line, ch: 0 }, { line, ch: editor.getLine(line).length });
|
|
if (line > 0) {
|
|
editor.replaceRange("", { line: line - 1, ch: prevLineLength }, { line, ch: 0 });
|
|
}
|
|
} else {
|
|
editor.replaceRange("", { line, ch: 0 }, { line: line + 1, ch: 0 });
|
|
}
|
|
return true;
|
|
}
|
|
// === HEADER COMMANDS ===
|
|
insertHeaderCommand(level) {
|
|
return (embedData) => {
|
|
const { editor, sectionInfo } = embedData;
|
|
if (sectionInfo) {
|
|
const { headerLevel } = sectionInfo;
|
|
if (level <= headerLevel) {
|
|
if (this.plugin.settings.showHeaderHints) {
|
|
const availableLevels = [];
|
|
for (let i = headerLevel + 1; i <= 6; i++) {
|
|
availableLevels.push(`H${i} (Alt+${i})`);
|
|
}
|
|
new Notice(
|
|
`\u26A0\uFE0F H${level} is not allowed in H${headerLevel} section.
|
|
Available: ${availableLevels.join(", ")}`,
|
|
5e3
|
|
);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
return this.insertHeader(embedData, level);
|
|
};
|
|
}
|
|
insertHeader(embedData, level) {
|
|
const { editor } = embedData;
|
|
const from = editor.getCursor("from");
|
|
const to = editor.getCursor("to");
|
|
if (from.line !== to.line) {
|
|
for (let line = from.line; line <= to.line; line++) {
|
|
this.toggleLineHeader(editor, line, level);
|
|
}
|
|
return true;
|
|
}
|
|
return this.toggleLineHeader(editor, from.line, level);
|
|
}
|
|
toggleLineHeader(editor, lineNum, level) {
|
|
const line = editor.getLine(lineNum);
|
|
const cursor = editor.getCursor();
|
|
const headerMatch = line.match(/^(\s*)(#{1,6})\s+(.*)$/);
|
|
if (headerMatch) {
|
|
const [, indent, hashes, content] = headerMatch;
|
|
const currentLevel = hashes.length;
|
|
if (currentLevel === level) {
|
|
const newLine = `${indent}${content}`;
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: lineNum, ch: 0 },
|
|
{ line: lineNum, ch: line.length }
|
|
);
|
|
if (cursor.line === lineNum) {
|
|
editor.setCursor({ line: lineNum, ch: indent.length });
|
|
}
|
|
} else {
|
|
const newHashes = "#".repeat(level);
|
|
const newLine = `${indent}${newHashes} ${content}`;
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: lineNum, ch: 0 },
|
|
{ line: lineNum, ch: line.length }
|
|
);
|
|
if (cursor.line === lineNum) {
|
|
const newCursorCh = Math.min(
|
|
cursor.ch + (newHashes.length - hashes.length),
|
|
newLine.length
|
|
);
|
|
editor.setCursor({ line: lineNum, ch: newCursorCh });
|
|
}
|
|
}
|
|
} else {
|
|
const indent = line.match(/^\s*/)[0];
|
|
const content = line.substring(indent.length);
|
|
const newHashes = "#".repeat(level);
|
|
const newLine = `${indent}${newHashes} ${content}`;
|
|
editor.replaceRange(
|
|
newLine,
|
|
{ line: lineNum, ch: 0 },
|
|
{ line: lineNum, ch: line.length }
|
|
);
|
|
if (cursor.line === lineNum) {
|
|
editor.setCursor({
|
|
line: lineNum,
|
|
ch: indent.length + newHashes.length + 1
|
|
});
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
};
|
|
module2.exports = CommandInterceptor2;
|
|
}
|
|
});
|
|
|
|
// src/settings.js
|
|
var require_settings = __commonJS({
|
|
"src/settings.js"(exports2, module2) {
|
|
var { PluginSettingTab, Setting, Notice } = require("obsidian");
|
|
var SyncEmbedsSettingTab2 = class extends PluginSettingTab {
|
|
constructor(app, plugin) {
|
|
super(app, plugin);
|
|
this.plugin = plugin;
|
|
}
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.createEl("h2", { text: "Sync Embeds Settings" });
|
|
containerEl.createEl("h3", { text: "Appearance" });
|
|
new Setting(containerEl).setName("Render as callout").setDesc("Render embeds as callouts with sticky headers and collapse functionality").addToggle((toggle) => toggle.setValue(this.plugin.settings.renderAsCallout).onChange(async (value) => {
|
|
this.plugin.settings.renderAsCallout = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new Setting(containerEl).setName("Embed height").setDesc("Default height for embeds").addDropdown((dropdown) => dropdown.addOption("auto", "Auto (fit content)").addOption("300px", "Compact (300px)").addOption("500px", "Normal (500px)").addOption("700px", "Large (700px)").addOption("custom", "Custom").setValue(this.getHeightPreset()).onChange(async (value) => {
|
|
if (value !== "custom") {
|
|
this.plugin.settings.embedHeight = value;
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
} else {
|
|
this.display();
|
|
}
|
|
}));
|
|
if (this.getHeightPreset() === "custom") {
|
|
new Setting(containerEl).setName("Custom height").setDesc("Enter a custom height (e.g., 450px, 60vh)").addText((text) => text.setPlaceholder("e.g., 450px, 60vh").setValue(this.plugin.settings.embedHeight).onChange(async (value) => {
|
|
if (this.validateCSSValue(value)) {
|
|
this.plugin.settings.embedHeight = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
}));
|
|
}
|
|
new Setting(containerEl).setName("Maximum height").setDesc("Maximum height before scrolling").addDropdown((dropdown) => dropdown.addOption("none", "None (no limit)").addOption("400px", "Compact (400px)").addOption("600px", "Normal (600px)").addOption("80vh", "Large (80vh)").addOption("custom", "Custom").setValue(this.getMaxHeightPreset()).onChange(async (value) => {
|
|
if (value !== "custom") {
|
|
this.plugin.settings.maxEmbedHeight = value;
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
} else {
|
|
this.display();
|
|
}
|
|
}));
|
|
if (this.getMaxHeightPreset() === "custom") {
|
|
new Setting(containerEl).setName("Custom maximum height").setDesc("Enter a custom maximum height (e.g., 550px, 70vh)").addText((text) => text.setPlaceholder("e.g., 550px, 70vh").setValue(this.plugin.settings.maxEmbedHeight).onChange(async (value) => {
|
|
if (this.validateCSSValue(value)) {
|
|
this.plugin.settings.maxEmbedHeight = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
}));
|
|
}
|
|
new Setting(containerEl).setName("Gap between embeds").setDesc("Spacing between multiple embeds in a sync block").addDropdown((dropdown) => dropdown.addOption("8px", "Compact (8px)").addOption("16px", "Normal (16px)").addOption("24px", "Spacious (24px)").addOption("custom", "Custom").setValue(this.getGapPreset()).onChange(async (value) => {
|
|
if (value !== "custom") {
|
|
this.plugin.settings.gapBetweenEmbeds = value;
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
} else {
|
|
this.display();
|
|
}
|
|
}));
|
|
if (this.getGapPreset() === "custom") {
|
|
new Setting(containerEl).setName("Custom gap").setDesc("Enter a custom gap (e.g., 20px, 1.5rem)").addText((text) => text.setPlaceholder("e.g., 20px, 1.5rem").setValue(this.plugin.settings.gapBetweenEmbeds).onChange(async (value) => {
|
|
if (this.validateCSSValue(value)) {
|
|
this.plugin.settings.gapBetweenEmbeds = value;
|
|
await this.plugin.saveSettings();
|
|
}
|
|
}));
|
|
}
|
|
containerEl.createEl("h3", { text: "Behavior" });
|
|
new Setting(containerEl).setName("Collapse properties by default").setDesc("Hide frontmatter/properties in embeds by default").addToggle((toggle) => toggle.setValue(this.plugin.settings.collapsePropertiesByDefault).onChange(async (value) => {
|
|
this.plugin.settings.collapsePropertiesByDefault = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new Setting(containerEl).setName("Show inline title").setDesc("Display note title at the top of whole-note embeds (not applicable to section embeds or embeds with aliases)").addToggle((toggle) => toggle.setValue(this.plugin.settings.showInlineTitle).onChange(async (value) => {
|
|
this.plugin.settings.showInlineTitle = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new Setting(containerEl).setName("Show focus highlight").setDesc("Highlight the focused embed with an outline").addToggle((toggle) => toggle.setValue(this.plugin.settings.showFocusHighlight).onChange(async (value) => {
|
|
this.plugin.settings.showFocusHighlight = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
new Setting(containerEl).setName("Hide linked mentions").setDesc("Hide the linked mentions panel that appears at the bottom of embedded notes").addToggle((toggle) => toggle.setValue(this.plugin.settings.hideLinkedMentions).onChange(async (value) => {
|
|
this.plugin.settings.hideLinkedMentions = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
containerEl.createEl("h3", { text: "Header Management" });
|
|
new Setting(containerEl).setName("Show header hints").setDesc("Display helpful notices when header creation is blocked in section embeds").addToggle((toggle) => toggle.setValue(this.plugin.settings.showHeaderHints).onChange(async (value) => {
|
|
this.plugin.settings.showHeaderHints = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
containerEl.createEl("h3", { text: "Performance" });
|
|
new Setting(containerEl).setName("Lazy loading threshold").setDesc("Start loading embeds this distance before they become visible").addDropdown((dropdown) => dropdown.addOption("0px", "On screen (0px)").addOption("100px", "Just before (100px)").addOption("200px", "Well before (200px)").addOption("500px", "Early (500px)").setValue(this.plugin.settings.lazyLoadThreshold).onChange(async (value) => {
|
|
this.plugin.settings.lazyLoadThreshold = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
containerEl.createEl("h3", { text: "Advanced" });
|
|
new Setting(containerEl).setName("Enable command interception").setDesc("Allow keyboard shortcuts (Ctrl+B, Ctrl+I, etc.) to work in embeds. Requires restart.").addToggle((toggle) => toggle.setValue(this.plugin.settings.enableCommandInterception).onChange(async (value) => {
|
|
this.plugin.settings.enableCommandInterception = value;
|
|
await this.plugin.saveSettings();
|
|
new Notice("Please restart Obsidian for this change to take effect");
|
|
}));
|
|
new Setting(containerEl).setName("Debug mode").setDesc("Enable detailed console logging for troubleshooting").addToggle((toggle) => toggle.setValue(this.plugin.settings.debugMode || false).onChange(async (value) => {
|
|
this.plugin.settings.debugMode = value;
|
|
await this.plugin.saveSettings();
|
|
}));
|
|
containerEl.createEl("h3", { text: "Help & Documentation" });
|
|
const helpDiv = containerEl.createDiv("sync-embeds-help");
|
|
helpDiv.innerHTML = `
|
|
<p><strong>Basic Usage:</strong></p>
|
|
<ul>
|
|
<li><code>![[Note Name]]</code> - Embed entire note</li>
|
|
<li><code>![[Note Name#Section]]</code> - Embed specific section</li>
|
|
<li><code>![[Note Name|Custom Title]]</code> - Display with custom title</li>
|
|
<li><code>![[Note Name#Section|Custom Title]]</code> - Section with custom title</li>
|
|
</ul>
|
|
|
|
<p><strong>Per-Embed Custom Options:</strong></p>
|
|
<ul>
|
|
<li><code>![[Note|Alias{height:500px}]]</code> - Custom height for this embed</li>
|
|
<li><code>![[Note|Alias{maxHeight:600px}]]</code> - Custom max height</li>
|
|
<li><code>![[Note|Alias{title:false}]]</code> - Hide title for this embed</li>
|
|
<li><code>![[Note|Alias{collapse:true}]]</code> - Start embed in collapsed state (requires callout style)</li>
|
|
<li><code>![[Note|Alias{callout:true}]]</code> - Force callout style for this embed</li>
|
|
<li><code>![[Note|Alias{height:400px,title:false}]]</code> - Multiple options</li>
|
|
</ul>
|
|
<p><em>Note: Options go inside curly braces before the closing ]]</em></p>
|
|
|
|
<p><strong>Dynamic Patterns:</strong></p>
|
|
<ul>
|
|
<li><code>![[Daily/{{date:YYYY-MM-DD}}|Today]]</code> - Current date with display name</li>
|
|
<li><code>![[Tasks#{{date:YYYY-MM-DD}}|Today's Tasks]]</code> - Dynamic section</li>
|
|
<li><code>{{date-7d:YYYY-MM-DD}}</code> - 7 days ago</li>
|
|
<li><code>{{date+1w:YYYY-MM-DD}}</code> - 1 week from now</li>
|
|
<li><code>{{date+2m:YYYY-MM-DD}}</code> - 2 months from now</li>
|
|
<li><code>{{time:HH:mm}}</code> - Current time</li>
|
|
<li><code>{{title}}</code> - Current note's title</li>
|
|
</ul>
|
|
|
|
<p><strong>Header Management:</strong></p>
|
|
<ul>
|
|
<li>Use <code>Alt+2</code> through <code>Alt+6</code> to insert headers (H2-H6)</li>
|
|
<li>In section embeds, only sub-headers are allowed (e.g., if section is H2, only H3-H6 work)</li>
|
|
<li>Typing <code>#</code> at line start is blocked in section embeds to prevent hierarchy violations</li>
|
|
<li>Press the same hotkey again on a header to remove formatting</li>
|
|
<li>Press a different hotkey to change header level</li>
|
|
<li>Whole-note embeds allow H1-H6 freely</li>
|
|
<li>These hotkeys can be customized in Obsidian's Hotkeys settings</li>
|
|
</ul>
|
|
|
|
<p><strong>Date Format Examples:</strong></p>
|
|
<ul>
|
|
<li><code>YYYY-MM-DD</code> - 2024-03-15</li>
|
|
<li><code>YYYY/MM/DD</code> - 2024/03/15</li>
|
|
<li><code>DD MMM YYYY</code> - 15 Mar 2024</li>
|
|
<li><code>dddd, MMMM Do YYYY</code> - Friday, March 15th 2024</li>
|
|
</ul>
|
|
|
|
<p><strong>Complete Example:</strong></p>
|
|
<pre><code>\`\`\`sync
|
|
![[Daily Notes/{{date:YYYY-MM-DD}}|Today's Note]]
|
|
![[Daily Notes/{{date-1d:YYYY-MM-DD}}|Yesterday]]
|
|
![[Tasks#Inbox|My Tasks{height:300px}]]
|
|
![[Projects/{{title}}#Notes|Project Notes{title:false}]]
|
|
\`\`\`</code></pre>
|
|
|
|
<p><strong>Tips:</strong></p>
|
|
<ul>
|
|
<li>Use aliases (text after <code>|</code>) with dynamic patterns for better display</li>
|
|
<li>Per-embed options override global settings: <code>{height:400px,title:false}</code></li>
|
|
<li>Use <code>collapse:true</code> to hide embed content by default (requires callout style)</li>
|
|
<li>Section embeds are fully editable and changes sync immediately</li>
|
|
<li>Press Tab/Shift+Tab to navigate between embeds</li>
|
|
<li>Keyboard shortcuts work inside embeds when command interception is enabled</li>
|
|
<li>Use lazy loading for better performance with many embeds</li>
|
|
<li>Multiple embeds of the same note/section are allowed</li>
|
|
<li>Header hierarchy enforcement maintains document structure in section embeds</li>
|
|
</ul>
|
|
|
|
<p><em>Note: Dynamic patterns are cached for 1 second to improve performance.</em></p>
|
|
`;
|
|
containerEl.createEl("h3", { text: "Reset" });
|
|
new Setting(containerEl).setName("Reset to defaults").setDesc("Reset all settings to their default values").addButton((button) => button.setButtonText("Reset").setWarning().onClick(async () => {
|
|
if (confirm("Are you sure you want to reset all settings to defaults?")) {
|
|
Object.assign(this.plugin.settings, this.plugin.DEFAULT_SETTINGS);
|
|
await this.plugin.saveSettings();
|
|
this.display();
|
|
new Notice("Settings reset to defaults");
|
|
}
|
|
}));
|
|
}
|
|
// Helper methods for presets
|
|
getHeightPreset() {
|
|
const value = this.plugin.settings.embedHeight;
|
|
if (["auto", "300px", "500px", "700px"].includes(value)) {
|
|
return value;
|
|
}
|
|
return "custom";
|
|
}
|
|
getMaxHeightPreset() {
|
|
const value = this.plugin.settings.maxEmbedHeight;
|
|
if (["none", "400px", "600px", "80vh"].includes(value)) {
|
|
return value;
|
|
}
|
|
return "custom";
|
|
}
|
|
getGapPreset() {
|
|
const value = this.plugin.settings.gapBetweenEmbeds;
|
|
if (["8px", "16px", "24px"].includes(value)) {
|
|
return value;
|
|
}
|
|
return "custom";
|
|
}
|
|
validateCSSValue(value) {
|
|
if (!value || value.trim() === "") return false;
|
|
const validPattern = /^(auto|none|\d+(\.\d+)?(px|em|rem|vh|vw|%))$/;
|
|
const isValid = validPattern.test(value.trim());
|
|
if (!isValid) {
|
|
new Notice('Invalid CSS value. Use units like: px, em, rem, vh, vw, %, or "auto"/"none"');
|
|
}
|
|
return isValid;
|
|
}
|
|
};
|
|
module2.exports = SyncEmbedsSettingTab2;
|
|
}
|
|
});
|
|
|
|
// src/main.js
|
|
var { Plugin, MarkdownView } = require("obsidian");
|
|
var { around } = require_dist();
|
|
var EmbedManager = require_embed_manager();
|
|
var CommandInterceptor = require_command_interceptor();
|
|
var SyncEmbedsSettingTab = require_settings();
|
|
var DEFAULT_SETTINGS = {
|
|
embedHeight: "auto",
|
|
maxEmbedHeight: "none",
|
|
collapsePropertiesByDefault: true,
|
|
showInlineTitle: true,
|
|
renderAsCallout: false,
|
|
enableCommandInterception: true,
|
|
gapBetweenEmbeds: "16px",
|
|
lazyLoadThreshold: "100px",
|
|
showFocusHighlight: true,
|
|
showHeaderHints: true,
|
|
// NEW: Header hints (enforcement is always on)
|
|
hideLinkedMentions: true,
|
|
debugMode: false
|
|
};
|
|
module.exports = class SyncEmbedPlugin extends Plugin {
|
|
constructor(app, manifest) {
|
|
super(app, manifest);
|
|
this.settings = DEFAULT_SETTINGS;
|
|
this.DEFAULT_SETTINGS = DEFAULT_SETTINGS;
|
|
this.embedManager = null;
|
|
this.commandInterceptor = null;
|
|
this.currentFocusedEmbed = null;
|
|
this.uninstallers = [];
|
|
}
|
|
async onload() {
|
|
await this.loadSettings();
|
|
this.embedManager = new EmbedManager(this);
|
|
this.commandInterceptor = new CommandInterceptor(this);
|
|
if (this.settings.enableCommandInterception) {
|
|
this.setupCommandInterception();
|
|
}
|
|
this.addCommand({
|
|
id: "insert-synced-embed",
|
|
name: "Insert synced embed",
|
|
editorCallback: (editor, view) => {
|
|
const selection = editor.getSelection();
|
|
const noteName = selection || "Note Name";
|
|
const textToInsert = `\`\`\`sync
|
|
![[${noteName}]]
|
|
\`\`\``;
|
|
editor.replaceSelection(textToInsert);
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-multi-synced-embed",
|
|
name: "Insert sync block with multiple embeds",
|
|
editorCallback: (editor, view) => {
|
|
const textToInsert = `\`\`\`sync
|
|
![[Note 1]]
|
|
![[Note 2]]
|
|
\`\`\``;
|
|
editor.replaceSelection(textToInsert);
|
|
}
|
|
});
|
|
this.addCommand({
|
|
id: "insert-dynamic-date-embed",
|
|
name: "Insert dynamic date embed (today)",
|
|
editorCallback: (editor, view) => {
|
|
const textToInsert = `\`\`\`sync
|
|
![[Daily/{{date:YYYY-MM-DD}}|Today]]
|
|
\`\`\``;
|
|
editor.replaceSelection(textToInsert);
|
|
}
|
|
});
|
|
this.registerHeaderCommands();
|
|
this.registerMarkdownCodeBlockProcessor("sync", (source, el, ctx) => {
|
|
this.embedManager.processSyncBlock(source, el, ctx);
|
|
});
|
|
this.registerDomEvent(document, "focusin", this.trackFocus.bind(this));
|
|
this.registerDomEvent(document, "focusout", this.trackFocusLoss.bind(this));
|
|
this.addSettingTab(new SyncEmbedsSettingTab(this.app, this));
|
|
this.updateFocusHighlight();
|
|
this.updateLinkedMentions();
|
|
this.log("Sync Embeds plugin loaded successfully");
|
|
}
|
|
onunload() {
|
|
this.log("Unloading Sync Embeds plugin");
|
|
this.uninstallers.forEach((uninstall) => uninstall());
|
|
this.uninstallers = [];
|
|
if (this.embedManager) {
|
|
this.embedManager.cleanup();
|
|
}
|
|
this.currentFocusedEmbed = null;
|
|
}
|
|
registerHeaderCommands() {
|
|
const headerLevels = [
|
|
{ level: 2, name: "Heading 2", key: "2" },
|
|
{ level: 3, name: "Heading 3", key: "3" },
|
|
{ level: 4, name: "Heading 4", key: "4" },
|
|
{ level: 5, name: "Heading 5", key: "5" },
|
|
{ level: 6, name: "Heading 6", key: "6" }
|
|
];
|
|
headerLevels.forEach(({ level, name, key }) => {
|
|
this.addCommand({
|
|
id: `insert-header-${level}`,
|
|
name: `Toggle ${name}`,
|
|
editorCallback: (editor, view) => {
|
|
const focusedEmbed = this.getFocusedEmbed();
|
|
if (focusedEmbed) {
|
|
const handler = this.commandInterceptor.insertHeaderCommand(level);
|
|
return handler(focusedEmbed);
|
|
}
|
|
this.commandInterceptor.insertHeader({ editor }, level);
|
|
},
|
|
hotkeys: [
|
|
{
|
|
modifiers: ["Alt"],
|
|
key
|
|
}
|
|
]
|
|
});
|
|
});
|
|
this.log("Registered header commands with default hotkeys (Alt+2-6)");
|
|
}
|
|
setupCommandInterception() {
|
|
try {
|
|
const executeCommandUninstall = around(this.app.commands, {
|
|
executeCommand: (old) => {
|
|
const plugin = this;
|
|
return function(command, ...args) {
|
|
const focusedEmbed = plugin.getFocusedEmbed();
|
|
if (focusedEmbed) {
|
|
const headerMatch = command.id.match(/^sync-embeds:insert-header-(\d+)$/);
|
|
if (headerMatch) {
|
|
const level = parseInt(headerMatch[1]);
|
|
plugin.log(`Intercepting header command: ${command.id}`);
|
|
const handler = plugin.commandInterceptor.insertHeaderCommand(level);
|
|
return handler(focusedEmbed);
|
|
}
|
|
if (plugin.commandInterceptor.hasHandler(command.id)) {
|
|
plugin.log(`Intercepting command: ${command.id}`);
|
|
return plugin.commandInterceptor.handle(command.id, focusedEmbed, ...args);
|
|
}
|
|
plugin.log(`Passing through command to embed: ${command.id}`);
|
|
if (command.editorCallback && focusedEmbed.editor) {
|
|
try {
|
|
return command.editorCallback(focusedEmbed.editor, focusedEmbed.view);
|
|
} catch (error) {
|
|
plugin.log(`Error executing command ${command.id} on embed:`, error);
|
|
}
|
|
}
|
|
}
|
|
return old.call(this, command, ...args);
|
|
};
|
|
}
|
|
});
|
|
this.uninstallers.push(executeCommandUninstall);
|
|
const getActiveViewUninstall = around(this.app.workspace, {
|
|
getActiveViewOfType: (old) => {
|
|
const plugin = this;
|
|
return function(type) {
|
|
const focusedEmbed = plugin.getFocusedEmbed();
|
|
if (focusedEmbed && focusedEmbed.view instanceof type) {
|
|
plugin.log(`Returning embed view for type: ${type.name}`);
|
|
return focusedEmbed.view;
|
|
}
|
|
return old.call(this, type);
|
|
};
|
|
}
|
|
});
|
|
this.uninstallers.push(getActiveViewUninstall);
|
|
const workspace = this.app.workspace;
|
|
const origLeafDesc = Object.getOwnPropertyDescriptor(workspace, "activeLeaf") || Object.getOwnPropertyDescriptor(Object.getPrototypeOf(workspace), "activeLeaf");
|
|
if (origLeafDesc) {
|
|
const plugin = this;
|
|
const origGet = origLeafDesc.get;
|
|
Object.defineProperty(workspace, "activeLeaf", {
|
|
...origLeafDesc,
|
|
configurable: true,
|
|
get() {
|
|
const focusedEmbed = plugin.getFocusedEmbed();
|
|
if (focusedEmbed && focusedEmbed.leaf) {
|
|
plugin.log("Returning embed leaf as active leaf");
|
|
return focusedEmbed.leaf;
|
|
}
|
|
return origGet ? origGet.call(this) : void 0;
|
|
}
|
|
});
|
|
this.uninstallers.push(() => Object.defineProperty(workspace, "activeLeaf", origLeafDesc));
|
|
}
|
|
this.log("Command interception setup complete");
|
|
} catch (error) {
|
|
console.error("Sync Embeds: Failed to setup command interception:", error);
|
|
}
|
|
}
|
|
// Focus tracking
|
|
trackFocus(event) {
|
|
var _a;
|
|
const embed = this.embedManager.getEmbedFromElement(event.target);
|
|
if (embed) {
|
|
this.currentFocusedEmbed = embed;
|
|
this.log("Focus changed to embed:", (_a = embed.file) == null ? void 0 : _a.basename);
|
|
}
|
|
}
|
|
trackFocusLoss(event) {
|
|
const embed = this.embedManager.getEmbedFromElement(event.relatedTarget);
|
|
if (!embed) {
|
|
this.log("Focus lost from embed");
|
|
this.currentFocusedEmbed = null;
|
|
}
|
|
}
|
|
getFocusedEmbed() {
|
|
return this.currentFocusedEmbed;
|
|
}
|
|
// Settings management
|
|
async loadSettings() {
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
this.refreshAllEmbeds();
|
|
this.updateFocusHighlight();
|
|
this.updateLinkedMentions();
|
|
}
|
|
updateFocusHighlight() {
|
|
if (this.settings.showFocusHighlight) {
|
|
document.body.removeClass("sync-embeds-no-focus-highlight");
|
|
} else {
|
|
document.body.addClass("sync-embeds-no-focus-highlight");
|
|
}
|
|
}
|
|
updateLinkedMentions() {
|
|
if (this.settings.hideLinkedMentions) {
|
|
document.body.addClass("sync-embeds-hide-linked-mentions");
|
|
} else {
|
|
document.body.removeClass("sync-embeds-hide-linked-mentions");
|
|
}
|
|
}
|
|
refreshAllEmbeds() {
|
|
document.querySelectorAll(".sync-container").forEach((container) => {
|
|
container.style.setProperty("--sync-embed-height", this.settings.embedHeight);
|
|
container.style.setProperty("--sync-max-height", this.settings.maxEmbedHeight);
|
|
container.style.setProperty("--sync-gap", this.settings.gapBetweenEmbeds);
|
|
});
|
|
this.log("Refreshed all embeds with new settings");
|
|
}
|
|
// Debug logging
|
|
log(...args) {
|
|
if (this.settings.debugMode) {
|
|
console.log("[Sync Embeds]", ...args);
|
|
}
|
|
}
|
|
};
|