mirror of
https://github.com/zen-browser/docs.git
synced 2025-07-08 01:10:03 +02:00
4367 lines
136 KiB
JavaScript
4367 lines
136 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
|
|
*/
|
|
|
|
"use strict";
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/main.ts
|
|
var main_exports = {};
|
|
__export(main_exports, {
|
|
default: () => CalloutManagerPlugin
|
|
});
|
|
module.exports = __toCommonJS(main_exports);
|
|
var import_obsidian25 = require("obsidian");
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getFloatingWindows.js
|
|
function getFloatingWindows(app2) {
|
|
var _a, _b, _c, _d;
|
|
return (_d = (_c = (_b = (_a = app2 === null || app2 === void 0 ? void 0 : app2.workspace) === null || _a === void 0 ? void 0 : _a.floatingSplit) === null || _b === void 0 ? void 0 : _b.children) === null || _c === void 0 ? void 0 : _c.map((split) => split.win)) !== null && _d !== void 0 ? _d : [];
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getCurrentThemeID.js
|
|
function getCurrentThemeID(app2) {
|
|
const theme = app2.customCss.theme;
|
|
return theme === "" ? null : theme;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getCurrentColorScheme.js
|
|
function getCurrentThemeID2(app2) {
|
|
const { body } = app2.workspace.containerEl.doc;
|
|
return body.classList.contains("theme-dark") ? "dark" : "light";
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getThemeManifest.js
|
|
function getThemeManifest(app2, themeID) {
|
|
const manifests = app2.customCss.themes;
|
|
if (!Object.prototype.hasOwnProperty.call(manifests, themeID)) {
|
|
return null;
|
|
}
|
|
return manifests[themeID];
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/isThemeInstalled.js
|
|
function isThemeInstalled(app2, themeID) {
|
|
return getThemeManifest(app2, themeID) !== null;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getThemeStyleElement.js
|
|
function getThemeStyleElement(app2) {
|
|
const currentTheme = getCurrentThemeID(app2);
|
|
if (currentTheme == null || !isThemeInstalled(app2, currentTheme)) {
|
|
return null;
|
|
}
|
|
return app2.customCss.styleEl;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/isSnippetEnabled.js
|
|
function isSnippetEnabled(app2, snippetID) {
|
|
return app2.customCss.enabledSnippets.has(snippetID);
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/fetchObsidianStyleSheet.js
|
|
var import_obsidian = require("obsidian");
|
|
|
|
// node_modules/obsidian-extra/dist/esm/internal/utils/versionCompare.js
|
|
function versionCompare(a, b) {
|
|
const aParts = a.split(".").map((n) => parseInt(n, 10));
|
|
const bParts = b.split(".").map((n) => parseInt(n, 10));
|
|
const partsSize = Math.max(aParts.length, bParts.length);
|
|
arrayPadEnd(aParts, partsSize, 0);
|
|
arrayPadEnd(bParts, partsSize, 0);
|
|
for (let i = 0; i < partsSize; i++) {
|
|
if (aParts[i] < bParts[i])
|
|
return -1;
|
|
if (aParts[i] > bParts[i])
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
function arrayPadEnd(arr, length, fill) {
|
|
const missing = length - arr.length;
|
|
if (missing > 0) {
|
|
arr.push(...new Array(missing).fill(fill));
|
|
}
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/fetchObsidianStyleSheet.js
|
|
var __awaiter = function(thisArg, _arguments, P, generator) {
|
|
function adopt(value) {
|
|
return value instanceof P ? value : new P(function(resolve) {
|
|
resolve(value);
|
|
});
|
|
}
|
|
return new (P || (P = Promise))(function(resolve, reject) {
|
|
function fulfilled(value) {
|
|
try {
|
|
step(generator.next(value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
function rejected(value) {
|
|
try {
|
|
step(generator["throw"](value));
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
}
|
|
function step(result) {
|
|
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
|
|
}
|
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
});
|
|
};
|
|
function fetchObsidianStyleSheet(app2) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
let errors = [];
|
|
const orElse = (cb) => (ex) => {
|
|
errors.push(ex);
|
|
return cb();
|
|
};
|
|
const result = yield viaElectron("app.css").catch(orElse(() => viaFetch("app.css"))).catch(orElse(() => viaDom("app.css")));
|
|
result._errors = errors;
|
|
return result;
|
|
});
|
|
}
|
|
function viaFetch(path) {
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (import_obsidian.Platform.isDesktopApp) {
|
|
throw new Error("Obsidian styles via fetch() does not work under Electron.");
|
|
}
|
|
return fetch(`/${path}`).then((r) => r.text()).then((t) => ({
|
|
method: "fetch",
|
|
cssText: t
|
|
}));
|
|
});
|
|
}
|
|
function viaElectron(path) {
|
|
var _a;
|
|
return __awaiter(this, void 0, void 0, function* () {
|
|
if (versionCompare(import_obsidian.apiVersion, "1.1.16") > 0) {
|
|
throw new Error(`Obsidian ${import_obsidian.apiVersion} has not been tested with this function`);
|
|
}
|
|
const require2 = globalThis.require;
|
|
const electron = (_a = globalThis.electron) !== null && _a !== void 0 ? _a : require2 === null || require2 === void 0 ? void 0 : require2("electron");
|
|
if (electron == null) {
|
|
throw new Error("Unable to get electron module from web renderer process");
|
|
}
|
|
const fs = require2 === null || require2 === void 0 ? void 0 : require2("fs/promises");
|
|
if ((fs === null || fs === void 0 ? void 0 : fs.readFile) == null) {
|
|
throw new Error("Unable to get fs/promises module from web renderer process");
|
|
}
|
|
const resources = electron.ipcRenderer.sendSync("resources");
|
|
return fs.readFile(`${resources}/${path}`, "utf8").then((t) => ({
|
|
method: "electron",
|
|
cssText: t
|
|
}));
|
|
});
|
|
}
|
|
function viaDom(path) {
|
|
let found = false;
|
|
const lines = [];
|
|
for (const styleSheet of Array.from(document.styleSheets)) {
|
|
if (!(styleSheet.ownerNode instanceof HTMLLinkElement))
|
|
continue;
|
|
const href = styleSheet.ownerNode.getAttribute("href");
|
|
if (href !== path && href !== `/${path}`)
|
|
continue;
|
|
found = true;
|
|
for (const rule of Array.from(styleSheet.cssRules)) {
|
|
lines.push(rule.cssText);
|
|
}
|
|
}
|
|
if (!found) {
|
|
throw new Error("Unable to find <link> element for Obsidian's stylesheet");
|
|
}
|
|
return {
|
|
method: "dom",
|
|
cssText: lines.join("\n")
|
|
};
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getInstalledSnippetIDs.js
|
|
function getInstalledSnippetIDs(app2) {
|
|
return app2.customCss.snippets;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/getSnippetStyleElements.js
|
|
function getSnippetStyleElements(app2) {
|
|
const styleManager = app2.customCss;
|
|
const snippets = getInstalledSnippetIDs(app2);
|
|
const map = /* @__PURE__ */ new Map();
|
|
for (let i = 0, elI = 0; i < snippets.length; i++) {
|
|
const snippetID = styleManager.snippets[i];
|
|
if (isSnippetEnabled(app2, snippetID)) {
|
|
map.set(snippetID, styleManager.extraStyleEls[elI]);
|
|
elI++;
|
|
}
|
|
}
|
|
return map;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/openPluginSettings.js
|
|
function openPluginSettings(app2, plugin) {
|
|
var _a, _b;
|
|
const settingManager = app2.setting;
|
|
const pluginId = typeof plugin === "string" ? plugin : plugin.manifest.id;
|
|
if (((_a = settingManager.activeTab) === null || _a === void 0 ? void 0 : _a.id) !== pluginId) {
|
|
settingManager.openTabById("");
|
|
}
|
|
settingManager.open();
|
|
if (((_b = settingManager.activeTab) === null || _b === void 0 ? void 0 : _b.id) !== pluginId) {
|
|
settingManager.openTabById(pluginId);
|
|
}
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/createCustomStyleSheet.js
|
|
var Counter = Symbol("CustomStylesheet count");
|
|
function foreachWindow(app2, fn) {
|
|
fn(app2.workspace.containerEl.doc, true);
|
|
for (const float of getFloatingWindows(app2)) {
|
|
fn(float.document, false);
|
|
}
|
|
}
|
|
function createCustomStyleSheet(app2, plugin) {
|
|
var _a;
|
|
let result;
|
|
const pl = plugin;
|
|
const plId = plugin.manifest.id;
|
|
const ssId = ((_a = pl[Counter]) !== null && _a !== void 0 ? _a : pl[Counter] = 0).toString();
|
|
pl[Counter]++;
|
|
const styleEl = app2.workspace.containerEl.doc.createElement("style");
|
|
const styleElInFloats = [];
|
|
styleEl.setAttribute("data-source-plugin", plId);
|
|
styleEl.setAttribute("data-source-id", ssId);
|
|
function unapply() {
|
|
styleElInFloats.splice(0, styleElInFloats.length).forEach((el) => el.remove());
|
|
styleEl.detach();
|
|
foreachWindow(app2, (doc) => {
|
|
for (const styleEl2 of Array.from(doc.head.querySelectorAll("style"))) {
|
|
if (result.is(styleEl2)) {
|
|
styleEl2.remove();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
function reapply() {
|
|
unapply();
|
|
foreachWindow(app2, (doc, isFloating) => {
|
|
let lastEl = doc.head.lastElementChild;
|
|
for (let el = lastEl; el != null; el = el.previousElementSibling) {
|
|
lastEl = el;
|
|
if (lastEl.tagName === "STYLE") {
|
|
break;
|
|
}
|
|
}
|
|
if (!isFloating) {
|
|
lastEl === null || lastEl === void 0 ? void 0 : lastEl.insertAdjacentElement("afterend", styleEl);
|
|
return;
|
|
}
|
|
const styleElClone = styleEl.cloneNode(true);
|
|
styleElInFloats.push(styleElClone);
|
|
lastEl === null || lastEl === void 0 ? void 0 : lastEl.insertAdjacentElement("afterend", styleElClone);
|
|
});
|
|
}
|
|
app2.workspace.on("css-change", reapply);
|
|
app2.workspace.on("layout-change", reapply);
|
|
result = Object.freeze(Object.defineProperties(() => {
|
|
unapply();
|
|
app2.workspace.off("css-change", reapply);
|
|
app2.workspace.off("layout-change", reapply);
|
|
}, {
|
|
css: {
|
|
enumerable: true,
|
|
configurable: false,
|
|
get() {
|
|
return styleEl.textContent;
|
|
},
|
|
set(v) {
|
|
styleEl.textContent = v;
|
|
for (const styleEl2 of styleElInFloats) {
|
|
styleEl2.textContent = v;
|
|
}
|
|
}
|
|
},
|
|
is: {
|
|
enumerable: false,
|
|
configurable: false,
|
|
value: (el) => {
|
|
return el.getAttribute("data-source-plugin") === plId && el.getAttribute("data-source-id") === ssId;
|
|
}
|
|
},
|
|
setAttribute: {
|
|
enumerable: false,
|
|
configurable: false,
|
|
value: (attr, value) => {
|
|
if (attr === "data-source-id" || attr === "data-source-plugin") {
|
|
throw new Error(`Cannot change attribute '${attr}' on custom style sheet.`);
|
|
}
|
|
styleEl.setAttribute(attr, value);
|
|
for (const styleEl2 of styleElInFloats) {
|
|
styleEl2.setAttribute(attr, value);
|
|
}
|
|
}
|
|
},
|
|
removeAttribute: {
|
|
enumerable: false,
|
|
configurable: false,
|
|
value: (attr) => {
|
|
if (attr === "data-source-id" || attr === "data-source-plugin") {
|
|
throw new Error(`Cannot remove attribute '${attr}' from custom style sheet.`);
|
|
}
|
|
styleEl.removeAttribute(attr);
|
|
for (const styleEl2 of styleElInFloats) {
|
|
styleEl2.removeAttribute(attr);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
reapply();
|
|
return result;
|
|
}
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/detectPlatformBrowser.js
|
|
var import_obsidian2 = require("obsidian");
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/detectPlatformRuntime.js
|
|
var import_obsidian3 = require("obsidian");
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/detectPlatformOperatingSystem.js
|
|
var import_obsidian4 = require("obsidian");
|
|
|
|
// src/ui/paned-setting-tab.ts
|
|
var import_obsidian6 = require("obsidian");
|
|
|
|
// node_modules/obsidian-extra/dist/esm/functions/closeSettings.js
|
|
function closeSettings(app2) {
|
|
const settingManager = app2.setting;
|
|
settingManager.close();
|
|
}
|
|
|
|
// src/ui/pane-layers.ts
|
|
var import_obsidian5 = require("obsidian");
|
|
var UIPaneLayers = class {
|
|
constructor(options) {
|
|
this.layers = [];
|
|
this.closeParent = options.close;
|
|
this.navInstance = {
|
|
open: (pane) => this.push(pane),
|
|
close: () => this.pop(),
|
|
replace: (pane) => this.top = pane
|
|
};
|
|
}
|
|
/**
|
|
* Pushes a new pane on top of the stack.
|
|
* The active pane will be suspended.
|
|
*
|
|
* @param pane The pane to push.
|
|
*/
|
|
push(pane) {
|
|
const { activePane: oldPane } = this;
|
|
if (oldPane !== void 0) {
|
|
const title = oldPane.title;
|
|
this.layers.push({
|
|
scroll: { top: this.scrollEl.scrollTop, left: this.scrollEl.scrollLeft },
|
|
state: oldPane.suspendState(),
|
|
pane: oldPane,
|
|
title: typeof title === "string" ? title : title.subtitle
|
|
});
|
|
this.setPaneVariables(oldPane, false);
|
|
this.containerEl.empty();
|
|
}
|
|
const newPane = this.activePane = pane;
|
|
this.setPaneVariables(newPane, true);
|
|
newPane.onReady();
|
|
this.doDisplay(true);
|
|
this.scrollEl.scrollTo({ top: 0, left: 0 });
|
|
}
|
|
/**
|
|
* Pops the active pane off the stack.
|
|
* The active pane will be destroyed, and the one underneath it will be restored.
|
|
*
|
|
* @param pane The pane to push.
|
|
*/
|
|
pop(options) {
|
|
if (this.activePane === void 0) {
|
|
this.closeParent();
|
|
return void 0;
|
|
}
|
|
const noDisplay = options?.noDisplay ?? false;
|
|
const oldPane = this.activePane;
|
|
const newPane = this.layers.pop();
|
|
this.activePane = void 0;
|
|
this.setPaneVariables(oldPane, false);
|
|
oldPane.onClose(options?.cancelled ?? false);
|
|
if (!noDisplay) {
|
|
this.containerEl.empty();
|
|
}
|
|
if (newPane !== void 0) {
|
|
this.activePane = newPane.pane;
|
|
this.setPaneVariables(newPane.pane, true);
|
|
newPane.pane.restoreState(newPane.state);
|
|
if (!noDisplay) {
|
|
this.doDisplay(true);
|
|
this.scrollEl.scrollTo(newPane.scroll);
|
|
}
|
|
}
|
|
return oldPane;
|
|
}
|
|
/**
|
|
* Removes all panes off the stack.
|
|
* All panes will be destroyed.
|
|
*
|
|
* @param pane The pane to push.
|
|
*/
|
|
clear(options) {
|
|
const removed = [];
|
|
const opts = {
|
|
noDisplay: true,
|
|
...options ?? {}
|
|
};
|
|
while (this.activePane !== void 0) {
|
|
removed.push(this.pop(opts));
|
|
}
|
|
return removed;
|
|
}
|
|
/**
|
|
* The top-most (i.e. currently active) pane in the layers.
|
|
*/
|
|
get top() {
|
|
return this.activePane;
|
|
}
|
|
set top(pane) {
|
|
const { activePane: oldTop } = this;
|
|
if (oldTop !== void 0) {
|
|
this.setPaneVariables(oldTop, false);
|
|
oldTop.onClose(false);
|
|
}
|
|
const newPane = this.activePane = pane;
|
|
this.setPaneVariables(newPane, true);
|
|
newPane.onReady();
|
|
this.doDisplay(true);
|
|
}
|
|
doDisplay(renderControls) {
|
|
const { activePane, titleEl, navEl, containerEl } = this;
|
|
if (activePane === void 0) {
|
|
return;
|
|
}
|
|
navEl.empty();
|
|
if (this.layers.length > 0) {
|
|
new import_obsidian5.ButtonComponent(this.navEl).setIcon("lucide-arrow-left-circle").setClass("clickable-icon").setTooltip(`Back to ${this.layers[this.layers.length - 1].title}`).onClick(() => this.navInstance.close());
|
|
}
|
|
titleEl.empty();
|
|
const { title } = activePane;
|
|
if (typeof title === "string") {
|
|
titleEl.createEl("h2", { text: title });
|
|
} else {
|
|
titleEl.createEl("h2", { text: title.title });
|
|
titleEl.createEl("h3", { text: title.subtitle });
|
|
}
|
|
if (renderControls) {
|
|
this.controlsEl.empty();
|
|
activePane.displayControls();
|
|
}
|
|
containerEl.empty();
|
|
activePane.display();
|
|
}
|
|
setPaneVariables(pane, attached) {
|
|
const notAttachedError = () => {
|
|
throw new Error("Not attached");
|
|
};
|
|
Object.defineProperties(pane, {
|
|
nav: {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: attached ? () => this.navInstance : notAttachedError
|
|
},
|
|
containerEl: {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: attached ? () => this.containerEl : notAttachedError
|
|
},
|
|
controlsEl: {
|
|
configurable: true,
|
|
enumerable: true,
|
|
get: attached ? () => this.controlsEl : notAttachedError
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/ui/paned-setting-tab.ts
|
|
var UISettingTab = class extends import_obsidian6.PluginSettingTab {
|
|
constructor(plugin, createDefault) {
|
|
super(plugin.app, plugin);
|
|
this.plugin = plugin;
|
|
this.createDefault = createDefault;
|
|
this.initLayer = null;
|
|
this.layers = new UIPaneLayers({
|
|
close: () => closeSettings(this.app)
|
|
});
|
|
}
|
|
openWithPane(pane) {
|
|
this.initLayer = pane;
|
|
openPluginSettings(this.plugin.app, this.plugin);
|
|
}
|
|
/** @override */
|
|
hide() {
|
|
this.initLayer = null;
|
|
this.layers.clear();
|
|
super.hide();
|
|
}
|
|
display() {
|
|
const { containerEl, layers } = this;
|
|
containerEl.empty();
|
|
containerEl.classList.add("calloutmanager-setting-tab", "calloutmanager-pane");
|
|
const headerEl = containerEl.createDiv({ cls: "calloutmanager-setting-tab-header" });
|
|
layers.navEl = headerEl.createDiv({ cls: "calloutmanager-setting-tab-nav" });
|
|
layers.titleEl = headerEl.createDiv({ cls: "calloutmanager-setting-tab-title" });
|
|
const controlsEl = headerEl.createDiv({ cls: "calloutmanager-setting-tab-controls" });
|
|
layers.controlsEl = controlsEl.createDiv();
|
|
layers.scrollEl = containerEl.createDiv({
|
|
cls: "calloutmanager-setting-tab-viewport vertical-tab-content"
|
|
});
|
|
layers.containerEl = layers.scrollEl.createDiv({ cls: "calloutmanager-setting-tab-content" });
|
|
controlsEl.createDiv({ cls: "modal-close-button" }, (closeButtonEl) => {
|
|
closeButtonEl.addEventListener("click", (ev) => {
|
|
if (!ev.isTrusted)
|
|
return;
|
|
closeSettings(this.app);
|
|
});
|
|
});
|
|
layers.clear();
|
|
const initLayer = this.initLayer ?? this.createDefault();
|
|
this.initLayer = null;
|
|
layers.top = initLayer;
|
|
}
|
|
};
|
|
|
|
// src/api-common.ts
|
|
var emitter = Symbol("emitter");
|
|
var destroy = Symbol("destroy");
|
|
|
|
// src/api-v1.ts
|
|
var import_obsidian7 = require("obsidian");
|
|
|
|
// src/util/color.ts
|
|
function toHSV(color) {
|
|
if ("h" in color && "s" in color && "v" in color)
|
|
return color;
|
|
const rFloat = color.r / 255;
|
|
const gFloat = color.g / 255;
|
|
const bFloat = color.b / 255;
|
|
const cmax = Math.max(rFloat, gFloat, bFloat);
|
|
const cmin = Math.min(rFloat, gFloat, bFloat);
|
|
const delta = cmax - cmin;
|
|
let h = 0;
|
|
if (cmax !== cmin) {
|
|
switch (cmax) {
|
|
case rFloat:
|
|
h = (60 * ((gFloat - bFloat) / delta) + 360) % 360;
|
|
break;
|
|
case gFloat:
|
|
h = (60 * ((bFloat - rFloat) / delta) + 120) % 360;
|
|
break;
|
|
case bFloat:
|
|
h = (60 * ((rFloat - gFloat) / delta) + 240) % 360;
|
|
break;
|
|
}
|
|
}
|
|
const s = cmax === 0 ? 0 : delta / cmax * 100;
|
|
const v = cmax * 100;
|
|
const hsv = { h, s, v };
|
|
if ("a" in color) {
|
|
hsv.a = color.a / 255 * 100;
|
|
}
|
|
return hsv;
|
|
}
|
|
function toHexRGB(color) {
|
|
const parts = [color.r, color.g, color.b, ..."a" in color ? [color.a] : []];
|
|
return parts.map((c) => c.toString(16).padStart(2, "0")).join("");
|
|
}
|
|
var REGEX_RGB = /^\s*rgba?\(\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?)\s*[, ]\s*([\d.]+%?\s*)\)\s*$/i;
|
|
function parseColorRGB(rgb) {
|
|
const matches2 = REGEX_RGB.exec(rgb);
|
|
if (matches2 === null)
|
|
return null;
|
|
const components = matches2.slice(1).map((v) => v.trim());
|
|
const rgbComponents = rgbComponentStringsToNumber(components);
|
|
if (rgbComponents === null) {
|
|
return null;
|
|
}
|
|
if (void 0 !== rgbComponents.find((v) => isNaN(v) || v < 0 || v > 255)) {
|
|
return null;
|
|
}
|
|
return {
|
|
r: rgbComponents[0],
|
|
g: rgbComponents[1],
|
|
b: rgbComponents[2]
|
|
};
|
|
}
|
|
function rgbComponentStringsToNumber(components) {
|
|
if (components[0].endsWith("%")) {
|
|
if (void 0 !== components.slice(1, 3).find((c) => !c.endsWith("%"))) {
|
|
return null;
|
|
}
|
|
return components.map((v) => parseFloat(v.substring(0, v.length - 1))).map((v) => Math.floor(v * 255 / 100));
|
|
}
|
|
if (void 0 !== components.slice(1, 3).find((c) => c.endsWith("%"))) {
|
|
return null;
|
|
}
|
|
return components.map((v) => parseInt(v, 10));
|
|
}
|
|
|
|
// src/callout-util.ts
|
|
function getColorFromCallout(callout) {
|
|
return parseColorRGB(`rgb(${callout.color})`);
|
|
}
|
|
function getTitleFromCallout(callout) {
|
|
const matches2 = /^(.)(.*)/u.exec(callout.id);
|
|
if (matches2 == null)
|
|
return callout.id;
|
|
const firstChar = matches2[1].toLocaleUpperCase();
|
|
const remainingChars = matches2[2].toLocaleLowerCase().replace(/-+/g, " ");
|
|
return `${firstChar}${remainingChars}`;
|
|
}
|
|
|
|
// src/api-v1.ts
|
|
var CalloutManagerAPI_V1 = class {
|
|
constructor(plugin, consumer) {
|
|
this.plugin = plugin;
|
|
this.consumer = consumer;
|
|
this[emitter] = new import_obsidian7.Events();
|
|
if (consumer != null) {
|
|
console.debug("Created API V1 Handle:", { plugin: consumer.manifest.id });
|
|
}
|
|
}
|
|
/**
|
|
* Called to destroy an API handle bound to a consumer.
|
|
*/
|
|
[(emitter, destroy)]() {
|
|
const consumer = this.consumer;
|
|
console.debug("Destroyed API V1 Handle:", { plugin: consumer.manifest.id });
|
|
}
|
|
/** @override */
|
|
getCallouts() {
|
|
return this.plugin.callouts.values().map((callout) => Object.freeze({ ...callout }));
|
|
}
|
|
/** @override */
|
|
getColor(callout) {
|
|
const color = getColorFromCallout(callout);
|
|
return color ?? { invalid: callout.color };
|
|
}
|
|
/** @override */
|
|
getTitle(callout) {
|
|
return getTitleFromCallout(callout);
|
|
}
|
|
/** @override */
|
|
on(event, listener) {
|
|
if (this.consumer == null) {
|
|
throw new Error("Cannot listen for events without an API consumer.");
|
|
}
|
|
this[emitter].on(event, listener);
|
|
}
|
|
/** @override */
|
|
off(event, listener) {
|
|
if (this.consumer == null) {
|
|
throw new Error("Cannot listen for events without an API consumer.");
|
|
}
|
|
this[emitter].off(event, listener);
|
|
}
|
|
};
|
|
|
|
// src/apis.ts
|
|
var CalloutManagerAPIs = class {
|
|
constructor(plugin) {
|
|
this.plugin = plugin;
|
|
this.handles = /* @__PURE__ */ new Map();
|
|
}
|
|
/**
|
|
* Creates (or gets) an instance of the Callout Manager API for a plugin.
|
|
* If the plugin is undefined, only trivial functions are available.
|
|
*
|
|
* @param version The API version.
|
|
* @param consumerPlugin The plugin using the API.
|
|
*
|
|
* @internal
|
|
*/
|
|
async newHandle(version, consumerPlugin, cleanupFunc) {
|
|
if (version !== "v1")
|
|
throw new Error(`Unsupported Callout Manager API: ${version}`);
|
|
if (consumerPlugin == null) {
|
|
return new CalloutManagerAPI_V1(this.plugin, void 0);
|
|
}
|
|
const existing = this.handles.get(consumerPlugin);
|
|
if (existing != null) {
|
|
return existing;
|
|
}
|
|
consumerPlugin.register(cleanupFunc);
|
|
const handle = new CalloutManagerAPI_V1(this.plugin, consumerPlugin);
|
|
this.handles.set(consumerPlugin, handle);
|
|
return handle;
|
|
}
|
|
/**
|
|
* Destroys an API handle created by {@link newHandle}.
|
|
*
|
|
* @param version The API version.
|
|
* @param consumerPlugin The plugin using the API.
|
|
*
|
|
* @internal
|
|
*/
|
|
destroyHandle(version, consumerPlugin) {
|
|
if (version !== "v1")
|
|
throw new Error(`Unsupported Callout Manager API: ${version}`);
|
|
const handle = this.handles.get(consumerPlugin);
|
|
if (handle == null)
|
|
return;
|
|
handle[destroy]();
|
|
this.handles.delete(consumerPlugin);
|
|
}
|
|
emitEventForCalloutChange(id) {
|
|
for (const handle of this.handles.values()) {
|
|
handle[emitter].trigger("change");
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/callout-collection.ts
|
|
var CalloutCollection = class {
|
|
constructor(resolver) {
|
|
this.resolver = resolver;
|
|
this.invalidated = /* @__PURE__ */ new Set();
|
|
this.invalidationCount = 0;
|
|
this.cacheById = /* @__PURE__ */ new Map();
|
|
this.cached = false;
|
|
this.snippets = new CalloutCollectionSnippets(this.invalidateSource.bind(this));
|
|
this.builtin = new CalloutCollectionObsidian(this.invalidateSource.bind(this));
|
|
this.theme = new CalloutCollectionTheme(this.invalidateSource.bind(this));
|
|
this.custom = new CalloutCollectionCustom(this.invalidateSource.bind(this));
|
|
}
|
|
get(id) {
|
|
if (!this.cached)
|
|
this.buildCache();
|
|
const cached = this.cacheById.get(id);
|
|
if (cached === void 0) {
|
|
return void 0;
|
|
}
|
|
if (this.invalidated.has(cached)) {
|
|
this.resolveOne(cached);
|
|
}
|
|
return cached.callout;
|
|
}
|
|
/**
|
|
* Checks if a callout with this ID is in the collection.
|
|
* @param id The callout ID.
|
|
* @returns True if the callout is in the collection.
|
|
*/
|
|
has(id) {
|
|
if (!this.cached)
|
|
this.buildCache();
|
|
return this.cacheById.has(id);
|
|
}
|
|
/**
|
|
* Gets all the known {@link CalloutID callout IDs}.
|
|
* @returns The callout IDs.
|
|
*/
|
|
keys() {
|
|
if (!this.cached)
|
|
this.buildCache();
|
|
return Array.from(this.cacheById.keys());
|
|
}
|
|
/**
|
|
* Gets all the known {@link Callout callouts}.
|
|
* @returns The callouts.
|
|
*/
|
|
values() {
|
|
if (!this.cached)
|
|
this.buildCache();
|
|
this.resolveAll();
|
|
return Array.from(this.cacheById.values()).map((c) => c.callout);
|
|
}
|
|
/**
|
|
* Returns a function that will return `true` if the collection has changed since the function was created.
|
|
* @returns The function.
|
|
*/
|
|
hasChanged() {
|
|
const countSnapshot = this.invalidationCount;
|
|
return () => this.invalidationCount !== countSnapshot;
|
|
}
|
|
/**
|
|
* Resolves the settings of a callout.
|
|
* This removes it from the set of invalidated callout caches.
|
|
*
|
|
* @param cached The callout's cache entry.
|
|
*/
|
|
resolveOne(cached) {
|
|
this.doResolve(cached);
|
|
this.invalidated.delete(cached);
|
|
}
|
|
/**
|
|
* Resolves the settings of all callouts.
|
|
*/
|
|
resolveAll() {
|
|
for (const cached of this.invalidated.values()) {
|
|
this.doResolve(cached);
|
|
}
|
|
this.invalidated.clear();
|
|
}
|
|
doResolve(cached) {
|
|
cached.callout = this.resolver(cached.id);
|
|
cached.callout.sources = Array.from(cached.sources.values()).map(sourceFromKey);
|
|
}
|
|
/**
|
|
* Builds the initial cache of callouts.
|
|
* This creates the cache entries and associates them to a source.
|
|
*/
|
|
buildCache() {
|
|
this.invalidated.clear();
|
|
this.cacheById.clear();
|
|
{
|
|
const source = sourceToKey({ type: "builtin" });
|
|
for (const callout of this.builtin.get()) {
|
|
this.addCalloutSource(callout, source);
|
|
}
|
|
}
|
|
if (this.theme.theme != null) {
|
|
const source = sourceToKey({ type: "theme", theme: this.theme.theme });
|
|
for (const callout of this.theme.get()) {
|
|
this.addCalloutSource(callout, source);
|
|
}
|
|
}
|
|
for (const snippet of this.snippets.keys()) {
|
|
const source = sourceToKey({ type: "snippet", snippet });
|
|
for (const callout of this.snippets.get(snippet)) {
|
|
this.addCalloutSource(callout, source);
|
|
}
|
|
}
|
|
{
|
|
const source = sourceToKey({ type: "custom" });
|
|
for (const callout of this.custom.keys()) {
|
|
this.addCalloutSource(callout, source);
|
|
}
|
|
}
|
|
this.cached = true;
|
|
}
|
|
/**
|
|
* Marks a callout as invalidated.
|
|
* This forces the callout to be resolved again.
|
|
*
|
|
* @param id The callout ID.
|
|
*/
|
|
invalidate(id) {
|
|
if (!this.cached)
|
|
return;
|
|
const callout = this.cacheById.get(id);
|
|
if (callout !== void 0) {
|
|
console.debug("Invalided Callout Cache:", id);
|
|
this.invalidated.add(callout);
|
|
}
|
|
}
|
|
addCalloutSource(id, sourceKey) {
|
|
let callout = this.cacheById.get(id);
|
|
if (callout == null) {
|
|
callout = new CachedCallout(id);
|
|
this.cacheById.set(id, callout);
|
|
}
|
|
callout.sources.add(sourceKey);
|
|
this.invalidated.add(callout);
|
|
}
|
|
removeCalloutSource(id, sourceKey) {
|
|
const callout = this.cacheById.get(id);
|
|
if (callout == null) {
|
|
return;
|
|
}
|
|
callout.sources.delete(sourceKey);
|
|
if (callout.sources.size === 0) {
|
|
this.cacheById.delete(id);
|
|
this.invalidated.delete(callout);
|
|
}
|
|
}
|
|
/**
|
|
* Called whenever a callout source has any changes.
|
|
* This will add or remove callouts from the cache, or invalidate a callout to mark it as requiring re-resolving.
|
|
*
|
|
* @param src The source that changed.
|
|
* @param data A diff of changes.
|
|
*/
|
|
invalidateSource(src, data) {
|
|
const sourceKey = sourceToKey(src);
|
|
if (!this.cached) {
|
|
return;
|
|
}
|
|
for (const removed of data.removed) {
|
|
this.removeCalloutSource(removed, sourceKey);
|
|
}
|
|
for (const added of data.added) {
|
|
this.addCalloutSource(added, sourceKey);
|
|
}
|
|
for (const changed of data.changed) {
|
|
const callout = this.cacheById.get(changed);
|
|
if (callout != null) {
|
|
this.invalidated.add(callout);
|
|
}
|
|
}
|
|
this.invalidationCount++;
|
|
}
|
|
};
|
|
var CachedCallout = class {
|
|
constructor(id) {
|
|
this.id = id;
|
|
this.sources = /* @__PURE__ */ new Set();
|
|
this.callout = null;
|
|
}
|
|
};
|
|
var CalloutCollectionSnippets = class {
|
|
constructor(invalidate) {
|
|
this.data = /* @__PURE__ */ new Map();
|
|
this.invalidate = invalidate;
|
|
}
|
|
get(id) {
|
|
const value = this.data.get(id);
|
|
if (value === void 0) {
|
|
return void 0;
|
|
}
|
|
return Array.from(value.values());
|
|
}
|
|
set(id, callouts) {
|
|
const source = { type: "snippet", snippet: id };
|
|
const old = this.data.get(id);
|
|
const updated = new Set(callouts);
|
|
this.data.set(id, updated);
|
|
if (old === void 0) {
|
|
this.invalidate(source, { added: callouts, changed: [], removed: [] });
|
|
return;
|
|
}
|
|
const diffs = diff(old, updated);
|
|
this.invalidate(source, {
|
|
added: diffs.added,
|
|
removed: diffs.removed,
|
|
changed: diffs.same
|
|
});
|
|
}
|
|
delete(id) {
|
|
const old = this.data.get(id);
|
|
const deleted = this.data.delete(id);
|
|
if (old !== void 0) {
|
|
this.invalidate(
|
|
{ type: "snippet", snippet: id },
|
|
{
|
|
added: [],
|
|
changed: [],
|
|
removed: Array.from(old.keys())
|
|
}
|
|
);
|
|
}
|
|
return deleted;
|
|
}
|
|
clear() {
|
|
for (const id of Array.from(this.data.keys())) {
|
|
this.delete(id);
|
|
}
|
|
}
|
|
keys() {
|
|
return Array.from(this.data.keys());
|
|
}
|
|
};
|
|
var CalloutCollectionObsidian = class {
|
|
constructor(invalidate) {
|
|
this.data = /* @__PURE__ */ new Set();
|
|
this.invalidate = invalidate;
|
|
}
|
|
set(callouts) {
|
|
const old = this.data;
|
|
const updated = this.data = new Set(callouts);
|
|
const diffs = diff(old, updated);
|
|
this.invalidate(
|
|
{ type: "builtin" },
|
|
{
|
|
added: diffs.added,
|
|
removed: diffs.removed,
|
|
changed: diffs.same
|
|
}
|
|
);
|
|
}
|
|
get() {
|
|
return Array.from(this.data.values());
|
|
}
|
|
};
|
|
var CalloutCollectionTheme = class {
|
|
constructor(invalidate) {
|
|
this.data = /* @__PURE__ */ new Set();
|
|
this.invalidate = invalidate;
|
|
this.oldTheme = "";
|
|
}
|
|
get theme() {
|
|
return this.oldTheme;
|
|
}
|
|
set(theme, callouts) {
|
|
const old = this.data;
|
|
const oldTheme = this.oldTheme;
|
|
const updated = this.data = new Set(callouts);
|
|
this.oldTheme = theme;
|
|
if (this.oldTheme === theme) {
|
|
const diffs = diff(old, updated);
|
|
this.invalidate(
|
|
{ type: "theme", theme },
|
|
{
|
|
added: diffs.added,
|
|
removed: diffs.removed,
|
|
changed: diffs.same
|
|
}
|
|
);
|
|
return;
|
|
}
|
|
this.invalidate(
|
|
{ type: "theme", theme: oldTheme ?? "" },
|
|
{
|
|
added: [],
|
|
removed: Array.from(old.values()),
|
|
changed: []
|
|
}
|
|
);
|
|
this.invalidate(
|
|
{ type: "theme", theme },
|
|
{
|
|
added: callouts,
|
|
removed: [],
|
|
changed: []
|
|
}
|
|
);
|
|
}
|
|
delete() {
|
|
const old = this.data;
|
|
const oldTheme = this.oldTheme;
|
|
this.data = /* @__PURE__ */ new Set();
|
|
this.oldTheme = null;
|
|
this.invalidate(
|
|
{ type: "theme", theme: oldTheme ?? "" },
|
|
{
|
|
added: [],
|
|
removed: Array.from(old.values()),
|
|
changed: []
|
|
}
|
|
);
|
|
}
|
|
get() {
|
|
return Array.from(this.data.values());
|
|
}
|
|
};
|
|
var CalloutCollectionCustom = class {
|
|
constructor(invalidate) {
|
|
this.data = [];
|
|
this.invalidate = invalidate;
|
|
}
|
|
has(id) {
|
|
return void 0 !== this.data.find((existingId) => existingId === id);
|
|
}
|
|
add(...ids) {
|
|
const set = new Set(this.data);
|
|
const added = [];
|
|
for (const id of ids) {
|
|
if (!set.has(id)) {
|
|
added.push(id);
|
|
set.add(id);
|
|
this.data.push(id);
|
|
}
|
|
}
|
|
if (added.length > 0) {
|
|
this.invalidate({ type: "custom" }, { added, removed: [], changed: [] });
|
|
}
|
|
}
|
|
delete(...ids) {
|
|
const { data } = this;
|
|
const removed = [];
|
|
for (const id of ids) {
|
|
const index = data.findIndex((existingId) => id === existingId);
|
|
if (index !== void 0) {
|
|
data.splice(index, 1);
|
|
removed.push(id);
|
|
}
|
|
}
|
|
if (removed.length > 0) {
|
|
this.invalidate({ type: "custom" }, { added: [], removed, changed: [] });
|
|
}
|
|
}
|
|
keys() {
|
|
return this.data.slice(0);
|
|
}
|
|
clear() {
|
|
const removed = this.data;
|
|
this.data = [];
|
|
this.invalidate({ type: "custom" }, { added: [], removed, changed: [] });
|
|
}
|
|
};
|
|
function diff(before, after) {
|
|
const added = [];
|
|
const removed = [];
|
|
const same = [];
|
|
for (const item of before.values()) {
|
|
(after.has(item) ? same : removed).push(item);
|
|
}
|
|
for (const item of after.values()) {
|
|
if (!before.has(item)) {
|
|
added.push(item);
|
|
}
|
|
}
|
|
return { added, removed, same };
|
|
}
|
|
function sourceToKey(source) {
|
|
switch (source.type) {
|
|
case "builtin":
|
|
return "builtin";
|
|
case "snippet":
|
|
return `snippet:${source.snippet}`;
|
|
case "theme":
|
|
return `theme:${source.theme}`;
|
|
case "custom":
|
|
return `custom`;
|
|
}
|
|
}
|
|
function sourceFromKey(sourceKey) {
|
|
if (sourceKey === "builtin") {
|
|
return { type: "builtin" };
|
|
}
|
|
if (sourceKey === "custom") {
|
|
return { type: "custom" };
|
|
}
|
|
if (sourceKey.startsWith("snippet:")) {
|
|
return { type: "snippet", snippet: sourceKey.substring("snippet:".length) };
|
|
}
|
|
if (sourceKey.startsWith("theme:")) {
|
|
return { type: "theme", theme: sourceKey.substring("theme:".length) };
|
|
}
|
|
throw new Error("Unknown source key: " + sourceKey);
|
|
}
|
|
|
|
// src/ui/component/callout-preview.ts
|
|
var import_obsidian8 = require("obsidian");
|
|
var NO_ATTACH = Symbol();
|
|
var CalloutPreviewComponent = class extends import_obsidian8.Component {
|
|
constructor(containerEl, options) {
|
|
super();
|
|
const { color, icon, id, title, content } = options;
|
|
const frag = document.createDocumentFragment();
|
|
const calloutEl = this.calloutEl = frag.createDiv({ cls: ["callout", "calloutmanager-preview"] });
|
|
const titleElContainer = calloutEl.createDiv({ cls: "callout-title" });
|
|
this.iconEl = titleElContainer.createDiv({ cls: "callout-icon" });
|
|
const titleEl = this.titleEl = titleElContainer.createDiv({ cls: "callout-title-inner" });
|
|
const contentEl = this.contentEl = content === void 0 ? void 0 : calloutEl.createDiv({ cls: "callout-content" });
|
|
this.setIcon(icon);
|
|
this.setColor(color);
|
|
this.setCalloutID(id);
|
|
if (title == null)
|
|
titleEl.textContent = id;
|
|
else if (typeof title === "function")
|
|
title(titleEl);
|
|
else if (typeof title === "string")
|
|
titleEl.textContent = title;
|
|
else
|
|
titleEl.appendChild(title);
|
|
if (contentEl != null) {
|
|
if (typeof content === "function")
|
|
content(contentEl);
|
|
else if (typeof content === "string")
|
|
contentEl.textContent = content;
|
|
else
|
|
contentEl.appendChild(content);
|
|
}
|
|
if (containerEl != NO_ATTACH) {
|
|
CalloutPreviewComponent.prototype.attachTo.call(this, containerEl);
|
|
}
|
|
}
|
|
/**
|
|
* Changes the callout ID.
|
|
* This will *not* change the appearance of the preview.
|
|
*
|
|
* @param id The new ID to use.
|
|
*/
|
|
setCalloutID(id) {
|
|
const { calloutEl } = this;
|
|
calloutEl.setAttribute("data-callout", id);
|
|
return this;
|
|
}
|
|
/**
|
|
* Changes the callout icon.
|
|
*
|
|
* @param icon The ID of the new icon to use.
|
|
*/
|
|
setIcon(icon) {
|
|
const { iconEl, calloutEl } = this;
|
|
calloutEl.style.setProperty("--callout-icon", icon);
|
|
iconEl.empty();
|
|
const iconSvg = (0, import_obsidian8.getIcon)(icon);
|
|
if (iconSvg != null) {
|
|
this.iconEl.appendChild(iconSvg);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Changes the callout color.
|
|
*
|
|
* @param color The color to use.
|
|
*/
|
|
setColor(color) {
|
|
const { calloutEl } = this;
|
|
if (color == null) {
|
|
calloutEl.style.removeProperty("--callout-color");
|
|
return this;
|
|
}
|
|
calloutEl.style.setProperty("--callout-color", `${color.r}, ${color.g}, ${color.b}`);
|
|
return this;
|
|
}
|
|
/**
|
|
* Attaches the callout preview to a DOM element.
|
|
* This places it at the end of the element.
|
|
*
|
|
* @param containerEl The container to attach to.
|
|
*/
|
|
attachTo(containerEl) {
|
|
containerEl.appendChild(this.calloutEl);
|
|
return this;
|
|
}
|
|
/**
|
|
* Resets the `--callout-color` and `--callout-icon` CSS properties added to the callout element.
|
|
*/
|
|
resetStylePropertyOverrides() {
|
|
const { calloutEl } = this;
|
|
calloutEl.style.removeProperty("--callout-color");
|
|
calloutEl.style.removeProperty("--callout-icon");
|
|
}
|
|
};
|
|
var IsolatedCalloutPreviewComponent = class extends CalloutPreviewComponent {
|
|
constructor(containerEl, options) {
|
|
super(NO_ATTACH, options);
|
|
const frag = document.createDocumentFragment();
|
|
const focused = options.focused ?? false;
|
|
const colorScheme = options.colorScheme;
|
|
const readingView = (options.viewType ?? "reading") === "reading";
|
|
const cssEls = options?.cssEls ?? getCurrentStyles(containerEl?.doc);
|
|
const shadowHostEl = this.shadowHostEl = frag.createDiv();
|
|
const shadowRoot = this.shadowRoot = shadowHostEl.attachShadow({ delegatesFocus: false, mode: "closed" });
|
|
const shadowHead = this.shadowHead = shadowRoot.createEl("head");
|
|
const shadowBody = this.shadowBody = shadowRoot.createEl("body");
|
|
const styleEls = this.styleEls = [];
|
|
for (const cssEl of cssEls) {
|
|
const cssElClone = cssEl.cloneNode(true);
|
|
if (cssEl.tagName === "STYLE") {
|
|
styleEls.push(cssElClone);
|
|
}
|
|
shadowHead.appendChild(cssElClone);
|
|
}
|
|
shadowHead.createEl("style", { text: SHADOW_DOM_RESET_STYLES });
|
|
this.customStyleEl = shadowHead.createEl("style", { attr: { "data-custom-styles": "true" } });
|
|
shadowBody.classList.add(`theme-${colorScheme}`, "obsidian-app");
|
|
const viewContentEl = shadowBody.createDiv({ cls: "app-container" }).createDiv({ cls: "horizontal-main-container" }).createDiv({ cls: "workspace" }).createDiv({ cls: "workspace-split mod-root" }).createDiv({ cls: `workspace-tabs ${focused ? "mod-active" : ""}` }).createDiv({ cls: "workspace-tab-container" }).createDiv({ cls: `workspace-leaf ${focused ? "mod-active" : ""}` }).createDiv({ cls: "workspace-leaf-content" }).createDiv({ cls: "view-content" });
|
|
const calloutParentEl = readingView ? createReadingViewContainer(viewContentEl) : createLiveViewContainer(viewContentEl);
|
|
calloutParentEl.appendChild(this.calloutEl);
|
|
if (containerEl != null) {
|
|
IsolatedCalloutPreviewComponent.prototype.attachTo.call(this, containerEl);
|
|
}
|
|
}
|
|
/**
|
|
* Replaces the `<style>` elements used by the isolated callout preview with the latest ones.
|
|
*/
|
|
updateStyles() {
|
|
return this.updateStylesWith(
|
|
getCurrentStyles(this.shadowHostEl.doc).filter((e) => e.tagName === "STYLE").map((e) => e.cloneNode(true))
|
|
);
|
|
}
|
|
/**
|
|
* Replaces the `<style>` elements used by the isolated callout preview.
|
|
* This can be used to update the preview with the latest styles.
|
|
*
|
|
* @param styleEls The new style elements to use. These will *not* be cloned.
|
|
*/
|
|
updateStylesWith(styleEls) {
|
|
const { styleEls: oldStyleEls, customStyleEl } = this;
|
|
let i, end;
|
|
let lastNode = customStyleEl.previousSibling;
|
|
for (i = 0, end = Math.min(styleEls.length, oldStyleEls.length); i < end; i++) {
|
|
const el = styleEls[i];
|
|
oldStyleEls[i].replaceWith(el);
|
|
lastNode = el;
|
|
}
|
|
for (end = styleEls.length; i < end; i++) {
|
|
const el = styleEls[i];
|
|
lastNode.insertAdjacentElement("afterend", el);
|
|
oldStyleEls.push(el);
|
|
}
|
|
const toRemove = oldStyleEls.splice(i, oldStyleEls.length - i);
|
|
for (const node of toRemove) {
|
|
node.remove();
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Removes matching style elements.
|
|
* @param predicate The predicate function. If it returns true, the element is removed.
|
|
*/
|
|
removeStyles(predicate) {
|
|
for (let i = 0; i < this.styleEls.length; i++) {
|
|
const el = this.styleEls[i];
|
|
if (predicate(el)) {
|
|
el.remove();
|
|
this.styleEls.splice(i, 1);
|
|
i--;
|
|
}
|
|
}
|
|
}
|
|
/**
|
|
* Changes the color scheme.
|
|
* @param colorScheme The color scheme to use.
|
|
*/
|
|
setColorScheme(colorScheme) {
|
|
const { classList } = this.shadowBody;
|
|
classList.toggle("theme-dark", colorScheme === "dark");
|
|
classList.toggle("theme-light", colorScheme === "light");
|
|
return this;
|
|
}
|
|
/**
|
|
* Attaches the callout preview to a DOM element.
|
|
* This places it at the end of the element.
|
|
*
|
|
* @param containerEl The container to attach to.
|
|
* @override
|
|
*/
|
|
attachTo(containerEl) {
|
|
containerEl.appendChild(this.shadowHostEl);
|
|
return this;
|
|
}
|
|
};
|
|
function getCurrentStyles(doc) {
|
|
const els = [];
|
|
let node = (doc ?? window.document).head.firstElementChild;
|
|
for (; node != null; node = node.nextElementSibling) {
|
|
const nodeTag = node.tagName;
|
|
if (nodeTag === "STYLE" || nodeTag === "LINK" && node.getAttribute("rel")?.toLowerCase() === "stylesheet") {
|
|
els.push(node);
|
|
}
|
|
}
|
|
return els;
|
|
}
|
|
function createReadingViewContainer(viewContentEl) {
|
|
return viewContentEl.createDiv({ cls: "markdown-reading-view" }).createDiv({ cls: "markdown-preview-view markdown-rendered" }).createDiv({ cls: "markdown-preview-section" }).createDiv();
|
|
}
|
|
function createLiveViewContainer(viewContentEl) {
|
|
return viewContentEl.createDiv({ cls: "markdown-source-view cm-s-obsidian mod-cm6 is-live-preview" }).createDiv({ cls: "cm-editor \u037C1 \u037C2 \u037Cq" }).createDiv({ cls: "cm-scroller" }).createDiv({ cls: "cm-sizer" }).createDiv({ cls: "cm-contentContainer" }).createDiv({ cls: "cm-content" }).createDiv({ cls: "cm-embed-block markdown-rendered cm-callout" });
|
|
}
|
|
var SHADOW_DOM_RESET_STYLES = `
|
|
/* Reset layout and stylings for all properties up to the callout. */
|
|
.app-container,
|
|
.horizontal-main-container,
|
|
.workspace,
|
|
.workspace-split,
|
|
.workspace-tabs,
|
|
.workspace-tab-container,
|
|
.workspace-leaf,
|
|
.workspace-leaf-content,
|
|
.view-content,
|
|
.markdown-reading-view,
|
|
.markdown-source-view,
|
|
.cm-editor.\u037C1.\u037C2.\u037Cq,
|
|
.cm-editor.\u037C1.\u037C2.\u037Cq > .cm-scroller,
|
|
.cm-sizer,
|
|
.cm-contentContainer,
|
|
.cm-content,
|
|
.markdown-preview-view {
|
|
all: initial !important;
|
|
display: block !important;
|
|
}
|
|
|
|
/* Set the text color of the container for the callout. */
|
|
.markdown-preview-section,
|
|
.cm-callout {
|
|
color: var(--text-normal) !important;
|
|
}
|
|
|
|
/* Override margin on callout to keep the preview as small as possible. */
|
|
.markdown-preview-section > div > .callout,
|
|
.cm-callout > .callout,
|
|
.calloutmanager-preview.callout {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
/* Set the font properties of the callout. */
|
|
.cm-callout,
|
|
.callout {
|
|
font-size: var(--font-text-size) !important;
|
|
font-family: var(--font-text) !important;
|
|
line-height: var(--line-height-normal) !important;
|
|
}
|
|
|
|
/* Use transparent background color. */
|
|
body {
|
|
background-color: transparent !important;
|
|
}
|
|
`;
|
|
|
|
// src/callout-resolver.ts
|
|
var CalloutResolver = class {
|
|
constructor() {
|
|
this.hostElement = document.body.createDiv({
|
|
cls: "calloutmanager-callout-resolver"
|
|
});
|
|
this.hostElement.style.setProperty("display", "none", "important");
|
|
this.calloutPreview = new IsolatedCalloutPreviewComponent(this.hostElement, {
|
|
id: "",
|
|
icon: "",
|
|
colorScheme: "dark"
|
|
});
|
|
this.calloutPreview.resetStylePropertyOverrides();
|
|
}
|
|
/**
|
|
* Reloads the styles of the callout resolver.
|
|
* This is necessary to get up-to-date styles when the application CSS changes.
|
|
*
|
|
* Note: This will not reload the Obsidian app.css stylesheet.
|
|
* @param styles The new style elements to use.
|
|
*/
|
|
reloadStyles() {
|
|
this.calloutPreview.setColorScheme(getCurrentThemeID2(app));
|
|
this.calloutPreview.updateStyles();
|
|
this.calloutPreview.removeStyles((el) => el.getAttribute("data-callout-manager") === "style-overrides");
|
|
}
|
|
/**
|
|
* Removes the host element.
|
|
* This should be called when the plugin is unloading.
|
|
*/
|
|
unload() {
|
|
this.hostElement.remove();
|
|
}
|
|
/**
|
|
* Gets the computed styles for a given type of callout.
|
|
* This uses the current Obsidian styles, themes, and snippets.
|
|
*
|
|
* @param id The callout ID.
|
|
* @param callback A callback function to run. The styles may only be accessed through this.
|
|
* @returns Whatever the callback function returned.
|
|
*/
|
|
getCalloutStyles(id, callback) {
|
|
const { calloutEl } = this.calloutPreview;
|
|
calloutEl.setAttribute("data-callout", id);
|
|
return callback(window.getComputedStyle(calloutEl));
|
|
}
|
|
/**
|
|
* Gets the icon and color for a given type of callout.
|
|
* This uses the current Obsidian styles, themes, and snippets.
|
|
*
|
|
* @param id The callout ID.
|
|
* @returns The callout icon and color.
|
|
*/
|
|
getCalloutProperties(id) {
|
|
return this.getCalloutStyles(id, (styles) => ({
|
|
icon: styles.getPropertyValue("--callout-icon").trim(),
|
|
color: styles.getPropertyValue("--callout-color").trim()
|
|
}));
|
|
}
|
|
get customStyleEl() {
|
|
return this.calloutPreview.customStyleEl;
|
|
}
|
|
};
|
|
|
|
// src/callout-settings.ts
|
|
function currentCalloutEnvironment(app2) {
|
|
const theme = getCurrentThemeID(app2) ?? "<default>";
|
|
return {
|
|
theme,
|
|
colorScheme: getCurrentThemeID2(app2)
|
|
};
|
|
}
|
|
function calloutSettingsToCSS(id, settings, environment) {
|
|
const styles = calloutSettingsToStyles(settings, environment).join(";\n ");
|
|
if (styles.length === 0) {
|
|
return "";
|
|
}
|
|
return `.callout[data-callout="${id}"] {
|
|
` + styles + "\n}";
|
|
}
|
|
function calloutSettingsToStyles(settings, environment) {
|
|
const styles = [];
|
|
for (const setting of settings) {
|
|
if (!checkCondition(setting.condition, environment)) {
|
|
continue;
|
|
}
|
|
const { changes } = setting;
|
|
if (changes.color != null)
|
|
styles.push(`--callout-color: ${changes.color}`);
|
|
if (changes.icon != null)
|
|
styles.push(`--callout-icon: ${changes.icon}`);
|
|
if (changes.customStyles != null)
|
|
styles.push(changes.customStyles);
|
|
}
|
|
return styles;
|
|
}
|
|
function checkCondition(condition, environment) {
|
|
if (condition == null) {
|
|
return true;
|
|
}
|
|
if ("or" in condition && condition.or !== void 0) {
|
|
return condition.or.findIndex((p) => checkCondition(p, environment) === true) !== void 0;
|
|
}
|
|
if ("and" in condition && condition.and !== void 0) {
|
|
return condition.and.findIndex((p) => checkCondition(p, environment) === false) === void 0;
|
|
}
|
|
if ("theme" in condition && condition.theme === environment.theme) {
|
|
return true;
|
|
}
|
|
if ("colorScheme" in condition && condition.colorScheme === environment.colorScheme) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
function typeofCondition(condition) {
|
|
if (condition === void 0)
|
|
return void 0;
|
|
const hasOwnProperty = Object.prototype.hasOwnProperty.bind(condition);
|
|
if (hasOwnProperty("colorScheme"))
|
|
return "colorScheme";
|
|
if (hasOwnProperty("theme"))
|
|
return "theme";
|
|
if (hasOwnProperty("and"))
|
|
return "and";
|
|
if (hasOwnProperty("or"))
|
|
return "or";
|
|
throw new Error(`Unsupported condition: ${JSON.stringify(condition)}`);
|
|
}
|
|
|
|
// src/css-parser.ts
|
|
function getCalloutsFromCSS(css) {
|
|
const REGEX_CALLOUT_SELECTOR = /\[data-callout([^\]]*)\]/gmi;
|
|
const REGEX_MATCH_QUOTED_STRING = {
|
|
"'": /^'([^']+)'( i)?$/,
|
|
'"': /^"([^"]+)"( i)?$/,
|
|
"": /^([^\]]+)$/
|
|
};
|
|
const attributeSelectors = [];
|
|
let matches2;
|
|
while ((matches2 = REGEX_CALLOUT_SELECTOR.exec(css)) != null) {
|
|
attributeSelectors.push(matches2[1]);
|
|
REGEX_CALLOUT_SELECTOR.lastIndex = matches2.index + matches2[0].length;
|
|
}
|
|
const ids = [];
|
|
for (const attributeSelector of attributeSelectors) {
|
|
let selectorString;
|
|
if (attributeSelector.startsWith("=")) {
|
|
selectorString = attributeSelector.substring(1);
|
|
} else if (attributeSelector.startsWith("^=")) {
|
|
selectorString = attributeSelector.substring(2);
|
|
} else {
|
|
continue;
|
|
}
|
|
const quoteChar = selectorString.charAt(0);
|
|
const stringRegex = REGEX_MATCH_QUOTED_STRING[quoteChar] ?? REGEX_MATCH_QUOTED_STRING[""];
|
|
const matches3 = stringRegex.exec(selectorString);
|
|
if (matches3 != null && matches3[1] != null) {
|
|
ids.push(matches3[1]);
|
|
}
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
// src/css-watcher.ts
|
|
var StylesheetWatcher = class {
|
|
constructor(app2) {
|
|
this.app = app2;
|
|
this.listeners = /* @__PURE__ */ new Map();
|
|
this.cachedSnippets = /* @__PURE__ */ new Map();
|
|
this.cachedObsidian = null;
|
|
this.cachedTheme = null;
|
|
this.watching = false;
|
|
}
|
|
/**
|
|
* Start watching for changes to stylesheets.
|
|
* @returns A callback function to pass to {@link Plugin.register}.
|
|
*/
|
|
watch() {
|
|
if (this.watching) {
|
|
throw new Error("Already watching.");
|
|
}
|
|
const events = [
|
|
{
|
|
event: "css-change",
|
|
listener: () => this.checkForChanges(false),
|
|
target: this.app.workspace
|
|
}
|
|
];
|
|
for (const evtl of events) {
|
|
evtl.target.on(evtl.event, evtl.listener);
|
|
}
|
|
this.checkForChanges();
|
|
return () => {
|
|
if (!this.watching) {
|
|
return;
|
|
}
|
|
for (const evtl of events) {
|
|
evtl.target.off(evtl.event, evtl.listener);
|
|
}
|
|
this.watching = false;
|
|
};
|
|
}
|
|
/**
|
|
* Describes how the Obsidian stylesheet was fetched, for the purpose of telling the user
|
|
* within the plugin's settings pane. The string will be concatenated as `Find built-in Obsidian callouts ${text}`.
|
|
*
|
|
* This uses the extra metadata attached to the {@link ObsidianStylesAndMetadata}.
|
|
*/
|
|
describeObsidianFetchMethod() {
|
|
switch (this.cachedObsidian?.method ?? "pending") {
|
|
case "dom":
|
|
return "using browser functions";
|
|
case "electron":
|
|
return "using undocumented functions";
|
|
case "fetch":
|
|
return "by reading Obsidian's styles";
|
|
case "pending":
|
|
return "";
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
on(event, listener) {
|
|
let listenersForEvent = this.listeners.get(event);
|
|
if (listenersForEvent === void 0) {
|
|
listenersForEvent = /* @__PURE__ */ new Set();
|
|
this.listeners.set(event, listenersForEvent);
|
|
}
|
|
listenersForEvent.add(listener);
|
|
}
|
|
/**
|
|
* Removes an event listener.
|
|
*
|
|
* @param event The event.
|
|
* @param listener The listener to remove.
|
|
*/
|
|
off(event, listener) {
|
|
const listenersForEvent = this.listeners.get(event);
|
|
if (listenersForEvent === void 0) {
|
|
return;
|
|
}
|
|
listenersForEvent.delete(listener);
|
|
if (listenersForEvent.size === 0) {
|
|
this.listeners.delete(event);
|
|
}
|
|
}
|
|
emit(event, ...data) {
|
|
const listenersForEvent = this.listeners.get(event);
|
|
if (listenersForEvent === void 0) {
|
|
return;
|
|
}
|
|
for (const listener of listenersForEvent) {
|
|
listener(...data);
|
|
}
|
|
}
|
|
/**
|
|
* Checks for any changes to the application stylesheets.
|
|
* If {@link watch} is being used, this will be called automatically.
|
|
*
|
|
* @param clear If set to true, the cache will be cleared.
|
|
* @returns True if there were any changes.
|
|
*/
|
|
async checkForChanges(clear) {
|
|
let changed = false;
|
|
this.emit("checkStarted");
|
|
if (clear === true) {
|
|
this.cachedSnippets.clear();
|
|
this.cachedTheme = null;
|
|
this.cachedObsidian = null;
|
|
}
|
|
if (this.cachedObsidian == null) {
|
|
changed = await this.checkForChangesObsidian() || changed;
|
|
}
|
|
changed = this.checkForChangesSnippets() || changed;
|
|
changed = this.checkForChangesTheme() || changed;
|
|
this.emit("checkComplete", changed);
|
|
return changed;
|
|
}
|
|
/**
|
|
* Attempts to fetch the Obsidian built-in stylesheet.
|
|
* This will fail if the version of obsidian is newer than the version supported by `obsidian-extra`.
|
|
*
|
|
* @returns true if the fetch was successful.
|
|
*/
|
|
async checkForChangesObsidian() {
|
|
try {
|
|
this.cachedObsidian = await fetchObsidianStyleSheet(this.app);
|
|
this.emit("change", {
|
|
type: "obsidian",
|
|
styles: this.cachedObsidian.cssText
|
|
});
|
|
return true;
|
|
} catch (ex) {
|
|
console.warn("Unable to fetch Obsidian stylesheet.", ex);
|
|
return false;
|
|
}
|
|
}
|
|
/**
|
|
* Checks for changes in the application's theme.
|
|
*/
|
|
checkForChangesTheme() {
|
|
const theme = getCurrentThemeID(this.app);
|
|
const themeManifest = theme == null ? null : getThemeManifest(this.app, theme);
|
|
const hasTheme = theme != null && themeManifest != null;
|
|
const styleEl = getThemeStyleElement(this.app);
|
|
const styles = styleEl?.textContent ?? "";
|
|
if (this.cachedTheme != null && !hasTheme) {
|
|
this.emit("remove", { type: "theme", theme: this.cachedTheme.id, styles: this.cachedTheme.contents });
|
|
this.cachedTheme = null;
|
|
return true;
|
|
}
|
|
if (this.cachedTheme == null && hasTheme) {
|
|
this.cachedTheme = { id: theme, version: themeManifest.version, contents: styles };
|
|
this.emit("add", { type: "theme", theme, styles });
|
|
return true;
|
|
}
|
|
if (!hasTheme || this.cachedTheme == null) {
|
|
return false;
|
|
}
|
|
const changed = this.cachedTheme.id !== theme || // Active theme changed
|
|
this.cachedTheme.version !== themeManifest.version || // Version of active theme changed
|
|
this.cachedTheme.contents !== styles;
|
|
if (changed) {
|
|
this.cachedTheme = {
|
|
id: theme,
|
|
version: themeManifest.version,
|
|
contents: styles
|
|
};
|
|
this.emit("change", { type: "theme", theme, styles });
|
|
}
|
|
return changed;
|
|
}
|
|
/**
|
|
* Checks for changes in the enabled snippets.
|
|
*/
|
|
checkForChangesSnippets() {
|
|
let anyChanges = false;
|
|
const snippets = getSnippetStyleElements(this.app);
|
|
const knownSnippets = Array.from(this.cachedSnippets.entries());
|
|
for (const [id, cachedStyles] of knownSnippets) {
|
|
const styleEl = snippets.get(id);
|
|
if (styleEl == null) {
|
|
this.cachedSnippets.delete(id);
|
|
this.emit("remove", { type: "snippet", snippet: id, styles: cachedStyles });
|
|
anyChanges = true;
|
|
continue;
|
|
}
|
|
if (styleEl.textContent != null && styleEl.textContent !== cachedStyles) {
|
|
this.cachedSnippets.set(id, styleEl.textContent);
|
|
this.emit("change", { type: "snippet", snippet: id, styles: styleEl.textContent });
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
for (const [id, styleEl] of snippets.entries()) {
|
|
if (styleEl == null)
|
|
continue;
|
|
if (!this.cachedSnippets.has(id) && styleEl.textContent != null) {
|
|
this.cachedSnippets.set(id, styleEl.textContent);
|
|
this.emit("add", { type: "snippet", snippet: id, styles: styleEl.textContent });
|
|
anyChanges = true;
|
|
}
|
|
}
|
|
return anyChanges;
|
|
}
|
|
};
|
|
|
|
// src/panes/manage-callouts-pane.ts
|
|
var import_obsidian22 = require("obsidian");
|
|
|
|
// src/ui/pane.ts
|
|
var UIPane = class {
|
|
/**
|
|
* Called to display the controls for the pane.
|
|
*/
|
|
displayControls() {
|
|
}
|
|
/**
|
|
* Called when the pane is created and attached to the setting tab, but before {@link display} is called.
|
|
*/
|
|
onReady() {
|
|
}
|
|
/**
|
|
* Called when the pane is removed and ready to be destroyed.
|
|
* Any important settings should be saved here.
|
|
*
|
|
* @param cancelled If true, the user closed the pane with the escape key.
|
|
*/
|
|
onClose(cancelled) {
|
|
}
|
|
/**
|
|
* Called to save the state of the setting pane.
|
|
* This is used for suspending a pane when another pane covers it up.
|
|
*
|
|
* @returns The saved state.
|
|
*/
|
|
suspendState() {
|
|
return void 0;
|
|
}
|
|
/**
|
|
* Called to load the state of the setting pane.
|
|
* This is called before {@link display}.
|
|
*
|
|
* @param state The state to restore.
|
|
*/
|
|
restoreState(state) {
|
|
}
|
|
};
|
|
|
|
// src/search/condition.ts
|
|
var import_obsidian9 = require("obsidian");
|
|
function matches(index, textToMatch, scores) {
|
|
const matchesQuery = (0, import_obsidian9.prepareFuzzySearch)(textToMatch.trim());
|
|
let mask = 0n;
|
|
for (const [text, bit] of index) {
|
|
const res = matchesQuery(text);
|
|
if (res != null) {
|
|
mask |= 1n << BigInt(bit);
|
|
scores[bit] += res.score;
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
function includes(index, textToInclude, scores) {
|
|
let mask = 0n;
|
|
for (const [text, bit] of index) {
|
|
if (text.includes(textToInclude)) {
|
|
mask |= 1n << BigInt(bit);
|
|
scores[bit] += textToInclude.length / text.length;
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
function equals(index, textToHave, scores) {
|
|
let mask = 0n;
|
|
for (const [text, bit] of index) {
|
|
if (text.includes(textToHave)) {
|
|
mask |= 1n << BigInt(bit);
|
|
scores[bit] += textToHave.length / text.length;
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
function startsWith(index, textToStart, scores) {
|
|
let mask = 0n;
|
|
for (const [text, bit] of index) {
|
|
if (text.startsWith(textToStart)) {
|
|
mask |= 1n << BigInt(bit);
|
|
scores[bit] += textToStart.length / text.length;
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
|
|
// src/search/bitfield.ts
|
|
var BitField;
|
|
((BitField3) => {
|
|
function fromPosition(position) {
|
|
return 1n << BigInt(position);
|
|
}
|
|
BitField3.fromPosition = fromPosition;
|
|
function fromPositionWithTrailing(position) {
|
|
return (1n << BigInt(position + 1)) - 1n;
|
|
}
|
|
BitField3.fromPositionWithTrailing = fromPositionWithTrailing;
|
|
function scanMostSignificant(field) {
|
|
const MASK = ~0xFFFFFFFFn;
|
|
let offset = 0;
|
|
for (let a = field; (a & MASK) > 0; a >>= 32n) {
|
|
offset += 32;
|
|
}
|
|
return offset + (31 - Math.clz32(Number(field)));
|
|
}
|
|
BitField3.scanMostSignificant = scanMostSignificant;
|
|
function or(a, b) {
|
|
return a | b;
|
|
}
|
|
BitField3.or = or;
|
|
function and(a, b) {
|
|
return a & b;
|
|
}
|
|
BitField3.and = and;
|
|
function andNot(a, b) {
|
|
return a & ~b;
|
|
}
|
|
BitField3.andNot = andNot;
|
|
function not(a, width) {
|
|
return fromPositionWithTrailing(width - 1) ^ a;
|
|
}
|
|
BitField3.not = not;
|
|
})(BitField || (BitField = {}));
|
|
var BitPositionRegistry = class {
|
|
constructor() {
|
|
this.recycled = [];
|
|
this.next = 0;
|
|
this.asField = 0n;
|
|
}
|
|
/**
|
|
* A field of all owned bits.
|
|
*/
|
|
get field() {
|
|
return this.asField;
|
|
}
|
|
/**
|
|
* The number of bits that are needed to represent a bitfield.
|
|
*/
|
|
get size() {
|
|
return this.next;
|
|
}
|
|
/**
|
|
* Claims a bit from the registry.
|
|
*/
|
|
claim() {
|
|
const { recycled } = this;
|
|
const claimed = recycled.length > 0 ? recycled.pop() : this.next++;
|
|
this.asField = BitField.or(this.asField, BitField.fromPosition(claimed));
|
|
return claimed;
|
|
}
|
|
/**
|
|
* Relinquishes a bit back to the registry.
|
|
* @param position The position to relinquish.
|
|
*/
|
|
relinquish(position) {
|
|
const { recycled } = this;
|
|
recycled.push(position);
|
|
this.asField = BitField.andNot(this.asField, BitField.fromPosition(position));
|
|
}
|
|
};
|
|
|
|
// src/search/effect.ts
|
|
function add(a, b) {
|
|
return BitField.or(a, b);
|
|
}
|
|
function remove(a, b) {
|
|
return BitField.andNot(a, b);
|
|
}
|
|
function filter(a, b) {
|
|
return BitField.and(a, b);
|
|
}
|
|
|
|
// src/sort.ts
|
|
function combinedComparison(fns) {
|
|
const compare = (a, b) => {
|
|
let delta = 0;
|
|
for (const compare2 of fns) {
|
|
delta = compare2(a, b);
|
|
if (delta !== 0)
|
|
break;
|
|
}
|
|
return delta;
|
|
};
|
|
compare.precompute = (value) => {
|
|
const obj = {};
|
|
for (const fn of fns) {
|
|
if ("precompute" in fn) {
|
|
Object.assign(obj, fn.precompute(value));
|
|
}
|
|
}
|
|
return obj;
|
|
};
|
|
return compare;
|
|
}
|
|
function compareColor({ computed: { colorValid: aValid, colorHSV: aHSV } }, { computed: { colorValid: bValid, colorHSV: bHSV } }) {
|
|
const validityDelta = (aValid ? 1 : 0) - (bValid ? 1 : 0);
|
|
if (validityDelta !== 0)
|
|
return validityDelta;
|
|
const saturatedDelta = (aHSV.s > 0 ? 1 : 0) - (bHSV.s > 0 ? 1 : 0);
|
|
if (saturatedDelta !== 0)
|
|
return saturatedDelta;
|
|
const hueDelta = aHSV.h - bHSV.h;
|
|
if (Math.abs(hueDelta) > 2)
|
|
return hueDelta;
|
|
const svDelta = aHSV.s + aHSV.v - (bHSV.s + bHSV.v);
|
|
if (svDelta !== 0)
|
|
return svDelta;
|
|
return 0;
|
|
}
|
|
((compareColor2) => {
|
|
function precompute(v) {
|
|
const color = getColorFromCallout(v);
|
|
return {
|
|
colorValid: color != null,
|
|
colorHSV: color == null ? { h: 0, s: 0, v: 0 } : toHSV(color)
|
|
};
|
|
}
|
|
compareColor2.precompute = precompute;
|
|
})(compareColor || (compareColor = {}));
|
|
function compareId({ value: { id: aId } }, { value: { id: bId } }) {
|
|
return bId.localeCompare(aId);
|
|
}
|
|
|
|
// src/search/search-index.ts
|
|
var SearchIndex = class {
|
|
constructor(columns) {
|
|
this.columns = /* @__PURE__ */ new Map();
|
|
this.registry = new BitPositionRegistry();
|
|
for (const [col, description] of Object.entries(columns)) {
|
|
this.columns.set(col, new SearchIndexColumn(this.registry, description));
|
|
}
|
|
}
|
|
get bitfield() {
|
|
return this.registry.field;
|
|
}
|
|
get size() {
|
|
return this.registry.size;
|
|
}
|
|
column(name) {
|
|
const col = this.columns.get(name);
|
|
if (col === void 0)
|
|
throw new NoSuchColumnError(name);
|
|
return col;
|
|
}
|
|
};
|
|
var NoSuchColumnError = class extends Error {
|
|
constructor(column) {
|
|
super(`No such column in index: ${column}`);
|
|
}
|
|
};
|
|
var SearchIndexColumn = class {
|
|
constructor(registry, options) {
|
|
this.entries = /* @__PURE__ */ new Map();
|
|
this.bitReg = registry;
|
|
this.normalize = options?.normalize ?? ((n) => n);
|
|
}
|
|
/**
|
|
* Adds a value to the column.
|
|
*
|
|
* @param value The value to add.
|
|
* @returns The associated bit position of the value.
|
|
*/
|
|
add(value) {
|
|
const { entries } = this;
|
|
const normalized = this.normalize(value);
|
|
const existingBit = entries.get(normalized);
|
|
if (existingBit !== void 0)
|
|
return existingBit;
|
|
const newBit = this.bitReg.claim();
|
|
entries.set(normalized, newBit);
|
|
return newBit;
|
|
}
|
|
/**
|
|
* Removes a value from the column.
|
|
* @param value The value to remove.
|
|
*/
|
|
delete(value) {
|
|
const { entries } = this;
|
|
const normalized = this.normalize(value);
|
|
const existingBit = entries.get(normalized);
|
|
if (existingBit === void 0)
|
|
return;
|
|
this.bitReg.relinquish(existingBit);
|
|
entries.delete(normalized);
|
|
}
|
|
/**
|
|
* Gets a value from the column.
|
|
*
|
|
* @param value The value to get.
|
|
* @returns The associated bit position, or undefined if it is not in the column.
|
|
*/
|
|
get(value) {
|
|
return this.entries.get(this.normalize(value));
|
|
}
|
|
/**
|
|
* The number of indices in this column.
|
|
*/
|
|
get size() {
|
|
return this.entries.size;
|
|
}
|
|
[Symbol.iterator]() {
|
|
return this.entries.entries();
|
|
}
|
|
};
|
|
|
|
// src/search/search.ts
|
|
var Search = class {
|
|
constructor(options) {
|
|
this.resetToAll = options?.resetToAll ?? false;
|
|
this.index = new SearchIndex(options.columns);
|
|
this.indexedItems = [];
|
|
const suppliedFnCompare = options.compareItem ?? ((a, b) => 0);
|
|
this.fnIndex = options.indexItem;
|
|
this.currentMask = 0n;
|
|
this.currentResults = null;
|
|
this.currentScores = new Float32Array(0);
|
|
this.reusableCurrentScoresBuffer = new Float32Array(0);
|
|
this.fnCompare = (options.resultRanking ?? true) === false ? (a, b) => suppliedFnCompare(a.value, b.value) : (a, b) => {
|
|
const delta = b.score - a.score;
|
|
if (delta !== 0)
|
|
return delta;
|
|
return suppliedFnCompare(b.value, a.value);
|
|
};
|
|
}
|
|
/**
|
|
* Adds items to the search.
|
|
* This will index the items.
|
|
*
|
|
* @param items The items to add.
|
|
*/
|
|
addItems(items) {
|
|
const { index, indexedItems } = this;
|
|
for (const item of items) {
|
|
const mask = this.fnIndex(item, index);
|
|
indexedItems.push({
|
|
value: item,
|
|
mask,
|
|
score: 0
|
|
});
|
|
}
|
|
const { size: newLength } = index;
|
|
if (newLength > this.currentScores.length) {
|
|
const { currentScores: oldScore } = this;
|
|
this.reusableCurrentScoresBuffer = new Float32Array(newLength);
|
|
this.currentScores = new Float32Array(newLength);
|
|
this.currentScores.set(oldScore, 0);
|
|
}
|
|
}
|
|
reset() {
|
|
const { index } = this;
|
|
this.currentMask = this.resetToAll ? index.bitfield : 0n;
|
|
this.currentResults = null;
|
|
this.currentScores = new Float32Array(index.size);
|
|
}
|
|
search(property, condition, text, effect, weight) {
|
|
const newScores = this.reusableCurrentScoresBuffer.fill(0);
|
|
this.currentResults = null;
|
|
const column = this.index.column(property);
|
|
const searchResult = condition(column, column.normalize(text), newScores);
|
|
this.currentMask = effect(this.currentMask, searchResult);
|
|
const scoreWeight = weight ?? 1;
|
|
const scores = this.currentScores;
|
|
for (let i = 0, end = scores.length; i < end; i++) {
|
|
scores[i] += newScores[i] * scoreWeight;
|
|
}
|
|
}
|
|
get results() {
|
|
const cached = this.currentResults;
|
|
if (cached != null)
|
|
return cached;
|
|
const { currentMask, currentScores, fnCompare } = this;
|
|
const results = this.indexedItems.filter(({ mask }) => (currentMask & mask) > 0n);
|
|
results.forEach((item) => {
|
|
let mask = item.mask;
|
|
let score = 0;
|
|
for (let index = 0; mask > 0n; index++, mask >>= 1n) {
|
|
if ((mask & 1n) !== 0n)
|
|
score += currentScores[index];
|
|
}
|
|
item.score = score;
|
|
});
|
|
results.sort(fnCompare);
|
|
return this.currentResults = results.map(({ value }) => value);
|
|
}
|
|
};
|
|
|
|
// src/search/factory.ts
|
|
var SearchFactory = class {
|
|
/**
|
|
* @param objects The objects to search through.
|
|
*/
|
|
constructor(objects) {
|
|
this.columns = [];
|
|
this.metadataGenerators = [];
|
|
this.sortFunctions = [];
|
|
this.options = {};
|
|
this.items = objects;
|
|
}
|
|
/**
|
|
* Adds a column that can be searched.
|
|
*
|
|
* @param name The column name.
|
|
* @param property The property name of the string property to index, or a getter function to return some string.
|
|
* @param normalize A function to normalize the value.
|
|
*/
|
|
withColumn(name, property, normalize) {
|
|
const getter = typeof property === "function" ? property : (obj) => [obj[property]];
|
|
this.columns.push({
|
|
name,
|
|
getter,
|
|
desc: {
|
|
normalize
|
|
}
|
|
});
|
|
return this;
|
|
}
|
|
/**
|
|
* Adds metadata to the search result items.
|
|
* This is useful for attaching cached data such as search previews.
|
|
*
|
|
* @param generator A function to generate the metadata.
|
|
*/
|
|
withMetadata(generator) {
|
|
this.metadataGenerators.push(generator);
|
|
return this;
|
|
}
|
|
/**
|
|
* Adds a sorting rule to the search result items.
|
|
* @param comparator The comparator for the sorting rule.
|
|
*/
|
|
withSorting(comparator) {
|
|
this.sortFunctions.push(comparator);
|
|
return this;
|
|
}
|
|
/**
|
|
* Changes if empty queries will be inclusive.
|
|
* @param enabled Whether enabled.
|
|
*/
|
|
withInclusiveDefaults(enabled) {
|
|
this.options.resetToAll = enabled;
|
|
return this;
|
|
}
|
|
/**
|
|
* Builds the index and returns a search class.
|
|
*/
|
|
build() {
|
|
const { metadataGenerators, sortFunctions, columns: columnGenerators } = this;
|
|
const compareFn = sortFunctions.length === 0 ? void 0 : sortFunctions.length === 1 ? sortFunctions[0] : combinedComparison(this.sortFunctions);
|
|
const items = this.items.map(
|
|
(item) => Object.assign({}, ...metadataGenerators.map((fn) => fn(item)), {
|
|
value: item,
|
|
computed: compareFn?.precompute?.(item)
|
|
})
|
|
);
|
|
const columns = Object.fromEntries(columnGenerators.map(({ name, desc }) => [name, desc]));
|
|
const indexFn = (item, index) => {
|
|
let field = 0n;
|
|
for (const { name, getter } of columnGenerators) {
|
|
const column = index.column(name);
|
|
for (const value of getter(item.value)) {
|
|
field = BitField.or(field, BitField.fromPosition(column.add(value)));
|
|
}
|
|
}
|
|
return field;
|
|
};
|
|
const search = new Search({
|
|
...this.options,
|
|
columns,
|
|
indexItem: indexFn,
|
|
compareItem: compareFn
|
|
});
|
|
search.addItems(items);
|
|
return search;
|
|
}
|
|
};
|
|
|
|
// src/search/normalize.ts
|
|
function casefold(text) {
|
|
return text.toLowerCase();
|
|
}
|
|
function unicode(text) {
|
|
return text.normalize("NFC");
|
|
}
|
|
function trimmed(text) {
|
|
return text.trim();
|
|
}
|
|
function combinedNormalization(fns) {
|
|
return (text) => {
|
|
for (const fn of fns) {
|
|
text = fn(text);
|
|
}
|
|
return text;
|
|
};
|
|
}
|
|
|
|
// src/search/query.ts
|
|
var REGEX_NOT_WHITESPACE = /\S/g;
|
|
var REGEX_FIELD_DELIMITER = /[ \\":]/g;
|
|
var REGEX_QUERYOP_EFFECT = /[-+&]+/y;
|
|
var REGEX_QUERYOP_FIELD = /[\w-]+/y;
|
|
var REGEX_QUERYOP_CONDITION = /[\^=~%:]+|(?:\[(?:is|equals|matches|has|includes|contains)\]:)/y;
|
|
function parseQuery(query) {
|
|
const ctx = new QueryParserContext(query);
|
|
const ops = [];
|
|
while (!ctx.done) {
|
|
ctx.take.trim();
|
|
ops.push(parseOperation(ctx));
|
|
}
|
|
return ops;
|
|
}
|
|
function parseOperation(ctx) {
|
|
ctx.stage.push("search term");
|
|
try {
|
|
const effect = parseOperationEffect(ctx);
|
|
let field = parseOperationField(ctx);
|
|
const condition = parseOperationCondition(ctx);
|
|
let text = ctx.done || ctx.peek.prefix(" ") ? null : parseText(ctx);
|
|
if (text == null && condition == null) {
|
|
text = field;
|
|
field = null;
|
|
}
|
|
if (text == "" && field == "" && effect == null && condition == null) {
|
|
throw new QuerySyntaxError(ctx, "Unexpected end");
|
|
}
|
|
if (text == "") {
|
|
throw new QuerySyntaxError(ctx, "Missing query text");
|
|
}
|
|
return {
|
|
field,
|
|
text,
|
|
condition,
|
|
effect
|
|
};
|
|
} finally {
|
|
ctx.stage.pop();
|
|
}
|
|
}
|
|
function parseOperationEffect(ctx) {
|
|
const [matches2] = ctx.take.findFirstRegex(REGEX_QUERYOP_EFFECT);
|
|
if (matches2 == null)
|
|
return null;
|
|
switch (matches2[0]) {
|
|
case "-":
|
|
return remove;
|
|
case "+":
|
|
return add;
|
|
case "&":
|
|
return filter;
|
|
default:
|
|
throw new QuerySyntaxError(
|
|
ctx,
|
|
`Unexpected symbol \`${matches2[1]}\`. Did you mean \`-\`, \`+\`, or \`&\`?`
|
|
);
|
|
}
|
|
}
|
|
function parseOperationField(ctx) {
|
|
const parts = [];
|
|
while (true) {
|
|
const [matches2] = ctx.take.findFirstRegex(REGEX_QUERYOP_FIELD);
|
|
if (matches2 != null) {
|
|
parts.push(matches2[0]);
|
|
continue;
|
|
}
|
|
if (ctx.peek.prefix("\\")) {
|
|
parts.push(parseEscapeSequence(ctx));
|
|
continue;
|
|
}
|
|
break;
|
|
}
|
|
return parts.join("");
|
|
}
|
|
function parseOperationCondition(ctx) {
|
|
const [matches2] = ctx.take.findFirstRegex(REGEX_QUERYOP_CONDITION);
|
|
if (matches2 == null)
|
|
return null;
|
|
const chr = matches2[0];
|
|
switch (chr) {
|
|
case ":":
|
|
case "[matches]:":
|
|
return matches;
|
|
case "=":
|
|
case "[equals]:":
|
|
case "[is]:":
|
|
return equals;
|
|
case "%=":
|
|
case "[includes]:":
|
|
case "[contains]:":
|
|
return includes;
|
|
case "^=":
|
|
return startsWith;
|
|
default:
|
|
throw new QuerySyntaxError(ctx, `Unexpected symbol \`${chr}\`. Did you mean \`:\`, \`=\`, or \`%=\`?`);
|
|
}
|
|
}
|
|
function parseText(ctx) {
|
|
ctx.stage.push("text");
|
|
try {
|
|
const parts = [];
|
|
let isQuoted = false;
|
|
while (true) {
|
|
const [matches2] = ctx.peek.findFirstRegex(REGEX_FIELD_DELIMITER);
|
|
if (matches2 == null) {
|
|
parts.push(ctx.take.remaining());
|
|
break;
|
|
}
|
|
parts.push(ctx.take.next(matches2.index - ctx.offset));
|
|
const chr = matches2[0];
|
|
if (chr === "\\") {
|
|
parts.push(parseEscapeSequence(ctx));
|
|
continue;
|
|
}
|
|
if (chr === " ") {
|
|
if (!isQuoted)
|
|
break;
|
|
ctx.take.next(chr.length);
|
|
parts.push(" ");
|
|
continue;
|
|
}
|
|
if (chr === '"') {
|
|
ctx.take.next(chr.length);
|
|
isQuoted = !isQuoted;
|
|
continue;
|
|
}
|
|
throw new QuerySyntaxError(ctx, `Unexpected symbol \`${chr}\``);
|
|
}
|
|
if (isQuoted) {
|
|
throw new QuerySyntaxError(ctx, 'Unexpected end of string while matching `"`');
|
|
}
|
|
return parts.join("");
|
|
} finally {
|
|
ctx.stage.pop();
|
|
}
|
|
}
|
|
function parseEscapeSequence(ctx) {
|
|
ctx.stage.push("escape sequence");
|
|
try {
|
|
if (!ctx.take.prefix("\\")) {
|
|
throw new QuerySyntaxError(ctx, "Not an escape sequence");
|
|
}
|
|
const chr = ctx.take.next(1);
|
|
switch (chr) {
|
|
case "\\":
|
|
case '"':
|
|
case "'":
|
|
case " ":
|
|
return chr;
|
|
case "n":
|
|
return "\n";
|
|
case "r":
|
|
return "\r";
|
|
case "x": {
|
|
const hex = ctx.take.next(2);
|
|
return hexStringToCharacter(ctx, hex);
|
|
}
|
|
case "u": {
|
|
if (ctx.peek.next(1) !== "{") {
|
|
return hexStringToCharacter(ctx, ctx.take.next(4));
|
|
}
|
|
const [matches2] = ctx.take.findLongestRegex(/\{([^}]+)\}/);
|
|
if (matches2 == null) {
|
|
throw new QuerySyntaxError(ctx, "Unexpected end of string while matching `{`");
|
|
}
|
|
return hexStringToCharacter(ctx, matches2[1]);
|
|
}
|
|
default: {
|
|
throw new QuerySyntaxError(ctx, `Unknown escape sequence (${chr})`);
|
|
}
|
|
}
|
|
} finally {
|
|
ctx.stage.pop();
|
|
}
|
|
}
|
|
function hexStringToCharacter(ctx, hex) {
|
|
if (!/^[A-Fa-f0-9]+$/.test(hex)) {
|
|
throw new QuerySyntaxError(ctx, `Invalid hex number \`${hex}\``);
|
|
}
|
|
const value = parseInt(hex, 16);
|
|
if (isNaN(value))
|
|
throw new QuerySyntaxError(ctx, `Invalid hex number \`${hex}\``);
|
|
return String.fromCharCode(value);
|
|
}
|
|
var QuerySyntaxError = class extends Error {
|
|
constructor(ctx, message) {
|
|
super(`Error parsing ${ctx.stage ?? "query"} at offset ${ctx.offset}: ${message}`);
|
|
}
|
|
};
|
|
var QueryParserContext = class {
|
|
constructor(source) {
|
|
this.source = source;
|
|
this._offset = 0;
|
|
this.take = new QueryParserContextFunctions(this, (n) => this._offset = n);
|
|
this.peek = new QueryParserContextFunctions(this, () => {
|
|
});
|
|
this.stage = [];
|
|
}
|
|
/**
|
|
* The current offset from the source.
|
|
*/
|
|
get offset() {
|
|
return this._offset;
|
|
}
|
|
/**
|
|
* True if the source is fully consumed.
|
|
*/
|
|
get done() {
|
|
return this._offset >= this.source.length;
|
|
}
|
|
};
|
|
var QueryParserContextFunctions = class {
|
|
constructor(ctx, setOffset) {
|
|
this.ctx = ctx;
|
|
this.setOffset = setOffset;
|
|
}
|
|
/**
|
|
* Checks if the next few characters are equal to a prefix string, and consumes them if they are.
|
|
* @param prefix The prefix string.
|
|
*/
|
|
prefix(prefix) {
|
|
const { source, offset } = this.ctx;
|
|
if (source.substring(offset, offset + prefix.length) === prefix) {
|
|
this.setOffset(offset + prefix.length);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Trims leading whitespace.
|
|
* @returns The leading whitespace.
|
|
*/
|
|
trim() {
|
|
const { source, offset } = this.ctx;
|
|
REGEX_NOT_WHITESPACE.lastIndex = offset;
|
|
const match = REGEX_NOT_WHITESPACE.exec(source);
|
|
if (match == null) {
|
|
this.setOffset(source.length);
|
|
return source.substring(offset);
|
|
}
|
|
this.setOffset(match.index);
|
|
return source.substring(offset, match.index);
|
|
}
|
|
/**
|
|
* Consumes the next few characters.
|
|
* @param length The number of characters to consume.
|
|
*/
|
|
maybeNext(length) {
|
|
const { source, offset } = this.ctx;
|
|
const substring = source.substring(offset, offset + length);
|
|
this.setOffset(offset + length);
|
|
return substring;
|
|
}
|
|
/**
|
|
* Consumes the next few characters.
|
|
* If the end of the string is reached, this will throw an error.
|
|
*
|
|
* @param length The number of characters to consume.
|
|
*/
|
|
next(length) {
|
|
const { source, offset } = this.ctx;
|
|
const substring = source.substring(offset, offset + length);
|
|
if (substring.length < length)
|
|
throw new QuerySyntaxError(this.ctx, "Unexpected end");
|
|
this.setOffset(offset + length);
|
|
return substring;
|
|
}
|
|
/**
|
|
* Takes the remaining text.
|
|
* @returns The remaining text.
|
|
*/
|
|
remaining() {
|
|
const { source, offset } = this.ctx;
|
|
this.setOffset(source.length);
|
|
return source.substring(offset);
|
|
}
|
|
/**
|
|
* Finds and consumes the longest matching regular expression.
|
|
* @param regexps The regular expressions.
|
|
*/
|
|
findLongestRegex(...regexps) {
|
|
const { source, offset } = this.ctx;
|
|
let longestLength = 0;
|
|
const result = [null, null];
|
|
for (const regexp of regexps) {
|
|
regexp.lastIndex = offset;
|
|
const matches2 = regexp.exec(source);
|
|
if (matches2 != null && matches2[0].length > longestLength) {
|
|
longestLength = matches2[0].length;
|
|
result[0] = matches2;
|
|
result[1] = regexp;
|
|
}
|
|
}
|
|
const resultMatches = result[0];
|
|
if (resultMatches != null) {
|
|
this.setOffset(resultMatches.index + resultMatches[0].length);
|
|
}
|
|
return result;
|
|
}
|
|
/**
|
|
* Finds and consumes the first matching regular expression.
|
|
* @param length The regular expressions.
|
|
*/
|
|
findFirstRegex(...regexps) {
|
|
const { source, offset } = this.ctx;
|
|
let firstMatch = source.length + 1;
|
|
const result = [null, null];
|
|
for (const regexp of regexps) {
|
|
regexp.lastIndex = offset;
|
|
const matches2 = regexp.exec(source);
|
|
if (matches2 != null && matches2.index < firstMatch) {
|
|
firstMatch = matches2.index;
|
|
result[0] = matches2;
|
|
result[1] = regexp;
|
|
}
|
|
}
|
|
const resultMatches = result[0];
|
|
if (resultMatches != null) {
|
|
this.setOffset(resultMatches.index + resultMatches[0].length);
|
|
}
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// src/callout-search.ts
|
|
function calloutSearch(callouts, options) {
|
|
const preview = options?.preview;
|
|
const defaultCondition = options?.defaultCondition ?? matches;
|
|
const standardNormalization = combinedNormalization([
|
|
casefold,
|
|
unicode,
|
|
trimmed,
|
|
(v) => v.replace(/[ -_.]+/g, "-")
|
|
]);
|
|
const standardSorting = combinedComparison([compareColor, compareId]);
|
|
const search = new SearchFactory(callouts).withColumn("id", "id", standardNormalization).withColumn("icon", "icon", standardNormalization).withColumn("from", sourceGetter, standardNormalization).withColumn("snippet", snippetGetter, standardNormalization).withMetadata((callout) => preview == null ? {} : { preview: preview(callout) }).withInclusiveDefaults(true).withSorting(standardSorting).build();
|
|
return (query) => {
|
|
const ops = parseQuery(query);
|
|
search.reset();
|
|
for (const op of ops) {
|
|
let field = op.field;
|
|
if (field === "" || field == null)
|
|
field = "id";
|
|
if (op.text === "" || op.text == null)
|
|
continue;
|
|
search.search(field, op.condition ?? defaultCondition, op.text, op.effect ?? filter);
|
|
}
|
|
return search.results;
|
|
};
|
|
}
|
|
function snippetGetter(callout) {
|
|
const values = [];
|
|
for (const source of callout.sources) {
|
|
if (source.type !== "snippet")
|
|
continue;
|
|
values.push(source.snippet);
|
|
}
|
|
return values;
|
|
}
|
|
function sourceGetter(callout) {
|
|
const sources = [];
|
|
for (const source of callout.sources) {
|
|
switch (source.type) {
|
|
case "builtin":
|
|
sources.push("obsidian");
|
|
sources.push("builtin");
|
|
sources.push("built-in");
|
|
break;
|
|
case "custom":
|
|
sources.push("custom");
|
|
sources.push("user");
|
|
sources.push("callout-manager");
|
|
break;
|
|
default:
|
|
sources.push(source.type);
|
|
}
|
|
}
|
|
return sources;
|
|
}
|
|
|
|
// src/panes/create-callout-pane.ts
|
|
var import_obsidian21 = require("obsidian");
|
|
|
|
// src/util/validity-set.ts
|
|
var import_obsidian10 = require("obsidian");
|
|
var ValiditySet = class {
|
|
constructor(reducer) {
|
|
this._emitter = new import_obsidian10.Events();
|
|
this._reducer = reducer;
|
|
this._lastReducedValidity = null;
|
|
this._cachedValidity = {};
|
|
}
|
|
/**
|
|
* The current validity.
|
|
*/
|
|
get valid() {
|
|
const { _lastReducedValidity } = this;
|
|
if (_lastReducedValidity == null)
|
|
throw new Error("No validity available.");
|
|
return _lastReducedValidity;
|
|
}
|
|
/**
|
|
* Runs the provided function when the reduced validity changes.
|
|
*
|
|
* @param callback The callback to run.
|
|
* @returns An event ref.
|
|
*/
|
|
onChange(callback) {
|
|
if (this._lastReducedValidity != null) {
|
|
callback(this._lastReducedValidity);
|
|
}
|
|
return this._emitter.on("change", callback);
|
|
}
|
|
/**
|
|
* Updates the provided component's disabled state when the reduced validity changes.
|
|
*
|
|
* @param component The component to update.
|
|
* @returns An event ref.
|
|
*/
|
|
onChangeUpdateDisabled(component) {
|
|
return this.onChange((valid) => {
|
|
component.setDisabled(!valid);
|
|
});
|
|
}
|
|
/**
|
|
* Adds a validity source.
|
|
*
|
|
* @param id The source's unique ID.
|
|
* @returns A function for updating the validity.
|
|
*/
|
|
addSource(id) {
|
|
return (valid) => {
|
|
const cachedValidity = this._cachedValidity[id];
|
|
if (cachedValidity === valid)
|
|
return;
|
|
this._cachedValidity[id] = valid;
|
|
const newValidity = this._reducer({ ...this._cachedValidity });
|
|
if (newValidity !== this._lastReducedValidity) {
|
|
this._lastReducedValidity = newValidity;
|
|
this._emitter.trigger("change", newValidity);
|
|
}
|
|
};
|
|
}
|
|
};
|
|
((ValiditySet2) => {
|
|
function AllValid(states) {
|
|
return !Object.values(states).includes(false);
|
|
}
|
|
ValiditySet2.AllValid = AllValid;
|
|
})(ValiditySet || (ValiditySet = {}));
|
|
|
|
// src/panes/edit-callout-pane/index.ts
|
|
var import_obsidian20 = require("obsidian");
|
|
|
|
// src/panes/edit-callout-pane/appearance-type.ts
|
|
function determineAppearanceType(settings) {
|
|
return determineNonComplexAppearanceType(settings) ?? {
|
|
type: "complex",
|
|
settings
|
|
};
|
|
}
|
|
function determineNonComplexAppearanceType(settings) {
|
|
const settingsWithColorSchemeCondition = [];
|
|
const settingsWithNoCondition = [];
|
|
for (const setting of settings) {
|
|
const type = typeofCondition(setting.condition);
|
|
switch (type) {
|
|
case "colorScheme":
|
|
settingsWithColorSchemeCondition.push(setting);
|
|
break;
|
|
case void 0:
|
|
settingsWithNoCondition.push(setting);
|
|
break;
|
|
case "and":
|
|
case "or":
|
|
case "theme": {
|
|
console.debug("Cannot represent callout settings with UI.", {
|
|
reason: `Has condition of type '${type}'`,
|
|
settings
|
|
});
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
const colorSchemeColor = { dark: void 0, light: void 0 };
|
|
for (const setting of settingsWithColorSchemeCondition) {
|
|
const changed = Object.keys(setting.changes);
|
|
if (changed.length === 0) {
|
|
continue;
|
|
}
|
|
if (changed.find((key) => key !== "color") !== void 0) {
|
|
console.debug("Cannot represent callout settings with UI.", {
|
|
reason: `Has 'colorScheme' condition with non-color change.`,
|
|
settings
|
|
});
|
|
return null;
|
|
}
|
|
const appearanceCond = setting.condition.colorScheme;
|
|
if (colorSchemeColor[appearanceCond] !== void 0) {
|
|
console.debug("Cannot represent callout settings with UI.", {
|
|
reason: `Has multiple 'colorScheme' conditions that change ${appearanceCond} color.`,
|
|
settings
|
|
});
|
|
return null;
|
|
}
|
|
colorSchemeColor[appearanceCond] = setting.changes.color;
|
|
}
|
|
const otherChanges = {};
|
|
for (const [change, value] of settingsWithNoCondition.flatMap((s) => Object.entries(s.changes))) {
|
|
if (value === void 0)
|
|
continue;
|
|
if (change in otherChanges) {
|
|
console.debug("Cannot represent callout settings with UI.", {
|
|
reason: `Has multiple changes to '${change}'.`,
|
|
settings
|
|
});
|
|
return null;
|
|
}
|
|
otherChanges[change] = value;
|
|
}
|
|
const otherChangesColor = otherChanges.color;
|
|
delete otherChanges.color;
|
|
if (colorSchemeColor.dark === void 0 && colorSchemeColor.light === void 0) {
|
|
if (otherChangesColor === void 0) {
|
|
return { type: "unified", color: void 0, otherChanges };
|
|
}
|
|
return { type: "unified", color: otherChangesColor, otherChanges };
|
|
}
|
|
const colorDark = colorSchemeColor.dark ?? colorSchemeColor.light;
|
|
const colorLight = colorSchemeColor.light ?? colorSchemeColor.dark;
|
|
return { type: "per-scheme", colorDark, colorLight, otherChanges };
|
|
}
|
|
|
|
// src/panes/edit-callout-pane/editor-complex.ts
|
|
var import_obsidian11 = require("obsidian");
|
|
|
|
// src/panes/edit-callout-pane/appearance-editor.ts
|
|
var AppearanceEditor = class {
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/editor-complex.ts
|
|
var ComplexAppearanceEditor = class extends AppearanceEditor {
|
|
/** @override */
|
|
toSettings() {
|
|
return this.appearance.settings;
|
|
}
|
|
/** @override */
|
|
render() {
|
|
const { containerEl } = this;
|
|
const { settings } = this.appearance;
|
|
const complexJson = JSON.stringify(settings, void 0, " ");
|
|
containerEl.createEl("p", {
|
|
text: "This callout has been configured using the plugin's data.json file. To prevent unintentional changes to the configuration, you need to edit it manually."
|
|
});
|
|
containerEl.createEl("code", { cls: "calloutmanager-edit-callout-appearance-json" }, (el) => {
|
|
el.createEl("pre", { text: complexJson });
|
|
});
|
|
containerEl.createEl("p", {
|
|
text: "Alternatively, you can reset the callout by clicking the button below twice."
|
|
});
|
|
let resetButtonClicked = false;
|
|
const resetButton = new import_obsidian11.ButtonComponent(containerEl).setButtonText("Reset Callout").setClass("calloutmanager-edit-callout-appearance-reset").setWarning().onClick(() => {
|
|
if (!resetButtonClicked) {
|
|
resetButtonClicked = true;
|
|
resetButton.setButtonText("Are you sure?");
|
|
return;
|
|
}
|
|
this.setAppearance({ type: "unified", color: void 0, otherChanges: {} });
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/ui/setting/callout-color.ts
|
|
var import_obsidian13 = require("obsidian");
|
|
|
|
// src/ui/component/reset-button.ts
|
|
var import_obsidian12 = require("obsidian");
|
|
var ResetButtonComponent = class extends import_obsidian12.ExtraButtonComponent {
|
|
constructor(containerEl) {
|
|
super(containerEl);
|
|
this.setIcon("lucide-undo");
|
|
this.extraSettingsEl.classList.add("calloutmanager-reset-button");
|
|
}
|
|
};
|
|
|
|
// src/default_colors.json
|
|
var defaultColors = {
|
|
"82, 139, 212": "blue",
|
|
"83, 223, 221": "cyan",
|
|
"68, 207, 110": "green",
|
|
"233, 151, 63": "orange",
|
|
"251, 70, 76": "red",
|
|
"168, 130, 255": "purple",
|
|
"166, 189, 197": "gray",
|
|
"158, 158, 158": "light gray",
|
|
"208, 181, 48": "yellow",
|
|
"227, 107, 167": "pink",
|
|
"161, 106, 73": "brown",
|
|
"0, 0, 0": "black"
|
|
};
|
|
var default_colors_default = {
|
|
defaultColors
|
|
};
|
|
|
|
// src/ui/setting/callout-color.ts
|
|
var CalloutColorSetting = class extends import_obsidian13.Setting {
|
|
constructor(containerEl, callout) {
|
|
super(containerEl);
|
|
this.onChanged = void 0;
|
|
this.callout = callout;
|
|
this.isDefault = true;
|
|
this.addColorPicker((picker) => {
|
|
this.colorComponent = picker;
|
|
picker.onChange(() => {
|
|
const { r, g, b } = this.getColor();
|
|
this.onChanged?.(`${r}, ${g}, ${b}`);
|
|
});
|
|
});
|
|
this.dropdownComponent = new import_obsidian13.DropdownComponent(this.controlEl).then((dropdown) => {
|
|
dropdown.addOptions(defaultColors);
|
|
dropdown.onChange((value) => {
|
|
this.setColorString(value);
|
|
});
|
|
});
|
|
this.components.push(this.dropdownComponent);
|
|
this.components.push(
|
|
new ResetButtonComponent(this.controlEl).then((btn) => {
|
|
this.resetComponent = btn;
|
|
btn.onClick(() => this.onChanged?.(void 0));
|
|
})
|
|
);
|
|
this.setColor(void 0);
|
|
}
|
|
/**
|
|
* Sets the color string.
|
|
* This only accepts comma-delimited RGB values.
|
|
*
|
|
* @param color The color (e.g. `255, 10, 25`) or undefined to reset the color to default.
|
|
* @returns `this`, for chaining.
|
|
*/
|
|
setColorString(color) {
|
|
if (color == null) {
|
|
return this.setColor(void 0);
|
|
}
|
|
return this.setColor(parseColorRGB(`rgb(${color})`) ?? { r: 0, g: 0, b: 0 });
|
|
}
|
|
/**
|
|
* Sets the color.
|
|
*
|
|
* @param color The color or undefined to reset the color to default.
|
|
* @returns `this`, for chaining.
|
|
*/
|
|
setColor(color) {
|
|
const isDefault = this.isDefault = color == null;
|
|
if (color == null) {
|
|
color = getColorFromCallout(this.callout) ?? { r: 0, g: 0, b: 0 };
|
|
}
|
|
if (color instanceof Array) {
|
|
color = { r: color[0], g: color[1], b: color[2] };
|
|
}
|
|
this.colorComponent.setValueRgb(color);
|
|
if (`${color.r}, ${color.g}, ${color.b}` in defaultColors) {
|
|
this.dropdownComponent.setValue(`${color.r}, ${color.g}, ${color.b}`);
|
|
} else {
|
|
this.dropdownComponent.setValue("");
|
|
}
|
|
this.resetComponent.setDisabled(isDefault).setTooltip(isDefault ? "" : "Reset Color");
|
|
return this;
|
|
}
|
|
getColor() {
|
|
return this.colorComponent.getValueRgb();
|
|
}
|
|
isDefaultColor() {
|
|
return this.isDefault;
|
|
}
|
|
onChange(cb) {
|
|
this.onChanged = cb;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/ui/setting/callout-icon.ts
|
|
var import_obsidian16 = require("obsidian");
|
|
|
|
// src/panes/select-icon-pane.ts
|
|
var import_obsidian15 = require("obsidian");
|
|
|
|
// src/ui/component/icon-preview.ts
|
|
var import_obsidian14 = require("obsidian");
|
|
var IconPreviewComponent = class extends import_obsidian14.Component {
|
|
constructor(containerEl) {
|
|
super();
|
|
this.componentEl = containerEl.createEl("button", { cls: "calloutmanager-icon-preview" });
|
|
this.iconEl = this.componentEl.createDiv({ cls: "calloutmanager-icon-preview--icon" });
|
|
this.idEl = this.componentEl.createDiv({ cls: "calloutmanager-icon-preview--id" });
|
|
}
|
|
/**
|
|
* Sets the icon of the icon preview component.
|
|
* This will update the label and the icon SVG.
|
|
*
|
|
* @param icon The icon name.
|
|
* @returns This, for chaining.
|
|
*/
|
|
setIcon(icon) {
|
|
const iconSvg = (0, import_obsidian14.getIcon)(icon);
|
|
this.componentEl.setAttribute("data-icon-id", icon);
|
|
this.idEl.textContent = icon;
|
|
this.iconEl.empty();
|
|
if (iconSvg != null) {
|
|
this.iconEl.appendChild(iconSvg);
|
|
}
|
|
return this;
|
|
}
|
|
/**
|
|
* Sets the `click` event listener for the component.
|
|
*
|
|
* @param listener The listener.
|
|
* @returns This, for chaining.
|
|
*/
|
|
onClick(listener) {
|
|
this.componentEl.onclick = listener;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/panes/select-icon-pane.ts
|
|
var recentIcons = /* @__PURE__ */ new Set();
|
|
var SelectIconPane = class extends UIPane {
|
|
constructor(plugin, title, options) {
|
|
super();
|
|
this.title = title;
|
|
this.plugin = plugin;
|
|
this.onChoose = options.onChoose;
|
|
this.previewLimit = options.limit ?? 250;
|
|
this.previewLimitOverage = 0;
|
|
this.searchQuery = "";
|
|
this.searchResults = [];
|
|
const usedIconIds = new Set(plugin.callouts.values().map((c) => c.icon));
|
|
const usedIcons = this.usedIcons = /* @__PURE__ */ new Map();
|
|
this.allIcons = (0, import_obsidian15.getIconIds)().map((id) => {
|
|
const icon = {
|
|
id,
|
|
searchId: id.trim().toLowerCase(),
|
|
component: null,
|
|
searchResult: null
|
|
};
|
|
if (usedIconIds.has(id)) {
|
|
this.usedIcons.set(id, icon);
|
|
}
|
|
return icon;
|
|
});
|
|
this.compareIcons = ({ id: a, searchId: aLC, searchResult: aSR }, { id: b, searchId: bLC, searchResult: bSR }) => {
|
|
const recency = (recentIcons.has(b) ? 1 : 0) - (recentIcons.has(a) ? 1 : 0);
|
|
const suggested = (usedIcons.has(b) ? 1 : 0) - (usedIcons.has(a) ? 1 : 0);
|
|
const searchRank = (bSR?.score ?? 0) - (aSR?.score ?? 0);
|
|
const sum = recency + suggested + searchRank;
|
|
if (sum !== 0)
|
|
return sum;
|
|
return bLC.localeCompare(aLC);
|
|
};
|
|
}
|
|
/**
|
|
* Changes the search query.
|
|
* @param query The search query.
|
|
*/
|
|
search(query) {
|
|
this.searchQuery = query;
|
|
if (query === "") {
|
|
this.resetSearchResults();
|
|
} else {
|
|
const search = (0, import_obsidian15.prepareFuzzySearch)(query.trim().toLowerCase());
|
|
this.calculateSearchResults((icon) => search(icon.searchId));
|
|
}
|
|
this.display();
|
|
}
|
|
/**
|
|
* Updatse the search results list.
|
|
* @param search The search function.
|
|
*/
|
|
calculateSearchResults(search) {
|
|
this.searchResults = this.allIcons.filter((icon) => {
|
|
icon.searchResult = search(icon);
|
|
return icon.searchResult != null;
|
|
});
|
|
this.searchResults.sort(this.compareIcons);
|
|
this.previewLimitOverage = this.searchResults.splice(this.previewLimit).length;
|
|
}
|
|
/**
|
|
* Resets the search results list to show a default list of suggested icons.
|
|
*/
|
|
resetSearchResults() {
|
|
const { allIcons, previewLimit, usedIcons } = this;
|
|
this.searchResults = Array.from(
|
|
(/* @__PURE__ */ new Set([
|
|
...allIcons.slice(0, previewLimit),
|
|
...Array.from(usedIcons.values()).slice(0, previewLimit)
|
|
])).values()
|
|
);
|
|
this.searchResults.sort(this.compareIcons).slice(0, this.previewLimit);
|
|
this.previewLimitOverage = this.allIcons.length - this.searchResults.length;
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const { containerEl } = this;
|
|
const gridEl = document.createDocumentFragment().createDiv({ cls: "calloutmanager-icon-picker" });
|
|
for (const icon of this.searchResults) {
|
|
if (icon.component == null) {
|
|
icon.component = new IconPreviewComponent(gridEl).setIcon(icon.id).componentEl;
|
|
} else {
|
|
gridEl.appendChild(icon.component);
|
|
}
|
|
}
|
|
gridEl.addEventListener("click", ({ targetNode }) => {
|
|
for (; targetNode != null && targetNode !== gridEl; targetNode = targetNode.parentElement) {
|
|
if (!(targetNode instanceof HTMLElement))
|
|
continue;
|
|
const iconId = targetNode.getAttribute("data-icon-id");
|
|
if (iconId != null) {
|
|
recentIcons.add(iconId);
|
|
this.nav.close();
|
|
this.onChoose(iconId);
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
containerEl.empty();
|
|
containerEl.appendChild(gridEl);
|
|
const { previewLimitOverage } = this;
|
|
if (previewLimitOverage > 0) {
|
|
const { pluralIs, pluralIcon } = previewLimitOverage === 1 ? { pluralIs: "is", pluralIcon: "icon" } : { pluralIs: "are", pluralIcon: "icons" };
|
|
containerEl.createEl("p", {
|
|
text: `There ${pluralIs} ${previewLimitOverage} more ${pluralIcon} to show. Refine your search to see more.`
|
|
});
|
|
}
|
|
}
|
|
/** @override */
|
|
displayControls() {
|
|
const { controlsEl } = this;
|
|
new import_obsidian15.TextComponent(controlsEl).setValue(this.searchQuery).setPlaceholder("Search icons...").onChange(this.search.bind(this));
|
|
}
|
|
/** @override */
|
|
onReady() {
|
|
this.resetSearchResults();
|
|
}
|
|
};
|
|
|
|
// src/ui/setting/callout-icon.ts
|
|
var CalloutIconSetting = class extends import_obsidian16.Setting {
|
|
constructor(containerEl, callout, plugin, getNav) {
|
|
super(containerEl);
|
|
this.onChanged = void 0;
|
|
this.callout = callout;
|
|
this.isDefault = true;
|
|
this.iconName = void 0;
|
|
this.addButton((btn) => {
|
|
this.buttonComponent = btn;
|
|
btn.onClick(() => {
|
|
getNav().open(
|
|
new SelectIconPane(plugin, "Select Icon", { onChoose: (icon) => this.onChanged?.(icon) })
|
|
);
|
|
});
|
|
});
|
|
this.components.push(
|
|
new ResetButtonComponent(this.controlEl).then((btn) => {
|
|
this.resetComponent = btn;
|
|
btn.onClick(() => this.onChanged?.(void 0));
|
|
})
|
|
);
|
|
this.setIcon(void 0);
|
|
}
|
|
/**
|
|
* Sets the icon.
|
|
*
|
|
* @param icon The icon name or undefined to reset the color to default.
|
|
* @returns `this`, for chaining.
|
|
*/
|
|
setIcon(icon) {
|
|
const isDefault = this.isDefault = icon == null;
|
|
const iconName = this.iconName = icon ?? this.callout.icon;
|
|
const iconExists = (0, import_obsidian16.getIcon)(iconName) != null;
|
|
if (iconExists) {
|
|
this.buttonComponent.setIcon(iconName);
|
|
} else {
|
|
this.buttonComponent.setButtonText(iconExists ? "" : `(missing icon: ${iconName})`);
|
|
}
|
|
this.resetComponent.setDisabled(isDefault).setTooltip(isDefault ? "" : "Reset Icon");
|
|
return this;
|
|
}
|
|
getIcon() {
|
|
return this.iconName ?? this.callout.icon;
|
|
}
|
|
isDefaultIcon() {
|
|
return this.isDefault;
|
|
}
|
|
onChange(cb) {
|
|
this.onChanged = cb;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/editor-per-scheme.ts
|
|
var PerSchemeAppearanceEditor = class extends AppearanceEditor {
|
|
/** @override */
|
|
toSettings() {
|
|
const { otherChanges, colorDark, colorLight } = this.appearance;
|
|
const forLight = {
|
|
condition: { colorScheme: "light" },
|
|
changes: {
|
|
color: colorLight
|
|
}
|
|
};
|
|
const forDark = {
|
|
condition: { colorScheme: "dark" },
|
|
changes: {
|
|
color: colorDark
|
|
}
|
|
};
|
|
if (forLight.changes.color === void 0)
|
|
delete forLight.changes.color;
|
|
if (forDark.changes.color === void 0)
|
|
delete forDark.changes.color;
|
|
return [{ changes: otherChanges }, forLight, forDark];
|
|
}
|
|
setAppearanceOrChangeToUnified(appearance) {
|
|
const { colorDark, colorLight, otherChanges } = appearance;
|
|
if (colorDark === void 0 && colorLight === void 0) {
|
|
this.setAppearance({ type: "unified", color: void 0, otherChanges });
|
|
return;
|
|
}
|
|
this.setAppearance(appearance);
|
|
}
|
|
render() {
|
|
const { callout, containerEl, appearance, plugin, nav } = this;
|
|
const { colorDark, colorLight, otherChanges } = this.appearance;
|
|
new CalloutColorSetting(containerEl, callout).setName("Dark Color").setDesc("Change the color of the callout for the dark color scheme.").setColorString(colorDark).onChange((color) => this.setAppearanceOrChangeToUnified({ ...appearance, colorDark: color }));
|
|
new CalloutColorSetting(containerEl, callout).setName(`Light Color`).setDesc(`Change the color of the callout for the light color scheme.`).setColorString(colorLight).onChange((color) => this.setAppearanceOrChangeToUnified({ ...appearance, colorLight: color }));
|
|
new CalloutIconSetting(containerEl, callout, plugin, () => nav).setName("Icon").setDesc("Change the callout icon.").setIcon(otherChanges.icon).onChange(
|
|
(icon) => this.setAppearanceOrChangeToUnified({ ...appearance, otherChanges: { ...otherChanges, icon } })
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/editor-unified.ts
|
|
var import_obsidian17 = require("obsidian");
|
|
var UnifiedAppearanceEditor = class extends AppearanceEditor {
|
|
/** @override */
|
|
toSettings() {
|
|
const { otherChanges, color } = this.appearance;
|
|
const changes = {
|
|
...otherChanges,
|
|
color
|
|
};
|
|
if (color === void 0) {
|
|
delete changes.color;
|
|
}
|
|
return Object.keys(changes).length === 0 ? [] : [{ changes }];
|
|
}
|
|
render() {
|
|
const { plugin, containerEl, callout, setAppearance } = this;
|
|
const { color, otherChanges } = this.appearance;
|
|
const colorScheme = getCurrentThemeID2(plugin.app);
|
|
const otherColorScheme = colorScheme === "dark" ? "light" : "dark";
|
|
new CalloutColorSetting(containerEl, callout).setName("Color").setDesc("Change the color of the callout.").setColorString(color).onChange((color2) => setAppearance({ type: "unified", otherChanges, color: color2 }));
|
|
new import_obsidian17.Setting(containerEl).setName(`Color Scheme`).setDesc(`Change the color of the callout for the ${otherColorScheme} color scheme.`).addButton(
|
|
(btn) => btn.setClass("clickable-icon").setIcon("lucide-sun-moon").onClick(() => {
|
|
const currentColor = color ?? callout.color;
|
|
setAppearance({
|
|
type: "per-scheme",
|
|
colorDark: currentColor,
|
|
colorLight: currentColor,
|
|
otherChanges
|
|
});
|
|
})
|
|
);
|
|
new CalloutIconSetting(containerEl, callout, plugin, () => this.nav).setName("Icon").setDesc("Change the callout icon.").setIcon(otherChanges.icon).onChange((icon) => setAppearance({ type: "unified", color, otherChanges: { ...otherChanges, icon } }));
|
|
}
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/misc-editor.ts
|
|
var import_obsidian18 = require("obsidian");
|
|
var MiscEditor = class {
|
|
constructor(plugin, callout, containerEl, viewOnly) {
|
|
this.plugin = plugin;
|
|
this.callout = callout;
|
|
this.containerEl = containerEl;
|
|
this.viewOnly = viewOnly;
|
|
this.renameSetting = this.createRenameSetting();
|
|
}
|
|
/**
|
|
* Renders the editors.
|
|
*/
|
|
render() {
|
|
this.containerEl.empty();
|
|
if (this.viewOnly)
|
|
return;
|
|
if (this.renameSetting != null)
|
|
this.containerEl.appendChild(this.renameSetting.settingEl);
|
|
}
|
|
createRenameSetting() {
|
|
const { plugin, containerEl, callout } = this;
|
|
if (callout.sources.length !== 1 || callout.sources[0].type !== "custom")
|
|
return null;
|
|
const validity = new ValiditySet(ValiditySet.AllValid);
|
|
const desc = document.createDocumentFragment();
|
|
desc.createEl("p", { text: "Change the name of this callout." });
|
|
desc.createEl("p", { text: "This will not update any references in your notes!", cls: "mod-warning" });
|
|
let newIdComponent;
|
|
return new import_obsidian18.Setting(containerEl).setName(`Rename`).setDesc(desc).addText((cmp) => {
|
|
newIdComponent = cmp;
|
|
cmp.setValue(callout.id).setPlaceholder(callout.id);
|
|
const isUnusedId = validity.addSource("unused");
|
|
cmp.onChange((value) => {
|
|
const alreadyExists = plugin.callouts.has(value);
|
|
isUnusedId(!alreadyExists);
|
|
cmp.inputEl.classList.toggle("invalid", alreadyExists);
|
|
});
|
|
makeTextComponentValidateCalloutID(cmp, "id", validity);
|
|
}).addButton((btn) => {
|
|
validity.onChangeUpdateDisabled(btn);
|
|
btn.setIcon("lucide-clipboard-signature").setTooltip("Rename").then(({ buttonEl }) => buttonEl.classList.add("clickable-icon", "mod-warning")).onClick(() => {
|
|
if (!validity.valid)
|
|
return;
|
|
const newId = newIdComponent.getValue();
|
|
plugin.renameCustomCallout(callout.id, newId);
|
|
this.nav.replace(new EditCalloutPane(plugin, newId, this.viewOnly));
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/section-info.ts
|
|
function renderInfo(app2, callout, containerEl) {
|
|
const frag = document.createDocumentFragment();
|
|
const contentEl = frag.createDiv({ cls: "calloutmanager-edit-callout-section" });
|
|
contentEl.createEl("h2", { text: "About this Callout" });
|
|
contentEl.createEl("div", { cls: "calloutmanager-edit-callout-info" }, (el) => {
|
|
el.appendText("The ");
|
|
el.createSpan({ cls: "calloutmanager-edit-callout--callout-id", text: callout.id });
|
|
el.appendText(" callout");
|
|
el.appendText(" has ");
|
|
appendColorInfo(el, callout);
|
|
el.appendText(" and ");
|
|
appendIconInfo(el, callout);
|
|
if (callout.sources.length === 1) {
|
|
if (callout.sources[0].type === "builtin") {
|
|
el.appendText(". It is one of the built-in callouts.");
|
|
return;
|
|
}
|
|
el.appendText(". It was added to Obsidian by the ");
|
|
appendSourceInfo(app2, el, callout.sources[0]);
|
|
el.appendText(".");
|
|
return;
|
|
}
|
|
el.appendText(". The callout comes from:");
|
|
const sources = el.createEl("ul", { cls: "calloutmanager-edit-callout--callout-source-list" });
|
|
for (const source of callout.sources) {
|
|
const itemEl = sources.createEl("li");
|
|
itemEl.appendText("The ");
|
|
appendSourceInfo(app2, itemEl, source);
|
|
itemEl.appendText(".");
|
|
}
|
|
});
|
|
containerEl.appendChild(frag);
|
|
}
|
|
function appendIconInfo(el, callout) {
|
|
el.appendText("is using the icon ");
|
|
el.createEl("code", { cls: "calloutmanager-edit-callout--callout-icon", text: callout.icon });
|
|
}
|
|
function appendColorInfo(el, callout) {
|
|
const calloutColor = getColorFromCallout(callout);
|
|
if (calloutColor == null) {
|
|
el.appendText("an invalid color (");
|
|
el.createEl("code", {
|
|
cls: "calloutmanager-edit-callout--color-invalid",
|
|
text: callout.color.trim()
|
|
});
|
|
el.appendText(")");
|
|
return;
|
|
}
|
|
el.appendText("the color ");
|
|
el.createEl(
|
|
"code",
|
|
{ cls: "calloutmanager-edit-callout--callout-color", text: describeColor(calloutColor) },
|
|
(colorEl) => colorEl.style.setProperty("--resolved-callout-color", callout.color)
|
|
);
|
|
}
|
|
function appendSourceInfo(app2, el, source) {
|
|
switch (source.type) {
|
|
case "builtin":
|
|
el.appendText("built-in callouts");
|
|
return true;
|
|
case "custom":
|
|
el.appendText("custom callouts you created");
|
|
return true;
|
|
case "snippet":
|
|
el.appendText("CSS snippet ");
|
|
el.createEl("code", {
|
|
cls: "calloutmanager-edit-callout--callout-source",
|
|
text: `${source.snippet}.css`
|
|
});
|
|
return true;
|
|
case "theme": {
|
|
el.appendText("theme ");
|
|
const themeName = getThemeManifest(app2, source.theme)?.name ?? source.theme;
|
|
el.createSpan({ cls: "calloutmanager-edit-callout--callout-source", text: themeName });
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
function describeColor(color) {
|
|
const hexString = toHexRGB(color);
|
|
const rgbString = `${color.r}, ${color.g}, ${color.b}`;
|
|
const namedColor = default_colors_default.defaultColors[rgbString];
|
|
if (namedColor != null) {
|
|
return namedColor;
|
|
}
|
|
return hexString;
|
|
}
|
|
|
|
// src/panes/edit-callout-pane/section-preview.ts
|
|
var import_obsidian19 = require("obsidian");
|
|
var EditCalloutPanePreview = class {
|
|
constructor(plugin, callout, viewOnly) {
|
|
this.previewMarkdown = "Lorem ipsum dolor sit amet.";
|
|
this.previewEditorEl = null;
|
|
this.calloutHasIconReady = false;
|
|
this.calloutId = callout.id;
|
|
this.plugin = plugin;
|
|
const frag = document.createDocumentFragment();
|
|
this.sectionEl = frag.createDiv({
|
|
cls: ["calloutmanager-preview-container", "calloutmanager-edit-callout-preview"]
|
|
});
|
|
this.preview = new IsolatedCalloutPreviewComponent(this.sectionEl, {
|
|
id: callout.id,
|
|
title: getTitleFromCallout(callout),
|
|
icon: callout.icon,
|
|
colorScheme: getCurrentThemeID2(plugin.app),
|
|
content: (containerEl) => {
|
|
containerEl.createEl("p", { text: this.previewMarkdown });
|
|
}
|
|
});
|
|
if (!viewOnly) {
|
|
this.makeEditable();
|
|
}
|
|
}
|
|
makeEditable() {
|
|
const contentEl = this.preview.contentEl;
|
|
this.previewEditorEl = null;
|
|
this.preview.calloutEl.addEventListener("click", () => {
|
|
if (this.previewEditorEl != null) {
|
|
return;
|
|
}
|
|
const height = contentEl.getBoundingClientRect().height;
|
|
contentEl.empty();
|
|
new import_obsidian19.TextAreaComponent(contentEl).setValue(this.previewMarkdown).setPlaceholder("Preview Markdown...").then((c) => {
|
|
const inputEl = this.previewEditorEl = c.inputEl;
|
|
inputEl.style.setProperty("height", `${height}px`);
|
|
inputEl.classList.add("calloutmanager-preview-editor");
|
|
inputEl.focus();
|
|
inputEl.addEventListener("blur", () => {
|
|
const value = c.getValue();
|
|
this.previewEditorEl = null;
|
|
this.previewMarkdown = value;
|
|
this.changeContent(value);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
/**
|
|
* Refreshes the callout preview's icon.
|
|
* We need to do this after the preview is attached to DOM, as we can't get the correct icon until that happens.
|
|
*/
|
|
refreshPreviewIcon() {
|
|
const { iconEl, calloutEl } = this.preview;
|
|
if (window.document.contains(this.sectionEl)) {
|
|
const icon = window.getComputedStyle(calloutEl).getPropertyValue("--callout-icon").trim();
|
|
const iconSvg = (0, import_obsidian19.getIcon)(icon) ?? document.createElement("svg");
|
|
iconEl.empty();
|
|
iconEl.appendChild(iconSvg);
|
|
this.calloutHasIconReady = true;
|
|
}
|
|
}
|
|
/**
|
|
* Changes the preview that is displayed inside the callout.
|
|
*
|
|
* @param markdown The markdown to render.
|
|
*/
|
|
async changeContent(markdown) {
|
|
const contentEl = this.preview.contentEl;
|
|
contentEl.empty();
|
|
try {
|
|
await import_obsidian19.MarkdownRenderer.renderMarkdown(markdown, contentEl, "", void 0);
|
|
} catch (ex) {
|
|
contentEl.createEl("code").createEl("pre", { text: markdown });
|
|
}
|
|
}
|
|
/**
|
|
* Changes the settings for the callout.
|
|
* This can be used to show the customized callout.
|
|
*
|
|
* @param settings The settings to use.
|
|
*/
|
|
async changeSettings(settings) {
|
|
const { preview } = this;
|
|
const styles = calloutSettingsToCSS(this.calloutId, settings, currentCalloutEnvironment(this.plugin.app));
|
|
preview.customStyleEl.textContent = styles;
|
|
preview.resetStylePropertyOverrides();
|
|
preview.removeStyles((el) => el.getAttribute("data-callout-manager") === "style-overrides");
|
|
this.calloutHasIconReady = false;
|
|
preview.removeStyles((el) => el.getAttribute("data-inject-id") === "callout-settings");
|
|
}
|
|
/**
|
|
* Attaches the preview to a container.
|
|
* @param containerEl The container element.
|
|
*/
|
|
attach(containerEl) {
|
|
containerEl.appendChild(this.sectionEl);
|
|
if (!this.calloutHasIconReady) {
|
|
this.refreshPreviewIcon();
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/panes/edit-callout-pane/index.ts
|
|
var IMPOSSIBLE_CALLOUT_ID = "[not a real callout]";
|
|
var EditCalloutPane = class extends UIPane {
|
|
constructor(plugin, id, viewOnly) {
|
|
super();
|
|
this.plugin = plugin;
|
|
this.viewOnly = viewOnly;
|
|
this.title = { title: "Callout", subtitle: id };
|
|
this.callout = plugin.callouts.get(id) ?? {
|
|
sources: [{ type: "custom" }],
|
|
...plugin.calloutResolver.getCalloutProperties(IMPOSSIBLE_CALLOUT_ID),
|
|
id
|
|
};
|
|
this.previewSection = new EditCalloutPanePreview(plugin, this.callout, false);
|
|
this.miscEditorContainerEl = document.createElement("div");
|
|
this.miscEditorContainerEl.classList.add(
|
|
"calloutmanager-edit-callout-section",
|
|
"calloutmanager-edit-callout-section--noborder",
|
|
"calloutmanager-edit-callout-misc"
|
|
);
|
|
this.miscEditor = new MiscEditor(plugin, this.callout, this.miscEditorContainerEl, viewOnly);
|
|
Object.defineProperty(this.miscEditor, "nav", {
|
|
get: () => this.nav
|
|
});
|
|
this.appearanceEditorContainerEl = document.createElement("div");
|
|
this.appearanceEditorContainerEl.classList.add(
|
|
"calloutmanager-edit-callout-section",
|
|
"calloutmanager-edit-callout-appearance"
|
|
);
|
|
this.appearanceEditorContainerEl.createEl("h2", { text: "Appearance" });
|
|
this.appearanceEditorEl = this.appearanceEditorContainerEl.createDiv();
|
|
this.changeSettings(plugin.getCalloutSettings(id) ?? []);
|
|
}
|
|
changeAppearanceEditor(newAppearance) {
|
|
const oldAppearance = this.appearance;
|
|
this.appearance = newAppearance;
|
|
if (newAppearance.type !== oldAppearance?.type) {
|
|
this.appearanceEditor = new APPEARANCE_EDITORS[newAppearance.type]();
|
|
Object.defineProperties(this.appearanceEditor, {
|
|
nav: { get: () => this.nav },
|
|
plugin: { value: this.plugin },
|
|
containerEl: { value: this.appearanceEditorEl },
|
|
setAppearance: { value: this.onSetAppearance.bind(this) }
|
|
});
|
|
}
|
|
const { appearanceEditor } = this;
|
|
appearanceEditor.appearance = newAppearance;
|
|
appearanceEditor.callout = this.callout;
|
|
}
|
|
onSetAppearance(appearance) {
|
|
this.changeAppearanceEditor(appearance);
|
|
const newSettings = this.appearanceEditor.toSettings();
|
|
const { callout } = this;
|
|
const { calloutResolver } = this.plugin;
|
|
this.plugin.setCalloutSettings(callout.id, newSettings);
|
|
const { color, icon } = calloutResolver.getCalloutProperties(callout.id);
|
|
callout.color = color;
|
|
callout.icon = icon;
|
|
this.previewSection.changeSettings(newSettings);
|
|
this.appearanceEditor.callout = callout;
|
|
this.appearanceEditorEl.empty();
|
|
this.appearanceEditor.render();
|
|
this.containerEl.empty();
|
|
this.display();
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const { containerEl, previewSection, appearanceEditorContainerEl, miscEditorContainerEl } = this;
|
|
containerEl.empty();
|
|
previewSection.attach(containerEl);
|
|
renderInfo(this.plugin.app, this.callout, containerEl);
|
|
containerEl.appendChild(miscEditorContainerEl);
|
|
containerEl.appendChild(appearanceEditorContainerEl);
|
|
}
|
|
/** @override */
|
|
displayControls() {
|
|
const { callout, controlsEl } = this;
|
|
if (!this.viewOnly && callout.sources.length === 1 && callout.sources[0].type === "custom") {
|
|
new import_obsidian20.ButtonComponent(controlsEl).setIcon("lucide-trash").setTooltip("Delete Callout").onClick(() => {
|
|
this.plugin.removeCustomCallout(callout.id);
|
|
this.nav.close();
|
|
}).then(({ buttonEl }) => buttonEl.classList.add("clickable-icon", "mod-warning"));
|
|
}
|
|
}
|
|
/**
|
|
* Changes the preview that is displayed inside the callout.
|
|
*
|
|
* @param markdown The markdown to render.
|
|
*/
|
|
async changePreview(markdown) {
|
|
return this.previewSection.changeContent(markdown);
|
|
}
|
|
/**
|
|
* Changes the styles of the preview that is displayed inside the callout.
|
|
*
|
|
* @param markdown The markdown to render.
|
|
*/
|
|
async changeSettings(settings) {
|
|
this.changeAppearanceEditor(determineAppearanceType(settings));
|
|
this.appearanceEditorEl.empty();
|
|
this.appearanceEditor.render();
|
|
this.miscEditor.render();
|
|
await this.previewSection.changeSettings(settings);
|
|
}
|
|
};
|
|
var APPEARANCE_EDITORS = {
|
|
complex: ComplexAppearanceEditor,
|
|
unified: UnifiedAppearanceEditor,
|
|
"per-scheme": PerSchemeAppearanceEditor
|
|
};
|
|
|
|
// src/panes/create-callout-pane.ts
|
|
var CreateCalloutPane = class extends UIPane {
|
|
constructor(plugin) {
|
|
super();
|
|
this.title = { title: "Callouts", subtitle: "New Callout" };
|
|
this.plugin = plugin;
|
|
this.validity = new ValiditySet(ValiditySet.AllValid);
|
|
const btnCreate = this.btnCreate = document.createElement("button");
|
|
btnCreate.textContent = "Create";
|
|
btnCreate.addEventListener("click", (evt) => {
|
|
if (!this.validity.valid) {
|
|
return;
|
|
}
|
|
const id = this.fieldIdComponent.getValue();
|
|
this.plugin.createCustomCallout(id);
|
|
this.nav.replace(new EditCalloutPane(this.plugin, id, false));
|
|
});
|
|
this.fieldId = new import_obsidian21.Setting(document.createElement("div")).setHeading().setName("Callout Name").setDesc("This is how you will refer to your callout in Markdown.").addText((cmp) => {
|
|
this.fieldIdComponent = cmp;
|
|
cmp.setPlaceholder("my-awesome-callout");
|
|
makeTextComponentValidateCalloutID(cmp, "id", this.validity);
|
|
});
|
|
this.validity.onChange((valid) => {
|
|
this.btnCreate.disabled = !valid;
|
|
});
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.appendChild(this.fieldId.settingEl);
|
|
containerEl.createDiv().appendChild(this.btnCreate);
|
|
}
|
|
};
|
|
function makeTextComponentValidateCalloutID(cmp, id, vs) {
|
|
cmp.then(({ inputEl }) => {
|
|
const update = vs.addSource(id);
|
|
inputEl.setAttribute("pattern", "^[a-z\\-]{1,}$");
|
|
inputEl.setAttribute("required", "required");
|
|
inputEl.addEventListener("change", onChange);
|
|
inputEl.addEventListener("input", onChange);
|
|
update(inputEl.validity.valid);
|
|
function onChange() {
|
|
update(inputEl.validity.valid);
|
|
}
|
|
});
|
|
}
|
|
|
|
// src/panes/manage-callouts-pane.ts
|
|
var ManageCalloutsPane = class extends UIPane {
|
|
constructor(plugin) {
|
|
super();
|
|
this.title = { title: "Callouts", subtitle: "Manage" };
|
|
this.plugin = plugin;
|
|
this.viewOnly = false;
|
|
this.searchQuery = "";
|
|
const { searchErrorDiv, searchErrorQuery } = createEmptySearchResultDiv();
|
|
this.searchErrorDiv = searchErrorDiv;
|
|
this.searchErrorQuery = searchErrorQuery;
|
|
}
|
|
/**
|
|
* Change the search query and re-render the panel.
|
|
* @param query The search query.
|
|
*/
|
|
search(query) {
|
|
this.doSearch(query);
|
|
this.display();
|
|
}
|
|
doSearch(query) {
|
|
try {
|
|
this.callouts = this.searchFn(query);
|
|
this.setSearchError?.(false);
|
|
} catch (ex) {
|
|
this.setSearchError?.(ex.message);
|
|
}
|
|
}
|
|
/**
|
|
* Refresh the callout previews.
|
|
* This regenerates the previews and their metadata from the list of callouts known to the plugin.
|
|
*/
|
|
invalidate() {
|
|
const { plugin, viewOnly } = this;
|
|
this.searchFn = calloutSearch(plugin.callouts.values(), {
|
|
preview: createPreviewFactory(viewOnly)
|
|
});
|
|
this.doSearch(this.searchQuery);
|
|
}
|
|
onCalloutButtonClick(evt) {
|
|
let id = null;
|
|
let action = null;
|
|
for (let target = evt.targetNode; target != null && (id == null || action == null); target = target?.parentElement) {
|
|
if (!(target instanceof Element))
|
|
continue;
|
|
if (id == null) {
|
|
id = target.getAttribute("data-callout-manager-callout");
|
|
}
|
|
if (action == null) {
|
|
action = target.getAttribute("data-callout-manager-action");
|
|
}
|
|
}
|
|
if (id == null || action == null) {
|
|
return;
|
|
}
|
|
if (action === "edit") {
|
|
this.nav.open(new EditCalloutPane(this.plugin, id, this.viewOnly));
|
|
} else if (action === "insert") {
|
|
const view = app.workspace.getActiveViewOfType(import_obsidian22.MarkdownView);
|
|
if (view) {
|
|
const cursor = view.editor.getCursor();
|
|
view.editor.replaceRange(
|
|
`> [!${id}]
|
|
> Contents`,
|
|
cursor
|
|
);
|
|
view.editor.setCursor(cursor.line + 1, 10);
|
|
closeSettings(app);
|
|
}
|
|
}
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const contentEl = document.createDocumentFragment().createDiv();
|
|
contentEl.addEventListener("click", this.onCalloutButtonClick.bind(this));
|
|
const { callouts } = this;
|
|
for (const callout of callouts) {
|
|
contentEl.appendChild(callout.preview);
|
|
}
|
|
if (callouts.length === 0) {
|
|
contentEl.appendChild(this.searchErrorDiv);
|
|
}
|
|
const { containerEl } = this;
|
|
containerEl.empty();
|
|
containerEl.appendChild(contentEl);
|
|
}
|
|
/** @override */
|
|
displayControls() {
|
|
const { controlsEl } = this;
|
|
const filter2 = new import_obsidian22.TextComponent(controlsEl).setValue(this.searchQuery).setPlaceholder("Filter callouts...").onChange(this.search.bind(this));
|
|
this.setSearchError = (message) => {
|
|
filter2.inputEl.classList.toggle("mod-error", !!message);
|
|
if (message) {
|
|
filter2.inputEl.setAttribute("aria-label", message);
|
|
} else {
|
|
filter2.inputEl.removeAttribute("aria-label");
|
|
}
|
|
};
|
|
if (!this.viewOnly) {
|
|
new import_obsidian22.ButtonComponent(controlsEl).setIcon("lucide-plus").setTooltip("New Callout").onClick(() => this.nav.open(new CreateCalloutPane(this.plugin))).then(({ buttonEl }) => buttonEl.classList.add("clickable-icon"));
|
|
}
|
|
}
|
|
/** @override */
|
|
restoreState(state) {
|
|
this.invalidate();
|
|
}
|
|
/** @override */
|
|
onReady() {
|
|
this.invalidate();
|
|
}
|
|
};
|
|
function createPreviewFactory(viewOnly) {
|
|
const editButtonContent = (viewOnly ? (0, import_obsidian22.getIcon)("lucide-view") : (0, import_obsidian22.getIcon)("lucide-edit")) ?? document.createTextNode("Edit Callout");
|
|
const insertButtonContent = (viewOnly ? (0, import_obsidian22.getIcon)("lucide-view") : (0, import_obsidian22.getIcon)("lucide-forward")) ?? document.createTextNode("Insert Callout");
|
|
return (callout) => {
|
|
const frag = document.createDocumentFragment();
|
|
const calloutContainerEl = frag.createDiv({
|
|
cls: ["calloutmanager-preview-container"],
|
|
attr: {
|
|
["data-callout-manager-callout"]: callout.id
|
|
}
|
|
});
|
|
new CalloutPreviewComponent(calloutContainerEl, {
|
|
id: callout.id,
|
|
icon: callout.icon,
|
|
title: getTitleFromCallout(callout),
|
|
color: getColorFromCallout(callout) ?? void 0
|
|
});
|
|
calloutContainerEl.classList.add("calloutmanager-preview-container-with-button");
|
|
const editButton = calloutContainerEl.createEl("button");
|
|
editButton.setAttribute("data-callout-manager-action", "edit");
|
|
editButton.appendChild(editButtonContent.cloneNode(true));
|
|
const insertButton = calloutContainerEl.createEl("button");
|
|
insertButton.setAttribute("data-callout-manager-action", "insert");
|
|
insertButton.appendChild(insertButtonContent.cloneNode(true));
|
|
return calloutContainerEl;
|
|
};
|
|
}
|
|
function createEmptySearchResultDiv() {
|
|
let searchErrorQuery;
|
|
const searchErrorDiv = document.createElement("div");
|
|
searchErrorDiv.className = "calloutmanager-centerbox";
|
|
const contentEl = searchErrorDiv.createDiv({ cls: "calloutmanager-search-error" });
|
|
contentEl.createEl("h2", { text: "No callouts found." });
|
|
contentEl.createEl("p", void 0, (el) => {
|
|
el.createSpan({ text: "Your search query " });
|
|
searchErrorQuery = el.createEl("code", { text: "" });
|
|
el.createSpan({ text: " did not return any results." });
|
|
});
|
|
contentEl.createDiv({ cls: "calloutmanager-search-error-suggestions" }, (el) => {
|
|
el.createDiv({ text: "Try searching:" });
|
|
el.createEl("ul", void 0, (el2) => {
|
|
el2.createEl("li", { text: "By name: " }, (el3) => {
|
|
el3.createEl("code", { text: "warning" });
|
|
});
|
|
el2.createEl("li", { text: "By icon: " }, (el3) => {
|
|
el3.createEl("code", { text: "icon:check" });
|
|
});
|
|
el2.createEl("li", { text: "Built-in callouts: " }, (el3) => {
|
|
el3.createEl("code", { text: "from:obsidian" });
|
|
});
|
|
el2.createEl("li", { text: "Theme callouts: " }, (el3) => {
|
|
el3.createEl("code", { text: "from:theme" });
|
|
});
|
|
el2.createEl("li", { text: "Snippet callouts: " }, (el3) => {
|
|
el3.createEl("code", { text: "from:my snippet" });
|
|
});
|
|
el2.createEl("li", { text: "Custom callouts: " }, (el3) => {
|
|
el3.createEl("code", { text: "from:custom" });
|
|
});
|
|
});
|
|
});
|
|
return { searchErrorDiv, searchErrorQuery };
|
|
}
|
|
|
|
// src/panes/manage-plugin-pane.ts
|
|
var import_obsidian24 = require("obsidian");
|
|
|
|
// src/changelog.ts
|
|
var import_obsidian23 = require("obsidian");
|
|
|
|
// CHANGELOG.md
|
|
var CHANGELOG_default = `# Version 1.1.0
|
|
|
|
> [!new] In-App Changelogs
|
|
> Learn about plugin changes and new features straight from the horse's mouth.
|
|
|
|
> [!new] Insert Callouts
|
|
> Goodbye to the days of needing to type callouts by hand, and hello to having more ways to suit your workflow. You now have the option to insert callouts directly from the "Manage Callouts" pane!
|
|
>
|
|
> Thank you, [**@decheine**](https://github.com/decheine)!
|
|
|
|
> [!new] Color Dropdown
|
|
> Pick from a nifty dropdown instead of memorizing color values.
|
|
>
|
|
> Thank you, [**@decheine**](https://github.com/decheine)!
|
|
|
|
> [!new] Rename Callouts
|
|
> You can now rename your custom callouts.
|
|
|
|
> [!fix] More Robust Callout Detection
|
|
> The code monkey (developer) learned a couple new tricks, and now Callout Manager can detect Obsidian callouts on all platforms and versions without resorting to fallback lists.
|
|
|
|
> [!fix] Integration with Completr
|
|
> You can now use [Completr](obsidian://show-plugin?id=obsidian-completr) to autocomplete callouts!
|
|
|
|
# Version 1.0.1
|
|
The first release available on Obsidian's community plugin browser!
|
|
|
|
# Version 1.0.0
|
|
|
|
> [!new] Callout Customization
|
|
> Change callouts to your heart's content!
|
|
|
|
> [!new] Automatic Detection
|
|
> Browse and search through your one and only list of available callouts.
|
|
`;
|
|
|
|
// src/changelog.ts
|
|
function getSections() {
|
|
const frag = document.createDocumentFragment();
|
|
const renderedEl = frag.createDiv();
|
|
import_obsidian23.MarkdownRenderer.renderMarkdown(CHANGELOG_default, renderedEl, "", null);
|
|
const sections = /* @__PURE__ */ new Map();
|
|
let heading = null;
|
|
let sectionContainer = frag.createEl("details");
|
|
let sectionSummary = sectionContainer.createEl("summary");
|
|
let sectionContents = [];
|
|
const addPreviousSection = () => {
|
|
if (heading != null && heading.textContent !== null) {
|
|
const headingText = heading.textContent;
|
|
const titleEl = sectionSummary.createEl("h2", {
|
|
cls: "calloutmanager-changelog-heading",
|
|
text: headingText
|
|
});
|
|
const contentsEl = sectionContainer.createDiv(
|
|
{
|
|
cls: "calloutmanager-changelog-section"
|
|
},
|
|
(el) => {
|
|
sectionContents.forEach((node) => el.appendChild(node));
|
|
}
|
|
);
|
|
Array.from(contentsEl.querySelectorAll(".callout[data-callout]")).forEach((el) => {
|
|
el.setAttribute("data-calloutmanager-changelog-callout", el.getAttribute("data-callout"));
|
|
el.removeAttribute("data-callout");
|
|
});
|
|
const version = /^\s*Version ([0-9.]+)\s*$/.exec(headingText)?.[1];
|
|
sections.set(version ?? heading.textContent, {
|
|
version,
|
|
contentsEl,
|
|
containerEl: sectionContainer,
|
|
titleEl
|
|
});
|
|
}
|
|
heading = null;
|
|
sectionContainer = frag.createEl("details");
|
|
sectionSummary = sectionContainer.createEl("summary");
|
|
sectionContents = [];
|
|
};
|
|
for (let node = renderedEl.firstChild; node != null; node = node?.nextSibling) {
|
|
if (node instanceof HTMLHeadingElement && node.tagName === "H1") {
|
|
addPreviousSection();
|
|
heading = node;
|
|
continue;
|
|
}
|
|
sectionContents.push(node);
|
|
}
|
|
addPreviousSection();
|
|
return sections;
|
|
}
|
|
|
|
// src/panes/changelog-pane.ts
|
|
var ChangelogPane = class extends UIPane {
|
|
constructor(plugin) {
|
|
super();
|
|
this.title = "Changelog";
|
|
this.plugin = plugin;
|
|
const sections = getSections();
|
|
const frag = document.createDocumentFragment();
|
|
this.changelogEl = frag.createDiv({ cls: "calloutmanager-changelog" });
|
|
Array.from(sections.values()).forEach(({ version, containerEl: el }) => {
|
|
this.changelogEl.appendChild(el);
|
|
if (version === this.plugin.manifest.version) {
|
|
el.setAttribute("open", "");
|
|
el.setAttribute("data-current-version", "true");
|
|
}
|
|
});
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const { containerEl } = this;
|
|
containerEl.appendChild(this.changelogEl);
|
|
}
|
|
};
|
|
|
|
// src/panes/manage-plugin-pane.ts
|
|
var ManagePluginPane = class extends UIPane {
|
|
constructor(plugin) {
|
|
super();
|
|
this.title = "Callout Manager Settings";
|
|
this.plugin = plugin;
|
|
}
|
|
/** @override */
|
|
display() {
|
|
const { containerEl, plugin } = this;
|
|
new import_obsidian24.Setting(containerEl).setName("Manage Callouts").setDesc("Create or edit Markdown callouts.").addButton((btn) => {
|
|
btn.setButtonText("Manage Callouts");
|
|
btn.onClick(() => this.nav.open(new ManageCalloutsPane(plugin)));
|
|
});
|
|
new import_obsidian24.Setting(containerEl).setHeading().setName("Callout Detection");
|
|
new import_obsidian24.Setting(containerEl).setName("Obsidian").setDesc(
|
|
(() => {
|
|
const desc = document.createDocumentFragment();
|
|
const container = desc.createDiv();
|
|
const method = plugin.cssWatcher.describeObsidianFetchMethod();
|
|
container.createDiv({
|
|
text: `Find built-in Obsidian callouts${method === "" ? "" : " "}${method}.`
|
|
});
|
|
return desc;
|
|
})()
|
|
).addToggle((setting) => {
|
|
setting.setValue(plugin.settings.calloutDetection.obsidian).onChange((v) => {
|
|
plugin.settings.calloutDetection.obsidian = v;
|
|
plugin.saveSettings();
|
|
plugin.refreshCalloutSources();
|
|
});
|
|
});
|
|
new import_obsidian24.Setting(containerEl).setName("Theme").setDesc("Find theme-provided callouts.").addToggle((setting) => {
|
|
setting.setValue(plugin.settings.calloutDetection.theme).onChange((v) => {
|
|
plugin.settings.calloutDetection.theme = v;
|
|
plugin.saveSettings();
|
|
plugin.refreshCalloutSources();
|
|
});
|
|
});
|
|
new import_obsidian24.Setting(containerEl).setName("Snippet").setDesc("Find callouts in custom CSS snippets.").addToggle((setting) => {
|
|
setting.setValue(plugin.settings.calloutDetection.snippet).onChange((v) => {
|
|
plugin.settings.calloutDetection.snippet = v;
|
|
plugin.saveSettings();
|
|
plugin.refreshCalloutSources();
|
|
});
|
|
});
|
|
new import_obsidian24.Setting(containerEl).setHeading().setName("What's New").setDesc(`Version ${this.plugin.manifest.version}`).addExtraButton((btn) => {
|
|
btn.setIcon("lucide-more-horizontal").setTooltip("More Changelogs").onClick(() => this.nav.open(new ChangelogPane(plugin)));
|
|
});
|
|
const latestChanges = getSections().get(this.plugin.manifest.version);
|
|
if (latestChanges != null) {
|
|
const desc = document.createDocumentFragment();
|
|
desc.appendChild(latestChanges.contentsEl);
|
|
new import_obsidian24.Setting(containerEl).setDesc(desc).then((setting) => setting.controlEl.remove()).then((setting) => setting.settingEl.classList.add("calloutmanager-latest-changes"));
|
|
}
|
|
new import_obsidian24.Setting(containerEl).setHeading().setName("Reset");
|
|
new import_obsidian24.Setting(containerEl).setName("Reset Callout Settings").setDesc("Reset all the changes you made to callouts.").addButton(
|
|
withConfirm((btn) => {
|
|
btn.setButtonText("Reset").onClick(() => {
|
|
this.plugin.settings.callouts.settings = {};
|
|
this.plugin.saveSettings();
|
|
this.plugin.applyStyles();
|
|
btn.setButtonText("Reset").setDisabled(true);
|
|
});
|
|
})
|
|
);
|
|
new import_obsidian24.Setting(containerEl).setName("Reset Custom Callouts").setDesc("Removes all the custom callouts you created.").addButton(
|
|
withConfirm((btn) => {
|
|
btn.setButtonText("Reset").onClick(() => {
|
|
const { settings } = this.plugin;
|
|
for (const custom of settings.callouts.custom) {
|
|
delete settings.callouts.settings[custom];
|
|
}
|
|
settings.callouts.custom = [];
|
|
this.plugin.saveSettings();
|
|
this.plugin.callouts.custom.clear();
|
|
this.plugin.applyStyles();
|
|
this.plugin.refreshCalloutSources();
|
|
btn.setButtonText("Reset").setDisabled(true);
|
|
});
|
|
})
|
|
);
|
|
}
|
|
};
|
|
function withConfirm(callback) {
|
|
let onClickHandler = void 0;
|
|
let resetButtonClicked = false;
|
|
return (btn) => {
|
|
btn.setWarning().onClick(() => {
|
|
if (!resetButtonClicked) {
|
|
resetButtonClicked = true;
|
|
btn.setButtonText("Confirm");
|
|
return;
|
|
}
|
|
if (onClickHandler != void 0) {
|
|
onClickHandler();
|
|
}
|
|
});
|
|
btn.onClick = (handler) => {
|
|
onClickHandler = handler;
|
|
return btn;
|
|
};
|
|
callback(btn);
|
|
};
|
|
}
|
|
|
|
// src/settings.ts
|
|
function defaultSettings() {
|
|
return {
|
|
callouts: {
|
|
custom: [],
|
|
settings: {}
|
|
},
|
|
calloutDetection: {
|
|
obsidian: true,
|
|
theme: true,
|
|
snippet: true
|
|
}
|
|
};
|
|
}
|
|
function migrateSettings(into, from) {
|
|
const merged = Object.assign(into, {
|
|
...from,
|
|
calloutDetection: {
|
|
...into.calloutDetection,
|
|
...from?.calloutDetection ?? {}
|
|
}
|
|
});
|
|
delete merged.calloutDetection.obsidianFallbackForced;
|
|
return merged;
|
|
}
|
|
|
|
// src/main.ts
|
|
var CalloutManagerPlugin = class extends import_obsidian25.Plugin {
|
|
constructor() {
|
|
super(...arguments);
|
|
this.apiReadyWait = new Promise((resolve, reject) => this.apiReadySignal = resolve);
|
|
}
|
|
/** @override */
|
|
async onload() {
|
|
await this.loadSettings();
|
|
await this.saveSettings();
|
|
const { settings } = this;
|
|
this.calloutResolver = new CalloutResolver();
|
|
this.register(() => this.calloutResolver.unload());
|
|
this.callouts = new CalloutCollection((id) => {
|
|
const { icon, color } = this.calloutResolver.getCalloutProperties(id);
|
|
console.debug("Resolved Callout:", id, { icon, color });
|
|
return {
|
|
id,
|
|
icon,
|
|
color
|
|
};
|
|
});
|
|
this.callouts.custom.add(...settings.callouts.custom);
|
|
this.cssApplier = createCustomStyleSheet(this.app, this);
|
|
this.cssApplier.setAttribute("data-callout-manager", "style-overrides");
|
|
this.register(this.cssApplier);
|
|
this.applyStyles();
|
|
this.cssWatcher = new StylesheetWatcher(this.app);
|
|
this.cssWatcher.on("add", this.updateCalloutSource.bind(this));
|
|
this.cssWatcher.on("change", this.updateCalloutSource.bind(this));
|
|
this.cssWatcher.on("remove", this.removeCalloutSource.bind(this));
|
|
this.cssWatcher.on("checkComplete", () => {
|
|
this.ensureChangedCalloutsKnown();
|
|
});
|
|
this.cssWatcher.on("checkComplete", (anyChanges) => {
|
|
if (anyChanges) {
|
|
this.api.emitEventForCalloutChange();
|
|
}
|
|
});
|
|
this.app.workspace.onLayoutReady(() => {
|
|
this.register(this.cssWatcher.watch());
|
|
});
|
|
this.registerEvent(
|
|
this.app.workspace.on("css-change", () => {
|
|
this.calloutResolver.reloadStyles();
|
|
this.applyStyles();
|
|
})
|
|
);
|
|
this.settingTab = new UISettingTab(this, () => new ManagePluginPane(this));
|
|
this.addSettingTab(this.settingTab);
|
|
this.addCommand({
|
|
id: "manage-callouts",
|
|
name: "Edit callouts",
|
|
callback: () => {
|
|
this.settingTab.openWithPane(new ManageCalloutsPane(this));
|
|
}
|
|
});
|
|
this.api = new CalloutManagerAPIs(this);
|
|
this.apiReadySignal();
|
|
this.addRibbonIcon("lucide-gallery-vertical", "Insert Callout", () => {
|
|
this.settingTab.openWithPane(new ManageCalloutsPane(this));
|
|
});
|
|
}
|
|
async loadSettings() {
|
|
this.settings = migrateSettings(defaultSettings(), await this.loadData());
|
|
}
|
|
async saveSettings() {
|
|
await this.saveData(this.settings);
|
|
}
|
|
/**
|
|
* Takes in a stylesheet from the watcher and updates the callout collection.
|
|
* @param ss The stylesheet.
|
|
*/
|
|
updateCalloutSource(ss) {
|
|
const callouts = getCalloutsFromCSS(ss.styles);
|
|
const { calloutDetection } = this.settings;
|
|
switch (ss.type) {
|
|
case "obsidian":
|
|
if (calloutDetection.obsidian === true && !calloutDetection.obsidianFallbackForced) {
|
|
callouts.push("note");
|
|
this.callouts.builtin.set(callouts);
|
|
}
|
|
return;
|
|
case "theme":
|
|
if (calloutDetection.theme) {
|
|
this.callouts.theme.set(ss.theme, callouts);
|
|
}
|
|
return;
|
|
case "snippet":
|
|
if (calloutDetection.snippet) {
|
|
this.callouts.snippets.set(ss.snippet, callouts);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
/**
|
|
* Forces the callout sources to be refreshed.
|
|
* This is used to re-detect the sources when settings are changed.
|
|
*/
|
|
refreshCalloutSources() {
|
|
this.callouts.snippets.clear();
|
|
this.callouts.theme.delete();
|
|
this.callouts.builtin.set([]);
|
|
this.cssWatcher.checkForChanges(true).then(() => {
|
|
this.ensureChangedCalloutsKnown();
|
|
});
|
|
}
|
|
/**
|
|
* Ensures that any callouts which have changed settings are detected by the plugin.
|
|
*
|
|
* If a non-builtin callout was configured by the user and then removed, this plugin should consider
|
|
* the callout to be custom so it can be seen in the list.
|
|
*/
|
|
ensureChangedCalloutsKnown() {
|
|
let hasAddedCallout = false;
|
|
const { settings, callouts } = this;
|
|
for (const [id, changes] of Object.entries(settings.callouts.settings)) {
|
|
if (!callouts.has(id) && changes.length > 0) {
|
|
hasAddedCallout = true;
|
|
callouts.custom.add(id);
|
|
}
|
|
}
|
|
if (hasAddedCallout) {
|
|
this.saveCustomCallouts();
|
|
this.api.emitEventForCalloutChange();
|
|
}
|
|
}
|
|
saveCustomCallouts() {
|
|
const { callouts, settings } = this;
|
|
settings.callouts.custom = callouts.custom.keys();
|
|
return this.saveSettings();
|
|
}
|
|
/**
|
|
* Create a custom callout and add it to Obsidian.
|
|
* @param id The custom callout ID.
|
|
*/
|
|
createCustomCallout(id) {
|
|
const { callouts } = this;
|
|
callouts.custom.add(id);
|
|
this.saveCustomCallouts();
|
|
this.api.emitEventForCalloutChange(id);
|
|
}
|
|
/**
|
|
* Rename a custom callout.
|
|
*
|
|
* @param oldId The old callout ID.
|
|
* @param newId The new callout ID.
|
|
* @throws If the callout has any other sources than "custom".
|
|
* @throws If the old ID does not exist.
|
|
* @throws If the new ID already exists.
|
|
*/
|
|
renameCustomCallout(oldId, newId) {
|
|
const { callouts, settings } = this;
|
|
const callout = callouts.get(oldId);
|
|
if (callout == null)
|
|
throw new Error(`Callout '${oldId}' does not exist.`);
|
|
if (callouts.get(newId) != null)
|
|
throw new Error(`Callout '${newId}' already exists.`);
|
|
if (callout.sources.length !== 1 || callout.sources[0].type !== "custom") {
|
|
throw new Error(`Cannot rename non-custom callout '${oldId}'.`);
|
|
}
|
|
callouts.custom.delete(oldId);
|
|
callouts.custom.add(newId);
|
|
settings.callouts.custom = callouts.custom.keys();
|
|
settings.callouts.settings[newId] = settings.callouts.settings[oldId];
|
|
delete settings.callouts.settings[oldId];
|
|
this.applyStyles();
|
|
this.saveCustomCallouts();
|
|
this.api.emitEventForCalloutChange(oldId);
|
|
this.api.emitEventForCalloutChange(newId);
|
|
}
|
|
/**
|
|
* Delete a custom callout.
|
|
* If there are no other sources for the callout, its settings will be purged.
|
|
*
|
|
* @param id The custom callout ID.
|
|
*/
|
|
removeCustomCallout(id) {
|
|
const { callouts, settings } = this;
|
|
callouts.custom.delete(id);
|
|
settings.callouts.custom = callouts.custom.keys();
|
|
const calloutInstance = callouts.get(id);
|
|
if (calloutInstance == null || calloutInstance.sources.length < 1) {
|
|
delete settings.callouts.settings[id];
|
|
this.applyStyles();
|
|
}
|
|
this.saveSettings();
|
|
this.api.emitEventForCalloutChange(id);
|
|
}
|
|
/**
|
|
* Gets the custom settings for a callout.
|
|
*
|
|
* @param id The callout ID.
|
|
* @returns The custom settings, or undefined if there are none.
|
|
*/
|
|
getCalloutSettings(id) {
|
|
const calloutSettings = this.settings.callouts.settings;
|
|
if (!Object.prototype.hasOwnProperty.call(calloutSettings, id)) {
|
|
return void 0;
|
|
}
|
|
return calloutSettings[id];
|
|
}
|
|
/**
|
|
* Sets the custom settings for a callout.
|
|
*
|
|
* @param id The callout ID.
|
|
* @param settings The callout settings.
|
|
*/
|
|
setCalloutSettings(id, settings) {
|
|
const calloutSettings = this.settings.callouts.settings;
|
|
if (settings === void 0 || settings.length < 1) {
|
|
delete calloutSettings[id];
|
|
} else {
|
|
calloutSettings[id] = settings;
|
|
}
|
|
this.saveSettings();
|
|
this.applyStyles();
|
|
this.callouts.invalidate(id);
|
|
this.api.emitEventForCalloutChange(id);
|
|
}
|
|
/**
|
|
* Generates the stylesheet for the user's custom callout settings and applies it to the page and the callout
|
|
* resolver's custom stylesheet.
|
|
*/
|
|
applyStyles() {
|
|
const env = currentCalloutEnvironment(this.app);
|
|
const css = [];
|
|
for (const [id, settings] of Object.entries(this.settings.callouts.settings)) {
|
|
css.push(calloutSettingsToCSS(id, settings, env));
|
|
}
|
|
const stylesheet = css.join("\n\n");
|
|
this.cssApplier.css = stylesheet;
|
|
this.calloutResolver.customStyleEl.textContent = stylesheet;
|
|
}
|
|
/**
|
|
* Takes in a stylesheet from the watcher and removes its callouts from the callout collection.
|
|
* @param ss The stylesheet.
|
|
*/
|
|
removeCalloutSource(ss) {
|
|
switch (ss.type) {
|
|
case "obsidian":
|
|
this.callouts.builtin.set([]);
|
|
return;
|
|
case "theme":
|
|
this.callouts.theme.delete();
|
|
return;
|
|
case "snippet":
|
|
this.callouts.snippets.delete(ss.snippet);
|
|
return;
|
|
}
|
|
}
|
|
/**
|
|
* Creates (or gets) an instance of the Callout Manager API for a plugin.
|
|
* If the plugin is undefined, only trivial functions are available.
|
|
*
|
|
* @param version The API version.
|
|
* @param consumerPlugin The plugin using the API.
|
|
*
|
|
* @internal
|
|
*/
|
|
async newApiHandle(version, consumerPlugin, cleanupFunc) {
|
|
await this.apiReadyWait;
|
|
return this.api.newHandle(version, consumerPlugin, cleanupFunc);
|
|
}
|
|
/**
|
|
* Destroys an API handle created by {@link newApiHandle}.
|
|
*
|
|
* @param version The API version.
|
|
* @param consumerPlugin The plugin using the API.
|
|
*
|
|
* @internal
|
|
*/
|
|
destroyApiHandle(version, consumerPlugin) {
|
|
if (version !== "v1")
|
|
throw new Error(`Unsupported Callout Manager API: ${version}`);
|
|
return this.api.destroyHandle(version, consumerPlugin);
|
|
}
|
|
};
|