1.1.1-rc.1

This commit is contained in:
Mantano 2025-10-05 15:28:00 +07:00
parent fa5323f538
commit a424f3e48c
27 changed files with 2460 additions and 721 deletions

13
.gitignore vendored
View file

@ -13,13 +13,14 @@ data.json
# Exclude macOS Finder (System Explorer) View States
.DS_Store
# Hot Reload plugin
.hotreload
# This plugin
## The compiled main.js file.
main.js
## Files are copied here on `pnpm run build`.
/dist/
## Created by plugin.
## Sym-linked to the plugin's folder in the dev/test vault. Files are copied here `pnpm run dev`.
## Git stores symlinks as a special file containing the target path string, not as a directory. So the gitignore rule needs to match a file, not a directory pattern.
dev-vault
main.js
cache.json

View file

@ -2,4 +2,6 @@
//
// For a full list of overridable settings, and general information on folder-specific settings,
// see the documentation: https://zed.dev/docs/configuring-zed#settings-files
{}
{
"project_name": "Come Down"
}

View file

@ -27,7 +27,7 @@ Having the cache located inside the plugins folder serves several purposes:
- It allows the cache to be automatically deleted if the plugin is uninstalled.
- A .gitignore file can be placed in the cache folder to exclude it from Git. Had it been in a visible folder, users might mistakenly think that files they add there would be committed.
### Embedded Images Are Already Cached
### Embedded Images are Already Cached
Obsidian is built on Electron, which embeds the Chromium web browser. Therefore, it has built-in caching just as any web browser. But this cache exists outside your vault.
@ -35,19 +35,12 @@ For example, if you copied your vault's root folder to a USB memory and opened i
### Disabling
If the plugin is disabled, everything will work as it did before the plugin was enabled — the embedded images will load by means of the underlying browser. Upon enabling it again, the plugin's already cached items will be used, bypassing the browser.
If the plugin is disabled, everything will work as it did before the plugin was enabled — the embedded images will load by means of the underlying browser. Upon enabling it again, the plugin's already cached items will be used, bypassing the browser.
Note that if an embedded image is removed from a note while the plugin is disabled, its potential cached file will not be removed until that specific note is opened again with the plugin enabled. If you want to disable the plugin you may delete the cache first from settings.
Note that if an embedded image is removed from a note while the plugin is disabled, its potential cached file will not be removed until that specific note is opened again with the plugin enabled. If you want to disable the plugin, you may want to delete the cache from the settings first.
### Syncing
It is best to avoid caching on several devices without synchronizing in between, as this can lead to synchronization conflicts and inconsistencies. For example, you may see the same image download again on another device.
[^1]: This excludes [Obsidian Sync](https://obsidian.md/sync).

View file

@ -1,6 +1,8 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import esbuild from "esbuild";
import fs from "fs";
import path from "path";
import process from "process";
const banner =
`/*
@ -10,6 +12,10 @@ if you want to view the source, please visit the github repository of this plugi
`;
const prod = (process.argv[2] === "production");
const outdir = prod ? "dist" : "dev-vault";
fs.copyFileSync("manifest.json", path.join(outdir, "manifest.json"));
fs.copyFileSync("styles.css", path.join(outdir, "styles.css"));
const context = await esbuild.context({
banner: {
@ -33,15 +39,16 @@ const context = await esbuild.context({
"@lezer/lr",
...builtins],
format: "cjs",
target: "es2022",
// Runtime of min supported Obsidian version 1.8.0, see manifest.json
target: "es2024",
logLevel: "info",
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
outdir: outdir,
minify: prod,
define: {
"process.env.NODE_ENV": prod ? '"production"' : '"development"',
},
"process.env.NODE_ENV": prod ? '"production"' : '"development"',
},
});
if (prod) {

78
eslint.config.js Normal file
View file

@ -0,0 +1,78 @@
// https://typescript-eslint.io/packages/typescript-eslint#usage
import eslint from "@eslint/js";
import { defineConfig } from 'eslint/config';
import tseslint from "typescript-eslint";
import globals from "globals";
//import obsidianmd from "eslint-plugin-obsidianmd";
// If using Svelte
// import sveltePlugin from "eslint-plugin-svelte";
// import svelteParser from "svelte-eslint-parser";
export default defineConfig(
{
ignores: [
"**/build/**",
"**/dist/**",
"./main.js",
"./src/**/*js",
],
},
eslint.configs.recommended,
// https://typescript-eslint.io/users/configs#recommended-configurations
...tseslint.configs.recommended,
//...obsidianmd.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
plugins: {
// https://typescript-eslint.io/packages/typescript-eslint#manual-usage
"@typescript-eslint": tseslint.plugin,
},
languageOptions: {
parser: tseslint.parser,
parserOptions: {
projectService: true,
sourceType: "module",
ecmaVersion: "latest",
},
globals: {
...globals.browser,
...globals.node,
// Instead of adding `obsidian-typings` package. Add whatever is used here instead.
//createDiv: "readonly",
}
},
rules: {
// You should always have "no-unused-vars": "off" alongside @typescript-eslint/no-unused-vars,
// https://typescript-eslint.io/rules/no-unused-vars/
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
"args": "all",
"argsIgnorePattern": "^_",
"caughtErrors": "all",
"caughtErrorsIgnorePattern": "^_",
"destructuredArrayIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"ignoreRestSiblings": true,
}],
//
"@typescript-eslint/ban-ts-comment": ["error", {
"ts-expect-error": false,
"ts-ignore": true,
"ts-nocheck": true,
"ts-check": true,
}],
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unnecessary-condition": ["warn", {
// https://typescript-eslint.io/rules/no-unnecessary-condition/#only-allowed-literals
"allowConstantLoopConditions": "only-allowed-literals"
}],
"@typescript-eslint/switch-exhaustiveness-check": "error",
},
},
);

