/* eslint-disable @typescript-eslint/no-unsafe-function-type */ export const SettingCache = new Map(); //@ts-ignore obsidian global globalThis.activeDocument = document; declare const hostPlatform: string | undefined; // import { interceptFetchForLogging } from "../harness/utils/intercept"; // interceptFetchForLogging(); globalThis.process = { platform: (hostPlatform || "win32") as any, } as any; console.warn(`[Obsidian Mock] process.platform is set to ${globalThis.process.platform}`); export class TAbstractFile { vault: Vault; path: string; name: string; parent: TFolder | null; constructor(vault: Vault, path: string, name: string, parent: TFolder | null) { this.vault = vault; this.path = path; this.name = name; this.parent = parent; } } export class TFile extends TAbstractFile { stat: { ctime: number; mtime: number; size: number; } = { ctime: Date.now(), mtime: Date.now(), size: 0 }; get extension(): string { return this.name.split(".").pop() || ""; } get basename(): string { const parts = this.name.split("."); if (parts.length > 1) parts.pop(); return parts.join("."); } } export class TFolder extends TAbstractFile { children: TAbstractFile[] = []; get isRoot(): boolean { return this.path === "" || this.path === "/"; } } export class EventRef {} // class StorageMap extends Map { // constructor(saveName?: string) { // super(); // if (saveName) { // this.saveName = saveName; // void this.restore(saveName); // } // } // private saveName: string = ""; // async restore(saveName: string) { // this.saveName = saveName; // const db = await OpenKeyValueDatabase(saveName); // const data = await db.get<{ [key: string]: U }>("data"); // if (data) { // for (const key of Object.keys(data)) { // this.set(key as any as T, data[key]); // } // } // db.close(); // return this; // } // saving: boolean = false; // async save() { // if (this.saveName === "") { // return; // } // if (this.saving) { // return; // } // try { // this.saving = true; // const db = await OpenKeyValueDatabase(this.saveName); // const data: { [key: string]: U } = {}; // for (const [key, value] of this.entries()) { // data[key as any as string] = value; // } // await db.set("data", data); // db.close(); // } finally { // this.saving = false; // } // } // set(key: T, value: U): this { // super.set(key, value); // void this.save(); // return this; // } // } export class Vault { adapter: DataAdapter; vaultName: string = "MockVault"; private files: Map = new Map(); private contents: Map = new Map(); private root: TFolder; private listeners: Map> = new Map(); constructor(vaultName?: string) { if (vaultName) { this.vaultName = vaultName; } this.files = new Map(); this.contents = new Map(); this.adapter = new DataAdapter(this); this.root = new TFolder(this, "", "", null); this.files.set("", this.root); } getAbstractFileByPath(path: string): TAbstractFile | null { if (path === "/") path = ""; const file = this.files.get(path); return file || null; } getAbstractFileByPathInsensitive(path: string): TAbstractFile | null { const lowerPath = path.toLowerCase(); for (const [p, file] of this.files.entries()) { if (p.toLowerCase() === lowerPath) { return file; } } return null; } getFiles(): TFile[] { return Array.from(this.files.values()).filter((f) => f instanceof TFile); } async _adapterRead(path: string): Promise { await Promise.resolve(); const file = this.contents.get(path); if (typeof file === "string") { return file; } if (file instanceof ArrayBuffer) { return new TextDecoder().decode(file); } return null; } async _adapterReadBinary(path: string): Promise { await Promise.resolve(); const file = this.contents.get(path); if (file instanceof ArrayBuffer) { return file; } if (typeof file === "string") { return new TextEncoder().encode(file).buffer; } return null; } async read(file: TFile): Promise { await Promise.resolve(); const content = this.contents.get(file.path); if (typeof content === "string") return content; if (content instanceof ArrayBuffer) { return new TextDecoder().decode(content); } return ""; } async readBinary(file: TFile): Promise { await Promise.resolve(); const content = this.contents.get(file.path); if (content instanceof ArrayBuffer) return content; if (typeof content === "string") { return new TextEncoder().encode(content).buffer; } return new ArrayBuffer(0); } private async _create(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise { if (this.files.has(path)) throw new Error("File already exists"); const name = path.split("/").pop() || ""; const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ""; let parent = this.getAbstractFileByPath(parentPath); if (!parent || !(parent instanceof TFolder)) { parent = await this.createFolder(parentPath); } const file = new TFile(this, path, name, parent as TFolder); file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength; file.stat.ctime = options?.ctime ?? Date.now(); file.stat.mtime = options?.mtime ?? Date.now(); this.files.set(path, file); this.contents.set(path, data); (parent as TFolder).children.push(file); // console.dir(this.files); this.trigger("create", file); return file; } async create(path: string, data: string, options?: DataWriteOptions): Promise { return await this._create(path, data, options); } async createBinary(path: string, data: ArrayBuffer, options?: DataWriteOptions): Promise { return await this._create(path, data, options); } async _modify(file: TFile, data: string | ArrayBuffer, options?: DataWriteOptions): Promise { await Promise.resolve(); this.contents.set(file.path, data); file.stat.mtime = options?.mtime ?? Date.now(); file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now(); file.stat.size = typeof data === "string" ? data.length : data.byteLength; console.warn(`[Obsidian Mock ${this.vaultName}] Modified file at path: '${file.path}'`); this.files.set(file.path, file); this.trigger("modify", file); } async modify(file: TFile, data: string, options?: DataWriteOptions): Promise { return await this._modify(file, data, options); } async modifyBinary(file: TFile, data: ArrayBuffer, options?: DataWriteOptions): Promise { return await this._modify(file, data, options); } async createFolder(path: string): Promise { if (path === "") return this.root; if (this.files.has(path)) { const f = this.files.get(path); if (f instanceof TFolder) return f; throw new Error("Path is a file"); } const name = path.split("/").pop() || ""; const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : ""; const parent = await this.createFolder(parentPath); const folder = new TFolder(this, path, name, parent); this.files.set(path, folder); parent.children.push(folder); return folder; } async delete(file: TAbstractFile, force?: boolean): Promise { await Promise.resolve(); this.files.delete(file.path); this.contents.delete(file.path); if (file.parent) { file.parent.children = file.parent.children.filter((c) => c !== file); } this.trigger("delete", file); } async trash(file: TAbstractFile, system: boolean): Promise { await Promise.resolve(); return this.delete(file); } on(name: string, callback: (...args: any[]) => any, ctx?: any): EventRef { if (!this.listeners.has(name)) { this.listeners.set(name, new Set()); } const boundCallback = ctx ? callback.bind(ctx) : callback; this.listeners.get(name)!.add(boundCallback); return { name, callback: boundCallback } as any; } off(name: string, callback: any) { this.listeners.get(name)?.delete(callback); } offref(ref: EventRef) { const { name, callback } = ref as any; this.off(name, callback); } trigger(name: string, ...args: any[]) { this.listeners.get(name)?.forEach((cb) => cb(...args)); } getName(): string { return this.vaultName; } } export class DataAdapter { vault: Vault; constructor(vault: Vault) { this.vault = vault; } stat(path: string): Promise<{ ctime: number; mtime: number; size: number }> { const file = this.vault.getAbstractFileByPath(path); if (file && file instanceof TFile) { return Promise.resolve({ ctime: file.stat.ctime, mtime: file.stat.mtime, size: file.stat.size, }); } return Promise.reject(new Error("File not found")); } async list(path: string): Promise<{ files: string[]; folders: string[] }> { await Promise.resolve(); const abstractFile = this.vault.getAbstractFileByPath(path); if (abstractFile instanceof TFolder) { const files: string[] = []; const folders: string[] = []; for (const child of abstractFile.children) { if (child instanceof TFile) files.push(child.path); else if (child instanceof TFolder) folders.push(child.path); } return { files, folders }; } return { files: [], folders: [] }; } async _write(path: string, data: string | ArrayBuffer, options?: DataWriteOptions): Promise { const file = this.vault.getAbstractFileByPath(path); if (file instanceof TFile) { if (typeof data === "string") { await this.vault.modify(file, data, options); } else { await this.vault.modifyBinary(file, data, options); } } else { if (typeof data === "string") { await this.vault.create(path, data, options); } else { await this.vault.createBinary(path, data, options); } } } async write(path: string, data: string, options?: DataWriteOptions): Promise { return await this._write(path, data, options); } async writeBinary(path: string, data: ArrayBuffer, options?: DataWriteOptions): Promise { return await this._write(path, data, options); } async read(path: string): Promise { const file = this.vault.getAbstractFileByPath(path); if (file instanceof TFile) return await this.vault.read(file); throw new Error("File not found"); } async readBinary(path: string): Promise { const file = this.vault.getAbstractFileByPath(path); if (file instanceof TFile) return await this.vault.readBinary(file); throw new Error("File not found"); } async exists(path: string): Promise { await Promise.resolve(); return this.vault.getAbstractFileByPath(path) !== null; } async mkdir(path: string): Promise { await this.vault.createFolder(path); } async remove(path: string): Promise { const file = this.vault.getAbstractFileByPath(path); if (file) await this.vault.delete(file); } } class Events { _eventEmitter = new EventTarget(); _events = new Map(); _eventTarget(cb: any) { const x = this._events.get(cb); if (x) { return x; } const callback = (evt: any) => { x(evt?.detail ?? undefined); }; this._events.set(cb, callback); return callback; } on(name: string, cb: any, ctx?: any) { this._eventEmitter.addEventListener(name, this._eventTarget(cb)); } trigger(name: string, args: any) { const evt = new CustomEvent(name, { detail: args, }); this._eventEmitter.dispatchEvent(evt); } } class Workspace extends Events { getActiveFile() { return null; } getMostRecentLeaf() { return null; } onLayoutReady(cb: any) { // cb(); // console.log("[Obsidian Mock] Workspace onLayoutReady registered"); // this._eventEmitter.addEventListener("layout-ready", () => { // console.log("[Obsidian Mock] Workspace layout-ready event triggered"); setTimeout(() => { cb(); }, 200); // }); } getLeavesOfType() { return []; } getLeaf() { return { setViewState: () => Promise.resolve(), revealLeaf: () => Promise.resolve() }; } revealLeaf() { return Promise.resolve(); } containerEl: HTMLElement = document.createElement("div"); } export class App { vaultName: string = "MockVault"; constructor(vaultName?: string) { if (vaultName) { this.vaultName = vaultName; } this.vault = new Vault(this.vaultName); } vault: Vault; workspace: Workspace = new Workspace(); metadataCache: any = { on: (name: string, cb: any, ctx?: any) => {}, getFileCache: () => null, }; } export class Plugin { app: App; manifest: any; settings: any; commands: Map = new Map(); constructor(app: App, manifest: any) { this.app = app; this.manifest = manifest; } async loadData(): Promise { await Promise.resolve(); return SettingCache.get(this.app) ?? {}; } async saveData(data: any): Promise { await Promise.resolve(); SettingCache.set(this.app, data); } onload() {} onunload() {} addSettingTab(tab: any) {} addCommand(command: any) { this.commands.set(command.id, command); } addStatusBarItem() { return { setText: () => {}, setClass: () => {}, addClass: () => {}, }; } addRibbonIcon() { const icon = { setAttribute: () => icon, addClass: () => icon, onclick: () => {}, }; return icon; } registerView(type: string, creator: any) {} registerObsidianProtocolHandler(handler: any) {} registerEvent(handler: any) {} registerDomEvent(target: any, eventName: string, handler: any) {} } export class Notice { constructor(message: string) { console.log("Notice:", message); } } export class Modal { app: App; contentEl: HTMLElement; titleEl: HTMLElement; modalEl: HTMLElement; isOpen: boolean = false; constructor(app: App) { this.app = app; this.contentEl = document.createElement("div"); this.contentEl.className = "modal-content"; this.titleEl = document.createElement("div"); this.titleEl.className = "modal-title"; this.modalEl = document.createElement("div"); this.modalEl.className = "modal"; this.modalEl.style.display = "none"; this.modalEl.appendChild(this.titleEl); this.modalEl.appendChild(this.contentEl); } open() { this.isOpen = true; this.modalEl.style.display = "block"; if (!this.modalEl.parentElement) { document.body.appendChild(this.modalEl); } this.onOpen(); } close() { this.isOpen = false; this.modalEl.style.display = "none"; this.onClose(); } onOpen() {} onClose() {} setPlaceholder(p: string) {} setTitle(t: string) { this.titleEl.textContent = t; } } export class PluginSettingTab { app: App; plugin: Plugin; containerEl: HTMLElement; constructor(app: App, plugin: Plugin) { this.app = app; this.plugin = plugin; this.containerEl = document.createElement("div"); } display() {} } export function normalizePath(path: string): string { return path.replace(/\\/g, "/").replace(/\/+$/, ""); } export const Platform = { isDesktop: true, isMobile: false, }; export class Menu { addItem(cb: (item: MenuItem) => any) { cb(new MenuItem()); return this; } showAtMouseEvent(evt: MouseEvent) {} } export class MenuItem { setTitle(title: string) { return this; } setIcon(icon: string) { return this; } onClick(cb: (evt: MouseEvent) => any) { return this; } } export class MenuSeparator {} export class Component { load() {} unload() {} } export class ButtonComponent extends Component { buttonEl: HTMLButtonElement = document.createElement("button"); private clickHandler: ((evt: MouseEvent) => any) | null = null; constructor() { super(); this.buttonEl = document.createElement("button"); this.buttonEl.type = "button"; } setButtonText(text: string) { this.buttonEl.textContent = text; return this; } setCta() { this.buttonEl.classList.add("mod-cta"); return this; } onClick(cb: (evt: MouseEvent) => any) { this.clickHandler = cb; this.buttonEl.removeEventListener("click", this.clickHandler); this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent)); return this; } setClass(c: string) { this.buttonEl.classList.add(c); return this; } setTooltip(tooltip: string) { this.buttonEl.title = tooltip; return this; } setDisabled(disabled: boolean) { this.buttonEl.disabled = disabled; return this; } } export class TextComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); private changeHandler: ((value: string) => any) | null = null; constructor() { super(); this.inputEl = document.createElement("input"); this.inputEl.type = "text"; } onChange(cb: (value: string) => any) { this.changeHandler = cb; this.inputEl.removeEventListener("change", this.handleChange); this.inputEl.addEventListener("change", this.handleChange); this.inputEl.addEventListener("input", (evt) => { const target = evt.target as HTMLInputElement; cb(target.value); }); return this; } private handleChange = (evt: Event) => { if (this.changeHandler) { const target = evt.target as HTMLInputElement; this.changeHandler(target.value); } }; setValue(v: string) { this.inputEl.value = v; return this; } setPlaceholder(p: string) { this.inputEl.placeholder = p; return this; } setDisabled(disabled: boolean) { this.inputEl.disabled = disabled; return this; } } export class ToggleComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); private changeHandler: ((value: boolean) => any) | null = null; constructor() { super(); this.inputEl = document.createElement("input"); this.inputEl.type = "checkbox"; } onChange(cb: (value: boolean) => any) { this.changeHandler = cb; this.inputEl.addEventListener("change", (evt) => { const target = evt.target as HTMLInputElement; cb(target.checked); }); return this; } setValue(v: boolean) { this.inputEl.checked = v; return this; } setDisabled(disabled: boolean) { this.inputEl.disabled = disabled; return this; } } export class DropdownComponent extends Component { selectEl: HTMLSelectElement = document.createElement("select"); private changeHandler: ((value: string) => any) | null = null; constructor() { super(); this.selectEl = document.createElement("select"); } addOption(v: string, d: string) { const option = document.createElement("option"); option.value = v; option.textContent = d; this.selectEl.appendChild(option); return this; } addOptions(o: Record) { for (const [value, display] of Object.entries(o)) { this.addOption(value, display); } return this; } onChange(cb: (value: string) => any) { this.changeHandler = cb; this.selectEl.addEventListener("change", (evt) => { const target = evt.target as HTMLSelectElement; cb(target.value); }); return this; } setValue(v: string) { this.selectEl.value = v; return this; } setDisabled(disabled: boolean) { this.selectEl.disabled = disabled; return this; } } export class SliderComponent extends Component { inputEl: HTMLInputElement = document.createElement("input"); private changeHandler: ((value: number) => any) | null = null; constructor() { super(); this.inputEl = document.createElement("input"); this.inputEl.type = "range"; } onChange(cb: (value: number) => any) { this.changeHandler = cb; this.inputEl.addEventListener("change", (evt) => { const target = evt.target as HTMLInputElement; cb(parseFloat(target.value)); }); this.inputEl.addEventListener("input", (evt) => { const target = evt.target as HTMLInputElement; cb(parseFloat(target.value)); }); return this; } setValue(v: number) { this.inputEl.value = String(v); return this; } setMin(min: number) { this.inputEl.min = String(min); return this; } setMax(max: number) { this.inputEl.max = String(max); return this; } setStep(step: number) { this.inputEl.step = String(step); return this; } setDisabled(disabled: boolean) { this.inputEl.disabled = disabled; return this; } } export class Setting { nameEl: HTMLElement; descEl: HTMLElement; controlEl: HTMLElement; infoEl: HTMLElement; constructor(containerEl: HTMLElement) { this.nameEl = containerEl.createDiv(); this.descEl = containerEl.createDiv(); this.controlEl = containerEl.createDiv(); this.infoEl = containerEl.createDiv(); } setName(name: string) { this.nameEl.setText(name); return this; } setDesc(desc: string) { this.descEl.setText(desc); return this; } setClass(c: string) { this.controlEl.addClass(c); return this; } addText(cb: (text: TextComponent) => any) { const component = new TextComponent(); this.controlEl.appendChild(component.inputEl); cb(component); return this; } addToggle(cb: (toggle: ToggleComponent) => any) { const component = new ToggleComponent(); cb(component); return this; } addButton(cb: (btn: ButtonComponent) => any) { const btn = new ButtonComponent(); this.controlEl.appendChild(btn.buttonEl); cb(btn); return this; } addDropdown(cb: (dropdown: DropdownComponent) => any) { const component = new DropdownComponent(); cb(component); return this; } addSlider(cb: (slider: SliderComponent) => any) { const component = new SliderComponent(); cb(component); return this; } } // HTMLElement extensions if (typeof HTMLElement !== "undefined") { const proto = HTMLElement.prototype as any; proto.createDiv = function (o?: any) { const div = document.createElement("div"); if (o?.cls) div.addClass(o.cls); if (o?.text) div.setText(o.text); this.appendChild(div); return div; }; proto.createEl = function (tag: string, o?: any) { const el = document.createElement(tag); if (o?.cls) el.addClass(o.cls); if (o?.text) el.setText(o.text); this.appendChild(el); return el; }; proto.createSpan = function (o?: any) { return this.createEl("span", o); }; proto.empty = function () { this.innerHTML = ""; }; proto.setText = function (t: string) { this.textContent = t; }; proto.addClass = function (c: string) { this.classList.add(c); }; proto.removeClass = function (c: string) { this.classList.remove(c); }; proto.toggleClass = function (c: string, b: boolean) { this.classList.toggle(c, b); }; proto.hasClass = function (c: string) { return this.classList.contains(c); }; } export class Editor {} export class FuzzySuggestModal { constructor(app: App) {} setPlaceholder(p: string) {} open() {} close() {} private __dummy(_: T): never { throw new Error("Not implemented."); } } export class MarkdownRenderer { static render(app: App, md: string, el: HTMLElement, path: string, component: Component) { el.innerHTML = md; return Promise.resolve(); } } export class MarkdownView {} export class TextAreaComponent extends Component {} export class ItemView {} export class WorkspaceLeaf {} export function sanitizeHTMLToDom(html: string) { const div = document.createElement("div"); div.innerHTML = html; return div; } export function addIcon() {} export const debounce = (fn: any) => fn; export async function request(options: any) { const result = await requestUrl(options); return result.text; } export async function requestUrl({ body, headers, method, url, contentType, }: RequestUrlParam): Promise { // console.log("[requestUrl] Mock called:", { method, url, contentType }); const reqHeadersObj: Record = {}; for (const key of Object.keys(headers || {})) { reqHeadersObj[key.toLowerCase()] = headers[key]; } if (contentType) { reqHeadersObj["content-type"] = contentType; } reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate"; reqHeadersObj["Pragma"] = "no-cache"; reqHeadersObj["Expires"] = "0"; const result = await fetch(url, { method: method, headers: { ...reqHeadersObj, }, body: body, }); const headersObj: Record = {}; result.headers.forEach((value, key) => { headersObj[key] = value; }); let json = undefined; let text = undefined; let arrayBuffer = undefined; try { const isJson = result.headers.get("content-type")?.includes("application/json"); arrayBuffer = await result.arrayBuffer(); const isText = result.headers.get("content-type")?.startsWith("text/"); if (isText || isJson) { text = new TextDecoder().decode(arrayBuffer); } if (isJson) { json = await JSON.parse(text || "{}"); } } catch (e) { console.warn("Failed to parse response:", e); // ignore } return { status: result.status, headers: headersObj, text: text, json: json, arrayBuffer: arrayBuffer, }; } export function stringifyYaml(obj: any) { return JSON.stringify(obj); } export function parseYaml(s: string) { return JSON.parse(s); } export function getLanguage() { return "en"; } export function setIcon(el: HTMLElement, icon: string) {} export function arrayBufferToBase64(buffer: ArrayBuffer): string { return btoa(String.fromCharCode(...new Uint8Array(buffer))); } export function base64ToArrayBuffer(base64: string): ArrayBuffer { return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer; } export type DataWriteOptions = any; export type PluginManifest = any; export type RequestUrlParam = any; export type RequestUrlResponse = any; export type MarkdownFileInfo = any; export type ListedFiles = { files: string[]; folders: string[]; }; export type ValueComponent = any;