uthvah_sync-embeds/main.js
Verity 675ca16fa9 fix: resolve Obsidian scorecard warnings and readable-line-width padding bug
- Replace deprecated builtin-modules with node:module built-in
- Pin monkey-around to exact version 3.0.0
- Upgrade esbuild 0.17.3 → 0.28.1 (fixes high-severity CVE)
- Commit package-lock.json for reproducible builds
- Add CONTRIBUTING.md and GitHub Actions release workflow with artifact attestation
- Fix #ccc shorthand hex in print styles
- Neutralize Obsidian's readable-line-width per-line margin-inline inside sync embeds

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-14 16:32:22 +01:00

1694 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();
}));
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)
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.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();
}
updateFocusHighlight() {
if (this.settings.showFocusHighlight) {
document.body.removeClass("sync-embeds-no-focus-highlight");
} else {
document.body.addClass("sync-embeds-no-focus-highlight");
}
}
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);
}
}
};