View file

@ -1,46 +0,0 @@
import js from "@eslint/js";
import globals from "globals";
import typeScriptPlugin from "@typescript-eslint/eslint-plugin";
import typeScriptParser from "@typescript-eslint/parser";
// If using Svelte
// import sveltePlugin from "eslint-plugin-svelte";
// import svelteParser from "svelte-eslint-parser";
export default [
{
files: ["**/*.ts", "**/*.tsx"],
plugins: {
"@typescript-eslint": typeScriptPlugin,
},
languageOptions: {
parser: typeScriptParser,
parserOptions: {
project: './tsconfig.json',
sourceType: 'module',
ecmaVersion: "latest",
},
globals: {
...globals.browser,
//...globals.node
}
},
rules: {
...js.configs.recommended.rules,
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "args": "none" }],
"@typescript-eslint/ban-ts-comment": "off",
"no-prototype-builtins": "off",
"@typescript-eslint/no-empty-function": "off",
}
},
{
ignores: [
"**/*js",
"**/node_modules/**",
],
},
];

View file

@ -1,7 +1,7 @@
{
"id": "come-down",
"name": "Come Down",
"version": "1.1.0",
"version": "1.1.1-rc.1",
"minAppVersion": "1.8.0",
"description": "Maintains a cache of your notes embedded external images.",
"author": "mntno",

View file

@ -4,30 +4,31 @@
"description": "An Obsidian plugin for caching embedded external images.",
"main": "main.js",
"scripts": {
"lint": "pnpm eslint .",
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"build": "(pnpm eslint . || true) && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [],
"author": "mntno",
"license": "MIT",
"devDependencies": {
"@codemirror/language": "6.11.3",
"@codemirror/language": "6.12.3",
"@codemirror/state": "6.5.0",
"@codemirror/view": "6.38.1",
"@eslint/js": "9.37.0",
"@lezer/common": "^1.2.3",
"@types/node": "24.6.2",
"@typescript-eslint/eslint-plugin": "8.45.0",
"@typescript-eslint/parser": "8.45.0",
"builtin-modules": "5.0.0",
"esbuild": "0.25.10",
"globals": "16.4.0",
"@codemirror/view": "6.38.6",
"@eslint/js": "^9.39.4",
"@lezer/common": "^1.5.2",
"@types/node": "25.6.0",
"builtin-modules": "5.1.0",
"esbuild": "^0.28.0",
"eslint": "^9.39.4",
"eslint-plugin-obsidianmd": "^0.1.9",
"globals": "17.5.0",
"image-size": "^2.0.2",
"obsidian": "latest",
"tslib": "2.8.1",
"typescript": "5.9.3",
"typescript-eslint": "8.45.0",
"tslib": "^2.8.1",
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.1",
"xxhash-wasm": "1.1.0"
},
"type": "module",

File diff suppressed because it is too large Load diff

View file

@ -54,11 +54,13 @@ export const Env = {
w: console.warn,
e: console.error,
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
debug: DevContext.logCategory.DEBUGGING ? devLogger.info : noopLogger.info,
edit: DevContext.logCategory.EDIT_UPDATE_PASS ? devLogger.info : noopLogger.info,
read: DevContext.logCategory.POST_PROCESS_PASS ? devLogger.info : noopLogger.info,
cm: DevContext.logCategory.CACHE_MANAGER ? devLogger.info : noopLogger.info,
workaround: DevContext.logCategory.WORKAROUNDS ? devLogger.info : noopLogger.info,
/* eslint-enable @typescript-eslint/no-unnecessary-condition */
},
perf: {
@ -69,18 +71,22 @@ export const Env = {
},
},
/**
* @param appOrCallback - Either a callback function to execute after the cache is cleared, or an App instance to reload Obsidian.
*/
clearBrowserCache: (appOrCallback: (() => void) | App) => {
if (isDev && Platform.isDesktopApp) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
require('electron').remote.session.defaultSession.clearCache()
.then(() => {
if (appOrCallback instanceof App) {
// @ts-expect-error
app.commands.executeCommandById("app:reload");
appOrCallback.commands.executeCommandById("app:reload");
}
else
appOrCallback();
})
.catch((error: any) => console.error('Error clearing cache:', error));
.catch((error: unknown) => console.error('Error clearing cache:', error));
}
},
@ -93,6 +99,10 @@ export const Env = {
return false;
},
obj: {
is: (value: unknown): value is object => typeof value === "object" && value !== null, // `null` is an object
} as const,
str: {
EMPTY: "",
SPACE: " ",

View file

@ -1,6 +1,6 @@
import { CacheManager } from "cache/CacheManager";
import { Env } from "Env";
import { Platform, Plugin, PluginSettingTab, Setting } from "obsidian";
import { Plugin, PluginSettingTab, Setting } from "obsidian";
import { Notice } from "ui/Notice";
export interface PluginSettings {
@ -29,7 +29,7 @@ type SettingsChanged = (settings: PluginSettings) => void;
export class SettingsManager {
public settings: PluginSettings;
public save: () => Promise<void>;
public onChangedCallback: (name: string, value: any) => void | undefined;
public onChangedCallback: (name: string, value: unknown) => void | undefined;
static readonly DEFAULT_SETTINGS: PluginSettings = {
noticeOnDownload: true,
@ -43,7 +43,7 @@ export class SettingsManager {
gitIgnoreCacheDir: "gitIgnoreCacheDir",
} as const;
constructor(settings: any, save: (settings: PluginSettings) => Promise<void>, onChangedCallback: (name: string, value: any) => void | undefined) {
constructor(settings: PluginSettings, save: (settings: PluginSettings) => Promise<void>, onChangedCallback: (name: string, value: unknown) => void | undefined) {
this.settings = settings;
this.save = () => save(this.settings);
this.onChangedCallback = onChangedCallback;
@ -87,7 +87,7 @@ export class SettingTab extends PluginSettingTab {
this.cacheManager.checkIfMetadataFileChangedExternally().then(() => this.render());
}
public hide(): void {
override hide(): void {
Env.log.d("SettingTab:hide");
if (this.isShown)
this.settingsManager.unregisterOnChangedCallback(this.onChangedCallback);
@ -102,7 +102,7 @@ export class SettingTab extends PluginSettingTab {
containerEl.empty();
let compactDownloadMessage: Setting | undefined;
let compactDownloadMessage: Setting | undefined = undefined;
const setCompactDownloadMessageVisibility = () => {
if (settings.noticeOnDownload)
compactDownloadMessage?.settingEl.show();
@ -161,11 +161,7 @@ export class SettingTab extends PluginSettingTab {
button.buttonEl.remove();
if (Env.isDev && Platform.isDesktopApp) {
require('electron').remote.session.defaultSession.clearCache()
.then(() => new Notice('Electron Cache cleared successfully. Restart vault.'))
.catch((error: any) => Env.log.e('Error clearing cache:', error));
}
Env.clearBrowserCache(() => new Notice('Electron Cache cleared successfully. Restart vault.'));
}
});
});
@ -198,7 +194,7 @@ export class SettingTab extends PluginSettingTab {
settings.gitIgnoreCacheDir = value;
await this.settingsManager.save();
refreshDisabled();
this.settingsManager?.onChangedCallback(SettingsManager.SETTING_NAME.gitIgnoreCacheDir, value);
this.settingsManager.onChangedCallback(SettingsManager.SETTING_NAME.gitIgnoreCacheDir, value);
});
});
}

View file

@ -84,7 +84,7 @@ export class CacheFetchError extends CacheError {
constructor(cacheKey: string, readonly error: Error | null, readonly info: { readonly sourceUrl: string, readonly statusCode?: number }) {
super(cacheKey, `Failed to fetch cache: ${cacheKey}. Status: ${info?.statusCode ?? "Unknown"}`, { cause: error });
super(cacheKey, `Failed to fetch cache: ${cacheKey}. Status: ${info.statusCode ?? "Unknown"}`, { cause: error });
if (error) {
if (error.message && error.message.includes("net::ERR_NAME_NOT_RESOLVED")) {
@ -100,7 +100,7 @@ export class CacheFetchError extends CacheError {
}
}
else {
this.isRetryable = !this.info?.statusCode || (this.info?.statusCode >= 500 && this.info?.statusCode < 600);
this.isRetryable = !this.info.statusCode || (this.info.statusCode >= 500 && this.info.statusCode < 600);
}
}
}
@ -136,6 +136,7 @@ export class CacheManager {
private readonly metadataFilePath: string;
private static hasher: XXHashAPI;
private static isHasherInitialized = false;
private readonly filePathsToOmitWhenClearingCache: string[]
@ -148,8 +149,10 @@ export class CacheManager {
public static async create(vault: Vault, cacheDir: string, metadataFilePath: string, filePathsToOmitWhenClearingCache: string[] = []) {
const instance = new this(vault, cacheDir, metadataFilePath, filePathsToOmitWhenClearingCache);
if (!this.hasher)
this.hasher = await xxhash(); // Seems to be quick.
if (!CacheManager.isHasherInitialized) {
CacheManager.hasher = await xxhash(); // Seems to be quick.
CacheManager.isHasherInitialized = true;
}
await instance.initCache(); // This can be called lazily if it turns out reading the json file takes too long time.
@ -172,8 +175,6 @@ export class CacheManager {
try {
await this.loadMetadata();
this.cacheInitiated = true;
} catch (error) {
throw error;
} finally {
this.initPromise = null; // Reset when done.
}
@ -235,7 +236,7 @@ export class CacheManager {
Env.log.cm(Env.dev.icon.CACHE_MANAGER, "\t", "Current retain count: ", this.retainCount());
// If retainer/file does not exist / is not yet registered, it will be.
let retainer = this.metadataRoot.retainers[retainerPath];
const retainer = this.metadataRoot.retainers[retainerPath];
const cacheKeysCurrentlyReferenced = retainer ? [...retainer.ref] : [];
// Populate set of keys to retain.
@ -340,7 +341,7 @@ export class CacheManager {
public renameRetainer(oldPath: string, path: string) {
Env.log.cm(Env.dev.icon.CACHE_MANAGER, `CacheManager.renameRetainer\n\t${oldPath}`);
let retainer = this.metadataRoot.retainers[oldPath];
const retainer = this.metadataRoot.retainers[oldPath];
if (retainer !== undefined) {
this.metadataRoot.retainers[path] = retainer;
delete this.metadataRoot.retainers[oldPath];
@ -354,7 +355,7 @@ export class CacheManager {
public async removeRetainer(path: string) {
Env.log.cm(Env.dev.icon.CACHE_MANAGER, `CacheManager.removeRetainer\n\t${path}`);
let retainer = this.metadataRoot.retainers[path];
const retainer = this.metadataRoot.retainers[path];
if (retainer === undefined) {
// This can happen if a file which hasn't been open was deleted without opening it.
Env.log.cm("\tFailed: No retainer found.");
@ -430,7 +431,7 @@ export class CacheManager {
// Increase count for each reference found.
for (const cacheKey of retainer.ref) {
// Note: make sure to `if (count !== undefined)` rather than `if(count)` otherwise `count` will not be treated as a `number` and addition will fail.
let count = retainCounts[cacheKey];
const count = retainCounts[cacheKey];
Env.assert(count !== undefined, "Retainer is referencing a cache key that does not exist", cacheKey);
if (count !== undefined)
retainCounts[cacheKey] = count + 1;
@ -452,7 +453,7 @@ export class CacheManager {
* @returns `true` if the key is retained in {@link retainCounts}.
*/
private isRetained(retainCounts: Record<string, number>, key: string) {
let count = retainCounts[key];
const count = retainCounts[key];
return count === undefined || count === 0 ? false : true; // Since 1.0.6, retain counts of zero are actually not present in the dictionary anymore.
}
@ -497,7 +498,7 @@ export class CacheManager {
//
const retainers: CacheRetainer[] = Object.values(this.metadataRoot.retainers);
const numberOfRetainers = retainers.length;
//const numberOfRetainers = retainers.length;
// Here the Mardown file has been deleted but its still exists as a retainer.
const retainersWithoutActualFile = [];
@ -787,7 +788,7 @@ export class CacheManager {
* Aborts current download requests for the specified file.
* @todo
*/
async cancelOngoing(filePath: string) {
async cancelOngoing(_filePath: string) {
await sleep(100);
}
@ -987,7 +988,7 @@ export class CacheManager {
return {
w: sizeCalcResult.width,
h: sizeCalcResult.height,
t: sizeCalcResult?.type ?? "",
t: sizeCalcResult.type ?? Env.str.EMPTY,
};
} catch (error) {
if (error instanceof TypeError)

View file

@ -26,7 +26,7 @@ export default class ComeDownPlugin extends Plugin {
private settingsManager!: SettingsManager;
private cacheManager!: CacheManager;
async onload() {
override async onload() {
Env.log.d("Plugin:onload");
Notice.setName(this.manifest.name);
@ -104,20 +104,19 @@ export default class ComeDownPlugin extends Plugin {
});
}
async onunload() {
override async onunload() {
Env.log.d("Plugin:onunload");
await this.cacheManager?.cancelAllOngoing();
await this.cacheManager.cancelAllOngoing();
}
public onExternalSettingsChange() {
override onExternalSettingsChange() {
Env.log.d("Plugin:onExternalSettingsChange");
if (this.data) {
ComeDownPlugin.loadPluginData(this).then(data => {
this.data = data;
this.settingsManager.onSettingsChangedExternally(this.data.settings);
setTimeout(() => this.cacheManager.checkIfMetadataFileChangedExternally(), 1000);
})
}
ComeDownPlugin.loadPluginData(this).then(data => {
this.data = data;
this.settingsManager.onSettingsChangedExternally(this.data.settings);
setTimeout(() => this.cacheManager.checkIfMetadataFileChangedExternally(), 1000);
})
}
private static async loadPluginData(plugin: Plugin): Promise<PluginData> {

View file

@ -28,7 +28,7 @@ export class EditorViewPlugin {
Env.log.d("EditorViewPlugin:constructor");
}
update(update: ViewUpdate) {
update(_update: ViewUpdate) {
//Env.log.d("EditorViewPlugin:update", this.getViewMetadata(update.view));
}

View file

@ -4,20 +4,23 @@ import { Logger as BaseLogger } from "utils/Logger";
export class Logger extends BaseLogger {
public beginMsg(...args: unknown[]) {
if (!Env.isDev) return "";
const joinedArgs = args.length > 0 ? `(${args.join(" ")})` : "";
return `${this.symbol} Begin pass. ${joinedArgs} ➡️🚪 ${this.idString}`;
if (!Env.isDev)
return Env.str.EMPTY;
return `${this.symbol} Begin pass. ${this.joinArgs(args)} ➡️🚪 ${this.idString}`;
}
public endMsg(...args: unknown[]) {
if (!Env.isDev) return "";
const joinedArgs = args.length > 0 ? `(${args.join(" ")})` : "";
return `${this.symbol} End pass. Finished. ${joinedArgs} ✅🚪➡️ ${this.idString}`;
if (!Env.isDev)
return Env.str.EMPTY;
return `${this.symbol} End pass. Finished. ${this.joinArgs(args)} ✅🚪➡️ ${this.idString}`;
}
public abortMsg(...args: unknown[]) {
if (!Env.isDev) return "";
const joinedArgs = args.length > 0 ? `(${args.join(" ")})` : "";
return `${this.symbol} End pass. Aborted. ${joinedArgs} ❌🚪➡️ ${this.idString}`;
if (!Env.isDev)
return Env.str.EMPTY;
return `${this.symbol} End pass. Aborted. ${this.joinArgs(args)} ❌🚪➡️ ${this.idString}`;
}
}

View file

@ -43,7 +43,7 @@ export class ProcessingContext {
this.vuCtx = underlyingCtx instanceof ViewUpdateContext ? underlyingCtx : null;
this.ppCtx = underlyingCtx instanceof PostProcessorContext ? underlyingCtx : null;
Env.dev.assert(this.vuCtx || this.ppCtx);
Env.dev.assert(this.vuCtx !== null || this.ppCtx !== null);
this.viewContainerEl = viewContainerEl;
@ -67,7 +67,7 @@ export class ProcessingContext {
const contentEl = viewUpdate.view.contentDOM;
let viewContainerEl: HTMLElement | null = null;
let view = ProcessingContext.tryGetViewInstance(app, contentEl);
const view = ProcessingContext.tryGetViewInstance(app, contentEl);
if (view)
viewContainerEl = view.containerEl;
@ -75,7 +75,7 @@ export class ProcessingContext {
if (viewContainerEl === null)
viewContainerEl = Arr.firstOrNull(ObsAssistant.viewContainerEl({ element: contentEl, dir: "up" }));
let viewingMode = ProcessingContext.tryGetViewingMode(view ?? undefined, viewContainerEl ?? undefined);
const viewingMode = ProcessingContext.tryGetViewingMode(view ?? undefined, viewContainerEl ?? undefined);
let associatedFile: TFile | null = null;
if (view !== null)
@ -101,7 +101,7 @@ export class ProcessingContext {
let viewContainerEl: HTMLElement | null = null;
let view = ProcessingContext.tryGetViewInstance(app, element);
const view = ProcessingContext.tryGetViewInstance(app, element);
if (view)
viewContainerEl = view.containerEl;

View file

@ -53,7 +53,7 @@ export class ProcessingPass {
public retainCache(urlOrCache: string | CacheRequest) {
if (this.ctx.isCacheAccessReadWrite()) {
Env.assert(urlOrCache);
Env.assert(Env.str.nonEmpty(urlOrCache) !== undefined || Env.obj.is(urlOrCache));
this.ctx.logr.log(this.ctx.logr.t(() => this.ctx.logr.msg(`retainCache: Will retain ${CacheManager.createCacheKeyFromOriginalSrc(Env.str.is(urlOrCache) ? urlOrCache : urlOrCache.source)} at the end of pass`)));
if (Env.str.is(urlOrCache)) {
@ -69,7 +69,7 @@ export class ProcessingPass {
public ignoreCache(src: string) {
if (this.ctx.isCacheAccessReadWrite()) {
Env.assert(src);
Env.assert(Env.str.nonEmpty(src) !== undefined);
this.ctx.logr.log(this.ctx.logr.t(() => this.ctx.logr.msg(`ignoreCache: Will ignore ${CacheManager.createCacheKeyFromOriginalSrc(src)} at the end of pass`)));
this.requestsToIgnore.add(CacheManager.createRequest(src, this.ctx.associatedFile.path));

View file

@ -1,6 +1,6 @@
import { ViewUpdate } from "@codemirror/view";
import { Env } from "Env";
import { HtmlAssistant, HTMLElementAttribute, HTMLElementCacheState } from "processing/HtmlAssistant";
import { HtmlAssistant, HTMLElementCacheState } from "processing/HtmlAssistant";
import { Url } from "utils/Url";
export class Workarounds {
@ -29,7 +29,7 @@ export class Workarounds {
if (update.changes.empty)
return null;
var sourcesToIgnore: string[] = [];
const sourcesToIgnore: string[] = [];
update.changes.iterChanges((_fromA, _toA, cursorStartPos, cursorEndPosition, inserted) => {
@ -83,7 +83,7 @@ export class Workarounds {
* Will not match if there's no link because an img element is not inserted until the link
* begins with `http:`, `https:`, `ftp:`, `ws:`, or `wss:`.
*/
private static readonly markdownLinkRegex = /(?<!\!)\[.*?\]\((.+?)\)/;
private static readonly markdownLinkRegex = /(?<!!)\[.*?\]\((.+?)\)/;
/**
* Since image elements are not available in {@link detectSourcesOfInvalidImageElements}, this method is used with the result of that when the elements are available.

View file

@ -1,6 +1,7 @@
// Global (see tsconfig `include`)
// Do not add `export`
// types.ts - Global type declarations
type Prettify<T> = {
// Global type aliases
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};

View file

@ -70,7 +70,7 @@ export class InfoModal extends Modal {
clearCacheButton!: ButtonComponent;
closeButton!: ButtonComponent;
onOpen(): void {
override onOpen(): void {
super.onOpen();
this.cacheManager.registerMetadataChanged(this.onMetadataChangedCallback);
setTimeout(() => {
@ -81,7 +81,7 @@ export class InfoModal extends Modal {
this.populate();
}
onClose(): void {
override onClose(): void {
super.onClose();
this.cacheManager.unregisterMetadataChanged(this.onMetadataChangedCallback);
}

View file

@ -1,6 +1,6 @@
import { ensureSyntaxTree, syntaxTree } from "@codemirror/language";
import { EditorView, ViewUpdate } from "@codemirror/view";
import { SyntaxNodeRef, Tree } from "@lezer/common";
import { Tree } from "@lezer/common";
import { Env } from "Env";
export interface ParsedImage {

View file

@ -5,7 +5,7 @@ export class Logger {
public readonly log: LoggerFn;
public readonly id: number;
public readonly symbol: string;
public readonly t = (action: () => string) => Env.isDev ? action() : "";
public readonly t = (action: () => string) => Env.isDev ? action() : Env.str.EMPTY;
constructor(log: LoggerFn, id: number, symbol: string) {
this.log = log;
@ -17,9 +17,15 @@ export class Logger {
return "— ID" + this.id;
}
public msg(...args: unknown[]) {
if (!Env.isDev) return "";
const mappedArgs = args.map(arg => arg === undefined ? "undefined" : arg);
return `${this.symbol} ${mappedArgs.join(" ")} ${this.idString}`;
protected joinArgs(args: unknown[]): string {
return args.length === 0 ? Env.str.EMPTY : `(${args.join(" ")})`;
}
}
public msg(...args: unknown[]) {
if (!Env.isDev)
return Env.str.EMPTY;
const mappedArgs = args.map(arg => arg === undefined ? "undefined" : arg);
return `${this.symbol} ${mappedArgs.join(Env.str.SPACE)} ${this.idString}`;
}
}

View file

@ -1,7 +1,7 @@
import { Env } from "Env";
import { App, FileView, getIcon, ItemView, MarkdownPostProcessorContext, MarkdownView, TextFileView, TFile, View } from "obsidian";
import { Prettify } from "types";
import { childEl, firstChildEl, firstParentEl } from "utils/dom";
import { Arr } from "utils/ts";
export type ObsViewMode = "none" | "reader" | "preview" | "source";
@ -21,7 +21,7 @@ export class ObsAssistant {
/** Internal API */
public static containerElFromPostProcessorContext(postProcessorContext: MarkdownPostProcessorContext) {
Env.assert("containerEl" in postProcessorContext, "MarkdownPostProcessorContext does not have `containerEl`");
// @ts-ignore
// @ts-expect-error
const containerEl = postProcessorContext.containerEl;
if (containerEl)
return containerEl as HTMLElement;
@ -56,13 +56,13 @@ export class ObsAssistant {
* ```html
* <div class="popover hover-popover">`
* <div class="markdown-embed is-loaded">
* <div class="markdown-embed-content …">
* <div class="markdown-preview-view markdown-rendered …">
* </div>
*
* </div>
* </div>
* </div>
* <div class="markdown-embed-content …">
* <div class="markdown-preview-view markdown-rendered …">
* </div>
*
* </div>
* </div>
* </div>
* ```
*
* @remarks `hover-popover` is a modifier class that extends or specifies the behavior. It indicates the popover appears on hover.
@ -89,31 +89,37 @@ export class ObsAssistant {
let element: HTMLElement[] | null = null;
const findChildOrChildren = (options as TryGetAllOptions).allDescendants === true ? childEl : firstChildEl;
const find = (el: HTMLElement) => {
if (options.dir === "down") {
element = Arr.orNull(findChildOrChildren(el, selectors));
}
else if (options.dir === "up") {
element = Arr.orNull(firstParentEl(el, selectors));
}
else if (options.dir === "downup") {
element = Arr.orNull(findChildOrChildren(el, selectors));
if (element === null)
element = Arr.orNull(firstParentEl(el, selectors));
}
else if (options.dir === "updown") {
element = Arr.orNull(firstParentEl(el, selectors));
if (element === null)
element = Arr.orNull(findChildOrChildren(el, selectors));
}
const orNull = (v: HTMLElement[] | HTMLElement | null): HTMLElement[] | null => v === null
? null
: (Array.isArray(v) ? v : [v]);
switch (options.dir) {
case "down":
return orNull(findChildOrChildren(el, selectors));
case "up":
return orNull(firstParentEl(el, selectors));
case "downup": {
let element = orNull(findChildOrChildren(el, selectors));
if (element === null)
element = orNull(firstParentEl(el, selectors));
return element;
}
case "updown": {
let element = orNull(firstParentEl(el, selectors));
if (element === null)
element = orNull(findChildOrChildren(el, selectors));
return element;
}
};
};
if (options.view !== undefined) {
if (options.view instanceof ItemView)
find(options.view.contentEl);
element = find(options.view.contentEl);
else
find(options.view.containerEl);
element = find(options.view.containerEl);
}
if (element === null && options.postProcessorContext) {
@ -229,7 +235,7 @@ export class ObsAssistant {
return null;
const iconColor = color ?? getComputedStyle(el).getPropertyValue(CssVar.Icon.COLOR).trim();
Env.assert(iconColor, "CSS variable not found:", CssVar.Icon.COLOR);
Env.assert(Env.str.nonEmpty(iconColor) !== undefined, "CSS variable not found:", CssVar.Icon.COLOR);
icon.setAttribute("stroke", iconColor || fallbackColor);
icon.setAttribute("stroke-width", "1");

View file

@ -63,7 +63,7 @@ export class Url {
try {
const url = new URL(src);
return url.protocol === "http:" || url.protocol === "https:";
} catch (error) {
} catch {
return false;
}
}

View file

@ -1,10 +1,6 @@
export const Arr = {
firstOrNull: <T>(a: Array<T>): T | null => a.first() ?? null,
orNull: <T>(v: T[] | T | null): T[] | null =>
v === null
? null
: (Array.isArray(v) ? v : [v]),
} as const;
export const Err = {

View file

@ -1,21 +1,47 @@
{
"compilerOptions": {
"baseUrl": "src",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES2022",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strict": true,
"strictNullChecks": true,
"noUncheckedIndexedAccess": true,
"lib": ["DOM", "ES2022"]
},
"include": [
"**/*.ts"
]
"compilerOptions": {
// Enable to check TS7 compatibility.
"stableTypeOrdering": false,
"rootDir": "./src",
"paths": {
"*": ["./src/*"],
"#/*": ["./src/*"]
},
"inlineSourceMap": true,
"inlineSources": true,
"target": "ES2024",
"lib": [
"DOM",
"ES2024"
],
"types": ["node"],
"moduleResolution": "bundler",
"isolatedModules": true,
"importHelpers": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"erasableSyntaxOnly": false,
"exactOptionalPropertyTypes": false,
// For ESLint to handle:
"allowUnreachableCode": true,
"allowUnusedLabels": true,
},
"include": [
"./src/**/*.ts"
],
"exclude": [
"./node_modules",
"./dist",
"./build",
"./main.js",
"./eslint.config.js"
]
}

View file

@ -1,4 +1,5 @@
{
"1.1.1": "1.8.0",
"1.1.0": "1.8.0",
"1.0.7": "1.8.0",
"1.0.6": "1.8.0",