mirror of
https://github.com/etaiso/obsidian-tag-fuzzy-find.git
synced 2026-07-22 06:53:02 +00:00
feat(settings): add triggerPrefix with 6-option allowlist
This commit is contained in:
parent
db4eadc491
commit
94463a0de0
5 changed files with 59 additions and 13 deletions
|
|
@ -61,7 +61,11 @@ export default class TagFinderPlugin extends Plugin {
|
|||
this.uninstallHook?.();
|
||||
this.uninstallHook = null;
|
||||
if (this.settings.enableQuickSwitcherHook) {
|
||||
this.uninstallHook = installQuickSwitcherHook(this.app, (q) => this.openTagPicker(q));
|
||||
this.uninstallHook = installQuickSwitcherHook(
|
||||
this.app,
|
||||
(q) => this.openTagPicker(q),
|
||||
this.settings.triggerPrefix,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,28 @@
|
|||
import { App } from "obsidian";
|
||||
|
||||
export type QuickSwitcherReroute = (initialQueryAfterHash: string) => void;
|
||||
export type QuickSwitcherReroute = (initialQueryAfterPrefix: string) => void;
|
||||
|
||||
/**
|
||||
* Install a document-level 'input' capture listener that detects when the
|
||||
* user has typed '#' as the first character in the built-in Quick Switcher
|
||||
* prompt. When that happens we close the built-in modal and invoke the
|
||||
* reroute callback with the text after the '#'.
|
||||
* user has typed `triggerPrefix` as the first character in the built-in
|
||||
* Quick Switcher prompt. When that happens we close the built-in modal and
|
||||
* invoke the reroute callback with the text after the prefix.
|
||||
*
|
||||
* Returns an uninstaller. DOM-inspection errors (e.g. Obsidian changes the
|
||||
* switcher internals) permanently disable the hook for the session; errors
|
||||
* thrown by the `reroute` callback are logged but do NOT disable the hook.
|
||||
*/
|
||||
export function installQuickSwitcherHook(app: App, reroute: QuickSwitcherReroute): () => void {
|
||||
export function installQuickSwitcherHook(
|
||||
app: App,
|
||||
reroute: QuickSwitcherReroute,
|
||||
triggerPrefix: string,
|
||||
): () => void {
|
||||
let disabled = false;
|
||||
|
||||
const handler = (evt: Event) => {
|
||||
if (disabled) return;
|
||||
|
||||
let queryAfterHash: string;
|
||||
let queryAfterPrefix: string;
|
||||
try {
|
||||
const target = evt.target as HTMLElement | null;
|
||||
if (!(target instanceof HTMLInputElement)) return;
|
||||
|
|
@ -29,7 +33,7 @@ export function installQuickSwitcherHook(app: App, reroute: QuickSwitcherReroute
|
|||
if (!target.closest(".prompt .prompt-input-container")) return;
|
||||
|
||||
const value = target.value;
|
||||
if (!value.startsWith("#")) return;
|
||||
if (!value.startsWith(triggerPrefix)) return;
|
||||
|
||||
// Stop the event from reaching the Quick Switcher's own input handler.
|
||||
evt.stopPropagation();
|
||||
|
|
@ -47,7 +51,7 @@ export function installQuickSwitcherHook(app: App, reroute: QuickSwitcherReroute
|
|||
cancelable: true,
|
||||
}));
|
||||
|
||||
queryAfterHash = value.slice(1);
|
||||
queryAfterPrefix = value.slice(triggerPrefix.length);
|
||||
} catch (err) {
|
||||
disabled = true;
|
||||
console.warn("[tag-finder] quick-switcher hook disabled after error", err);
|
||||
|
|
@ -56,9 +60,9 @@ export function installQuickSwitcherHook(app: App, reroute: QuickSwitcherReroute
|
|||
|
||||
// Call reroute outside the DOM-inspection try/catch so that downstream
|
||||
// errors don't disable the hook. They'll surface in the console but the
|
||||
// next '#' keypress will still be intercepted.
|
||||
// next prefix keypress will still be intercepted.
|
||||
try {
|
||||
reroute(queryAfterHash);
|
||||
reroute(queryAfterPrefix);
|
||||
} catch (err) {
|
||||
console.warn("[tag-finder] reroute callback threw", err);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import { DEFAULT_SETTINGS, EmptyStateMode, PluginSettings } from "./types";
|
||||
import {
|
||||
DEFAULT_SETTINGS,
|
||||
EmptyStateMode,
|
||||
PluginSettings,
|
||||
TriggerPrefix,
|
||||
VALID_PREFIXES,
|
||||
} from "./types";
|
||||
|
||||
const VALID_MODES: EmptyStateMode[] = ["recent-then-usage", "usage", "alphabetical", "blank"];
|
||||
|
||||
|
|
@ -6,6 +12,10 @@ function isValidMode(v: unknown): v is EmptyStateMode {
|
|||
return typeof v === "string" && (VALID_MODES as string[]).includes(v);
|
||||
}
|
||||
|
||||
function isValidPrefix(v: unknown): v is TriggerPrefix {
|
||||
return typeof v === "string" && (VALID_PREFIXES as string[]).includes(v);
|
||||
}
|
||||
|
||||
export function normalizeSettings(raw: unknown): PluginSettings {
|
||||
const source = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
||||
|
||||
|
|
@ -22,5 +32,9 @@ export function normalizeSettings(raw: unknown): PluginSettings {
|
|||
recentLimit = Math.max(0, Math.min(30, Math.floor(source.recentLimit)));
|
||||
}
|
||||
|
||||
return { emptyStateMode, enableQuickSwitcherHook, recentLimit };
|
||||
const triggerPrefix = isValidPrefix(source.triggerPrefix)
|
||||
? source.triggerPrefix
|
||||
: DEFAULT_SETTINGS.triggerPrefix;
|
||||
|
||||
return { emptyStateMode, enableQuickSwitcherHook, recentLimit, triggerPrefix };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,17 +11,24 @@ export type EmptyStateMode =
|
|||
| "alphabetical"
|
||||
| "blank";
|
||||
|
||||
export type TriggerPrefix = "#" | ":" | "@" | "!" | ">" | "?";
|
||||
|
||||
export const VALID_PREFIXES: TriggerPrefix[] = ["#", ":", "@", "!", ">", "?"];
|
||||
|
||||
export type PluginSettings = {
|
||||
emptyStateMode: EmptyStateMode;
|
||||
enableQuickSwitcherHook: boolean;
|
||||
/** 0..30; 0 disables recent-tags entirely. */
|
||||
recentLimit: number;
|
||||
/** Single-character prefix that activates the Quick Switcher hook. */
|
||||
triggerPrefix: TriggerPrefix;
|
||||
};
|
||||
|
||||
export const DEFAULT_SETTINGS: PluginSettings = {
|
||||
emptyStateMode: "recent-then-usage",
|
||||
enableQuickSwitcherHook: true,
|
||||
recentLimit: 10,
|
||||
triggerPrefix: "#",
|
||||
};
|
||||
|
||||
/** Shape of the blob stored via plugin.saveData(). */
|
||||
|
|
|
|||
|
|
@ -47,4 +47,21 @@ describe("normalizeSettings", () => {
|
|||
it("treats array input as empty object (all defaults)", () => {
|
||||
expect(normalizeSettings(["not", "an", "object"])).toEqual(DEFAULT_SETTINGS);
|
||||
});
|
||||
|
||||
it("keeps each valid triggerPrefix", () => {
|
||||
for (const prefix of ["#", ":", "@", "!", ">", "?"] as const) {
|
||||
expect(normalizeSettings({ triggerPrefix: prefix }).triggerPrefix).toBe(prefix);
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to '#' when triggerPrefix is missing", () => {
|
||||
expect(normalizeSettings({}).triggerPrefix).toBe("#");
|
||||
});
|
||||
|
||||
it("falls back to '#' when triggerPrefix is invalid", () => {
|
||||
const cases = ["", "##", "a", "Z", "1", " ", "/", null, 42, ["#"], { v: "#" }];
|
||||
for (const bad of cases) {
|
||||
expect(normalizeSettings({ triggerPrefix: bad }).triggerPrefix).toBe("#");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue