@lapis-notes/bookmarks 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,170 @@
1
+ import { basename, Notice, Plugin, } from "@lapis-notes/api";
2
+ import AddBookmarkDialog from "./add-bookmark-dialog.svelte";
3
+ import { bookmarkableTarget, isAllowedBookmarkUrl } from "./activate-bookmark";
4
+ import { BookmarksStore, createPluginBookmarksPersistence, createVaultRenameBinding, } from "./bookmarks-store";
5
+ import { BookmarksView } from "./bookmarks-view";
6
+ import { BOOKMARKS_PLUGIN_ID, BookmarksViewType, } from "./bookmarks-view-type";
7
+ import { listBookmarkGroups } from "./bookmarks-schema";
8
+ import { dialogPortalPropsForApp, mountDialog, } from "./mount-dialog";
9
+ import PickBookmarkGroup from "./pick-bookmark-group.svelte";
10
+ import manifestSpec from "../../manifest.json";
11
+ export class BookmarksPlugin extends Plugin {
12
+ store;
13
+ constructor(app, manifest = manifestSpec) {
14
+ super(app, manifest);
15
+ this.store = new BookmarksStore(createPluginBookmarksPersistence(this));
16
+ }
17
+ async onload() {
18
+ await this.store.load();
19
+ this.register(createVaultRenameBinding(this.app, this.store));
20
+ this.registerSidebarView(BookmarksViewType, (leaf) => new BookmarksView(leaf, this), { side: "left", title: "Bookmarks", icon: "bookmark" }, {
21
+ kind: "command",
22
+ command: {
23
+ id: "open-bookmarks",
24
+ name: "Open Bookmarks",
25
+ callback: () => void this.openBookmarks(),
26
+ },
27
+ });
28
+ this.addCommand({
29
+ id: "add-bookmark",
30
+ name: "Add bookmark",
31
+ checkCallback: (checking) => {
32
+ const target = bookmarkableTarget(this.app);
33
+ if (!target)
34
+ return false;
35
+ if (!checking)
36
+ void this.promptAddBookmark();
37
+ return true;
38
+ },
39
+ });
40
+ this.addCommand({
41
+ id: "bookmark-url",
42
+ name: "Bookmark URL",
43
+ callback: () => void this.promptBookmarkUrl(),
44
+ });
45
+ }
46
+ async openBookmarks() {
47
+ const existing = this.app.workspace.getLeavesOfType(BookmarksViewType)[0];
48
+ const target = existing ?? this.app.workspace.ensureSideLeaf(BookmarksViewType, "left");
49
+ if (!existing) {
50
+ await target.setViewState({ type: BookmarksViewType, state: {} });
51
+ }
52
+ this.app.workspace.activateLeaf(target, {
53
+ focusRootHost: false,
54
+ source: "api",
55
+ operation: "open-bookmarks",
56
+ });
57
+ await this.app.workspace.revealLeaf(target);
58
+ }
59
+ async createGroup(parentCtime = null) {
60
+ return this.store.addGroup("Untitled group", parentCtime);
61
+ }
62
+ async promptAddBookmark() {
63
+ const target = bookmarkableTarget(this.app);
64
+ if (!target) {
65
+ new Notice("No active file or search to bookmark.");
66
+ return;
67
+ }
68
+ if (target.kind === "file") {
69
+ await this.promptAdd({
70
+ kind: "file",
71
+ targetLabel: "Path",
72
+ targetValue: target.file.path,
73
+ titlePlaceholder: target.file.basename || basename(target.file.path),
74
+ });
75
+ return;
76
+ }
77
+ await this.promptAdd({
78
+ kind: "search",
79
+ targetLabel: "Query",
80
+ targetValue: target.query,
81
+ titlePlaceholder: target.query || "Search",
82
+ });
83
+ }
84
+ async promptBookmarkUrl() {
85
+ await this.promptAdd({
86
+ kind: "url",
87
+ targetLabel: "URL",
88
+ targetValue: "",
89
+ titlePlaceholder: "Example",
90
+ targetEditable: true,
91
+ });
92
+ }
93
+ async promptAdd(draft) {
94
+ const confirmed = await new Promise((resolve) => {
95
+ const host = mountDialog(this.app, AddBookmarkDialog, {
96
+ targetLabel: draft.targetLabel,
97
+ targetValue: draft.targetValue,
98
+ titlePlaceholder: draft.titlePlaceholder,
99
+ targetEditable: draft.targetEditable ?? false,
100
+ portalProps: dialogPortalPropsForApp(this.app),
101
+ onCancel: () => {
102
+ host.close();
103
+ resolve(null);
104
+ },
105
+ onConfirm: (title, targetValue) => {
106
+ host.close();
107
+ resolve({ title, targetValue });
108
+ },
109
+ });
110
+ });
111
+ if (!confirmed)
112
+ return;
113
+ if (draft.kind === "url" && !isAllowedBookmarkUrl(confirmed.targetValue)) {
114
+ new Notice("Only http and https bookmark URLs can be opened.");
115
+ return;
116
+ }
117
+ const parentCtime = await this.pickGroup();
118
+ if (parentCtime === undefined)
119
+ return;
120
+ if (draft.kind === "file") {
121
+ await this.store.addFile(confirmed.targetValue, {
122
+ title: confirmed.title || undefined,
123
+ parentCtime,
124
+ });
125
+ return;
126
+ }
127
+ if (draft.kind === "search") {
128
+ await this.store.addSearch(confirmed.targetValue, {
129
+ title: confirmed.title || undefined,
130
+ parentCtime,
131
+ });
132
+ return;
133
+ }
134
+ await this.store.addUrl(confirmed.targetValue, {
135
+ title: confirmed.title || undefined,
136
+ parentCtime,
137
+ });
138
+ }
139
+ async pickGroup() {
140
+ const groups = [
141
+ { ctime: null, label: "Root", description: "Top level" },
142
+ ...listBookmarkGroups(this.store.items).map(({ item, crumbs }) => ({
143
+ ctime: item.ctime,
144
+ label: item.title || crumbs.at(-1) || "Untitled group",
145
+ description: crumbs.join(" / "),
146
+ })),
147
+ ];
148
+ return new Promise((resolve) => {
149
+ const host = mountDialog(this.app, PickBookmarkGroup, {
150
+ groups,
151
+ portalProps: dialogPortalPropsForApp(this.app),
152
+ onCancel: () => {
153
+ host.close();
154
+ resolve(undefined);
155
+ },
156
+ onSelect: (ctime, createTitle) => {
157
+ host.close();
158
+ void (async () => {
159
+ if (createTitle) {
160
+ const created = await this.store.addGroup(createTitle, ctime);
161
+ resolve(created.ctime);
162
+ return;
163
+ }
164
+ resolve(ctime);
165
+ })();
166
+ },
167
+ });
168
+ });
169
+ }
170
+ }
@@ -0,0 +1,75 @@
1
+ export declare const KNOWN_BOOKMARK_TYPES: readonly ["file", "folder", "group", "search", "url", "graph"];
2
+ export type KnownBookmarkType = (typeof KNOWN_BOOKMARK_TYPES)[number];
3
+ export interface FileBookmarkItem {
4
+ type: "file";
5
+ ctime: number;
6
+ path: string;
7
+ title?: string;
8
+ subpath?: string;
9
+ }
10
+ export interface FolderBookmarkItem {
11
+ type: "folder";
12
+ ctime: number;
13
+ path: string;
14
+ title?: string;
15
+ }
16
+ export interface GroupBookmarkItem {
17
+ type: "group";
18
+ ctime: number;
19
+ title?: string;
20
+ items: BookmarkItem[];
21
+ }
22
+ export interface SearchBookmarkItem {
23
+ type: "search";
24
+ ctime: number;
25
+ query: string;
26
+ title?: string;
27
+ }
28
+ export interface UrlBookmarkItem {
29
+ type: "url";
30
+ ctime: number;
31
+ url: string;
32
+ title?: string;
33
+ }
34
+ export interface GraphBookmarkItem {
35
+ type: "graph";
36
+ ctime: number;
37
+ title?: string;
38
+ }
39
+ export interface UnknownBookmarkItem {
40
+ type: string;
41
+ ctime: number;
42
+ title?: string;
43
+ [key: string]: unknown;
44
+ }
45
+ export type BookmarkItem = FileBookmarkItem | FolderBookmarkItem | GroupBookmarkItem | SearchBookmarkItem | UrlBookmarkItem | GraphBookmarkItem | UnknownBookmarkItem;
46
+ export interface BookmarksDocument {
47
+ items: BookmarkItem[];
48
+ }
49
+ export type BookmarkIconName = "file" | "folder" | "search" | "external-link" | "git-fork";
50
+ export declare function isKnownBookmarkType(type: string): type is KnownBookmarkType;
51
+ export declare function isFileBookmark(item: BookmarkItem): item is FileBookmarkItem;
52
+ export declare function isFolderBookmark(item: BookmarkItem): item is FolderBookmarkItem;
53
+ export declare function isGroupBookmark(item: BookmarkItem): item is GroupBookmarkItem;
54
+ export declare function isSearchBookmark(item: BookmarkItem): item is SearchBookmarkItem;
55
+ export declare function isUrlBookmark(item: BookmarkItem): item is UrlBookmarkItem;
56
+ export declare function isGraphBookmark(item: BookmarkItem): item is GraphBookmarkItem;
57
+ export declare function parseBookmarkItem(value: unknown): BookmarkItem;
58
+ export declare function parseBookmarkItems(value: unknown): BookmarkItem[];
59
+ export declare function cloneBookmarkItems(items: BookmarkItem[]): BookmarkItem[];
60
+ export declare function parseBookmarksDocument(value: unknown): BookmarksDocument;
61
+ export declare function serializeBookmarksDocument(document: BookmarksDocument): Record<string, unknown>;
62
+ export declare function bookmarkLabel(item: BookmarkItem): string;
63
+ export declare function bookmarkIcon(item: BookmarkItem): BookmarkIconName | null;
64
+ export declare function bookmarkSearchText(item: BookmarkItem): string;
65
+ export declare function nextBookmarkCtime(items: BookmarkItem[], now?: number): number;
66
+ export declare function walkBookmarkItems(items: BookmarkItem[], visit: (item: BookmarkItem, parent: GroupBookmarkItem | null) => void, parent?: GroupBookmarkItem | null): void;
67
+ export declare function listBookmarkGroups(items: BookmarkItem[], ancestors?: string[]): Array<{
68
+ item: GroupBookmarkItem;
69
+ crumbs: string[];
70
+ }>;
71
+ export declare function findBookmarkItem(items: BookmarkItem[], ctime: number): BookmarkItem | null;
72
+ export declare function isDescendantGroup(items: BookmarkItem[], ancestorCtime: number, candidateCtime: number): boolean;
73
+ export declare function removeBookmarkItem(items: BookmarkItem[], ctime: number): BookmarkItem | null;
74
+ export declare function insertBookmarkItem(items: BookmarkItem[], item: BookmarkItem, parentCtime: number | null, index: number): boolean;
75
+ export declare function rewriteBookmarkPaths(items: BookmarkItem[], fromPath: string, toPath: string): boolean;
@@ -0,0 +1,258 @@
1
+ import { basename } from "@lapis-notes/api";
2
+ export const KNOWN_BOOKMARK_TYPES = [
3
+ "file",
4
+ "folder",
5
+ "group",
6
+ "search",
7
+ "url",
8
+ "graph",
9
+ ];
10
+ function isRecord(value) {
11
+ return typeof value === "object" && value !== null && !Array.isArray(value);
12
+ }
13
+ function readCtime(value) {
14
+ return typeof value === "number" && Number.isFinite(value) ? value : Date.now();
15
+ }
16
+ function readOptionalString(value) {
17
+ return typeof value === "string" && value.length > 0 ? value : undefined;
18
+ }
19
+ export function isKnownBookmarkType(type) {
20
+ return KNOWN_BOOKMARK_TYPES.includes(type);
21
+ }
22
+ export function isFileBookmark(item) {
23
+ return item.type === "file";
24
+ }
25
+ export function isFolderBookmark(item) {
26
+ return item.type === "folder";
27
+ }
28
+ export function isGroupBookmark(item) {
29
+ return item.type === "group";
30
+ }
31
+ export function isSearchBookmark(item) {
32
+ return item.type === "search";
33
+ }
34
+ export function isUrlBookmark(item) {
35
+ return item.type === "url";
36
+ }
37
+ export function isGraphBookmark(item) {
38
+ return item.type === "graph";
39
+ }
40
+ export function parseBookmarkItem(value) {
41
+ if (!isRecord(value)) {
42
+ return { type: "unknown", ctime: Date.now() };
43
+ }
44
+ const type = typeof value.type === "string" ? value.type : "unknown";
45
+ const ctime = readCtime(value.ctime);
46
+ const title = readOptionalString(value.title);
47
+ const extras = { ...value };
48
+ if (type === "file") {
49
+ return {
50
+ ...extras,
51
+ type: "file",
52
+ ctime,
53
+ path: typeof value.path === "string" ? value.path : "",
54
+ ...(title ? { title } : {}),
55
+ ...(typeof value.subpath === "string" ? { subpath: value.subpath } : {}),
56
+ };
57
+ }
58
+ if (type === "folder") {
59
+ return {
60
+ ...extras,
61
+ type: "folder",
62
+ ctime,
63
+ path: typeof value.path === "string" ? value.path : "",
64
+ ...(title ? { title } : {}),
65
+ };
66
+ }
67
+ if (type === "group") {
68
+ return {
69
+ ...extras,
70
+ type: "group",
71
+ ctime,
72
+ items: parseBookmarkItems(value.items),
73
+ ...(title ? { title } : {}),
74
+ };
75
+ }
76
+ if (type === "search") {
77
+ return {
78
+ ...extras,
79
+ type: "search",
80
+ ctime,
81
+ query: typeof value.query === "string" ? value.query : "",
82
+ ...(title ? { title } : {}),
83
+ };
84
+ }
85
+ if (type === "url") {
86
+ return {
87
+ ...extras,
88
+ type: "url",
89
+ ctime,
90
+ url: typeof value.url === "string" ? value.url : "",
91
+ ...(title ? { title } : {}),
92
+ };
93
+ }
94
+ if (type === "graph") {
95
+ return {
96
+ ...extras,
97
+ type: "graph",
98
+ ctime,
99
+ ...(title ? { title } : {}),
100
+ };
101
+ }
102
+ return {
103
+ ...extras,
104
+ type,
105
+ ctime,
106
+ ...(title ? { title } : {}),
107
+ };
108
+ }
109
+ export function parseBookmarkItems(value) {
110
+ if (!Array.isArray(value))
111
+ return [];
112
+ return value.map((item) => parseBookmarkItem(item));
113
+ }
114
+ export function cloneBookmarkItems(items) {
115
+ return parseBookmarkItems(items);
116
+ }
117
+ export function parseBookmarksDocument(value) {
118
+ if (!isRecord(value))
119
+ return { items: [] };
120
+ return { items: parseBookmarkItems(value.items) };
121
+ }
122
+ export function serializeBookmarksDocument(document) {
123
+ return { items: document.items };
124
+ }
125
+ export function bookmarkLabel(item) {
126
+ if (item.title) {
127
+ return isFileBookmark(item) && item.subpath
128
+ ? `${item.title} ${item.subpath}`
129
+ : item.title;
130
+ }
131
+ if (isFileBookmark(item)) {
132
+ const name = basename(item.path) || item.path || "Untitled";
133
+ return item.subpath ? `${name} ${item.subpath}` : name;
134
+ }
135
+ if (isFolderBookmark(item)) {
136
+ return basename(item.path) || item.path || "Untitled folder";
137
+ }
138
+ if (isGroupBookmark(item))
139
+ return item.title || "Untitled group";
140
+ if (isSearchBookmark(item))
141
+ return item.query || "Search";
142
+ if (isUrlBookmark(item))
143
+ return item.url || "URL";
144
+ if (isGraphBookmark(item))
145
+ return "Graph";
146
+ return item.type;
147
+ }
148
+ export function bookmarkIcon(item) {
149
+ if (item.type === "group")
150
+ return null;
151
+ if (item.type === "file")
152
+ return "file";
153
+ if (item.type === "folder")
154
+ return "folder";
155
+ if (item.type === "search")
156
+ return "search";
157
+ if (item.type === "url")
158
+ return "external-link";
159
+ if (item.type === "graph")
160
+ return "git-fork";
161
+ return "file";
162
+ }
163
+ export function bookmarkSearchText(item) {
164
+ const parts = [item.title ?? "", item.type];
165
+ if ("path" in item && typeof item.path === "string")
166
+ parts.push(item.path);
167
+ if ("subpath" in item && typeof item.subpath === "string") {
168
+ parts.push(item.subpath);
169
+ }
170
+ if ("query" in item && typeof item.query === "string")
171
+ parts.push(item.query);
172
+ if ("url" in item && typeof item.url === "string")
173
+ parts.push(item.url);
174
+ return parts.join(" ").toLocaleLowerCase();
175
+ }
176
+ export function nextBookmarkCtime(items, now = Date.now()) {
177
+ const used = new Set();
178
+ walkBookmarkItems(items, (item) => {
179
+ used.add(item.ctime);
180
+ });
181
+ let ctime = now;
182
+ while (used.has(ctime))
183
+ ctime += 1;
184
+ return ctime;
185
+ }
186
+ export function walkBookmarkItems(items, visit, parent = null) {
187
+ for (const item of items) {
188
+ visit(item, parent);
189
+ if (isGroupBookmark(item)) {
190
+ walkBookmarkItems(item.items, visit, item);
191
+ }
192
+ }
193
+ }
194
+ export function listBookmarkGroups(items, ancestors = []) {
195
+ const groups = [];
196
+ for (const item of items) {
197
+ if (!isGroupBookmark(item))
198
+ continue;
199
+ const crumbs = [...ancestors, bookmarkLabel(item)];
200
+ groups.push({ item, crumbs });
201
+ groups.push(...listBookmarkGroups(item.items, crumbs));
202
+ }
203
+ return groups;
204
+ }
205
+ export function findBookmarkItem(items, ctime) {
206
+ for (const item of items) {
207
+ if (item.ctime === ctime)
208
+ return item;
209
+ if (isGroupBookmark(item)) {
210
+ const nested = findBookmarkItem(item.items, ctime);
211
+ if (nested)
212
+ return nested;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ export function isDescendantGroup(items, ancestorCtime, candidateCtime) {
218
+ const ancestor = findBookmarkItem(items, ancestorCtime);
219
+ if (!ancestor || !isGroupBookmark(ancestor))
220
+ return false;
221
+ return findBookmarkItem(ancestor.items, candidateCtime) !== null;
222
+ }
223
+ export function removeBookmarkItem(items, ctime) {
224
+ const index = items.findIndex((item) => item.ctime === ctime);
225
+ if (index >= 0) {
226
+ return items.splice(index, 1)[0] ?? null;
227
+ }
228
+ for (const item of items) {
229
+ if (isGroupBookmark(item)) {
230
+ const removed = removeBookmarkItem(item.items, ctime);
231
+ if (removed)
232
+ return removed;
233
+ }
234
+ }
235
+ return null;
236
+ }
237
+ export function insertBookmarkItem(items, item, parentCtime, index) {
238
+ if (parentCtime === null) {
239
+ items.splice(Math.max(0, Math.min(index, items.length)), 0, item);
240
+ return true;
241
+ }
242
+ const parent = findBookmarkItem(items, parentCtime);
243
+ if (!parent || !isGroupBookmark(parent))
244
+ return false;
245
+ parent.items.splice(Math.max(0, Math.min(index, parent.items.length)), 0, item);
246
+ return true;
247
+ }
248
+ export function rewriteBookmarkPaths(items, fromPath, toPath) {
249
+ let changed = false;
250
+ walkBookmarkItems(items, (item) => {
251
+ if ((isFileBookmark(item) || isFolderBookmark(item)) &&
252
+ item.path === fromPath) {
253
+ item.path = toPath;
254
+ changed = true;
255
+ }
256
+ });
257
+ return changed;
258
+ }
@@ -0,0 +1,44 @@
1
+ import type { App } from "@lapis-notes/api";
2
+ import { type BookmarkItem, type FileBookmarkItem, type FolderBookmarkItem, type GroupBookmarkItem, type SearchBookmarkItem, type UrlBookmarkItem } from "./bookmarks-schema";
3
+ export interface BookmarksPersistence {
4
+ load(): Promise<unknown>;
5
+ save(data: unknown): Promise<void>;
6
+ }
7
+ export declare class BookmarksStore {
8
+ #private;
9
+ private readonly persistence;
10
+ constructor(persistence: BookmarksPersistence);
11
+ get items(): BookmarkItem[];
12
+ subscribe(listener: () => void): () => void;
13
+ load(): Promise<void>;
14
+ addItem(item: BookmarkItem, parentCtime?: number | null): Promise<BookmarkItem>;
15
+ addFile(path: string, options?: {
16
+ title?: string;
17
+ subpath?: string;
18
+ parentCtime?: number | null;
19
+ }): Promise<FileBookmarkItem>;
20
+ addFolder(path: string, options?: {
21
+ title?: string;
22
+ parentCtime?: number | null;
23
+ }): Promise<FolderBookmarkItem>;
24
+ addGroup(title?: string, parentCtime?: number | null): Promise<GroupBookmarkItem>;
25
+ addSearch(query: string, options?: {
26
+ title?: string;
27
+ parentCtime?: number | null;
28
+ }): Promise<SearchBookmarkItem>;
29
+ addUrl(url: string, options?: {
30
+ title?: string;
31
+ parentCtime?: number | null;
32
+ }): Promise<UrlBookmarkItem>;
33
+ renameItem(ctime: number, title: string): Promise<void>;
34
+ removeItem(ctime: number): Promise<void>;
35
+ moveItem(ctime: number, parentCtime: number | null, index: number): Promise<boolean>;
36
+ rewritePaths(fromPath: string, toPath: string): Promise<void>;
37
+ private notify;
38
+ private persist;
39
+ }
40
+ export declare function createPluginBookmarksPersistence(app: {
41
+ loadData(): Promise<unknown>;
42
+ saveData(data: unknown): Promise<void>;
43
+ }): BookmarksPersistence;
44
+ export declare function createVaultRenameBinding(app: App, store: BookmarksStore): () => void;
@@ -0,0 +1,143 @@
1
+ import { findBookmarkItem, insertBookmarkItem, isDescendantGroup, isGroupBookmark, nextBookmarkCtime, parseBookmarksDocument, removeBookmarkItem, rewriteBookmarkPaths, serializeBookmarksDocument, } from "./bookmarks-schema";
2
+ export class BookmarksStore {
3
+ persistence;
4
+ #document = { items: [] };
5
+ #listeners = new Set();
6
+ constructor(persistence) {
7
+ this.persistence = persistence;
8
+ }
9
+ get items() {
10
+ return this.#document.items;
11
+ }
12
+ subscribe(listener) {
13
+ this.#listeners.add(listener);
14
+ return () => this.#listeners.delete(listener);
15
+ }
16
+ async load() {
17
+ this.#document = parseBookmarksDocument(await this.persistence.load());
18
+ this.notify();
19
+ }
20
+ async addItem(item, parentCtime = null) {
21
+ const created = {
22
+ ...item,
23
+ ctime: item.ctime ?? nextBookmarkCtime(this.#document.items),
24
+ };
25
+ if (isGroupBookmark(created) && !Array.isArray(created.items)) {
26
+ created.items = [];
27
+ }
28
+ const parent = parentCtime
29
+ ? findBookmarkItem(this.#document.items, parentCtime)
30
+ : null;
31
+ insertBookmarkItem(this.#document.items, created, parentCtime, parentCtime === null
32
+ ? this.#document.items.length
33
+ : parent && isGroupBookmark(parent)
34
+ ? parent.items.length
35
+ : 0);
36
+ await this.persist();
37
+ return created;
38
+ }
39
+ async addFile(path, options = {}) {
40
+ const item = {
41
+ type: "file",
42
+ ctime: nextBookmarkCtime(this.#document.items),
43
+ path,
44
+ ...(options.title ? { title: options.title } : {}),
45
+ ...(options.subpath ? { subpath: options.subpath } : {}),
46
+ };
47
+ return (await this.addItem(item, options.parentCtime ?? null));
48
+ }
49
+ async addFolder(path, options = {}) {
50
+ const item = {
51
+ type: "folder",
52
+ ctime: nextBookmarkCtime(this.#document.items),
53
+ path,
54
+ ...(options.title ? { title: options.title } : {}),
55
+ };
56
+ return (await this.addItem(item, options.parentCtime ?? null));
57
+ }
58
+ async addGroup(title = "Untitled group", parentCtime = null) {
59
+ const item = {
60
+ type: "group",
61
+ ctime: nextBookmarkCtime(this.#document.items),
62
+ title,
63
+ items: [],
64
+ };
65
+ return (await this.addItem(item, parentCtime));
66
+ }
67
+ async addSearch(query, options = {}) {
68
+ const item = {
69
+ type: "search",
70
+ ctime: nextBookmarkCtime(this.#document.items),
71
+ query,
72
+ ...(options.title ? { title: options.title } : {}),
73
+ };
74
+ return (await this.addItem(item, options.parentCtime ?? null));
75
+ }
76
+ async addUrl(url, options = {}) {
77
+ const item = {
78
+ type: "url",
79
+ ctime: nextBookmarkCtime(this.#document.items),
80
+ url,
81
+ ...(options.title ? { title: options.title } : {}),
82
+ };
83
+ return (await this.addItem(item, options.parentCtime ?? null));
84
+ }
85
+ async renameItem(ctime, title) {
86
+ const item = findBookmarkItem(this.#document.items, ctime);
87
+ if (!item)
88
+ return;
89
+ const next = title.trim();
90
+ if (next)
91
+ item.title = next;
92
+ else
93
+ delete item.title;
94
+ await this.persist();
95
+ }
96
+ async removeItem(ctime) {
97
+ removeBookmarkItem(this.#document.items, ctime);
98
+ await this.persist();
99
+ }
100
+ async moveItem(ctime, parentCtime, index) {
101
+ if (parentCtime === ctime)
102
+ return false;
103
+ if (parentCtime !== null &&
104
+ isDescendantGroup(this.#document.items, ctime, parentCtime)) {
105
+ return false;
106
+ }
107
+ const removed = removeBookmarkItem(this.#document.items, ctime);
108
+ if (!removed)
109
+ return false;
110
+ const inserted = insertBookmarkItem(this.#document.items, removed, parentCtime, index);
111
+ if (!inserted) {
112
+ insertBookmarkItem(this.#document.items, removed, null, this.#document.items.length);
113
+ return false;
114
+ }
115
+ await this.persist();
116
+ return true;
117
+ }
118
+ async rewritePaths(fromPath, toPath) {
119
+ if (!rewriteBookmarkPaths(this.#document.items, fromPath, toPath))
120
+ return;
121
+ await this.persist();
122
+ }
123
+ notify() {
124
+ for (const listener of this.#listeners)
125
+ listener();
126
+ }
127
+ async persist() {
128
+ this.notify();
129
+ await this.persistence.save(serializeBookmarksDocument(this.#document));
130
+ }
131
+ }
132
+ export function createPluginBookmarksPersistence(app) {
133
+ return {
134
+ load: () => app.loadData(),
135
+ save: (data) => app.saveData(data),
136
+ };
137
+ }
138
+ export function createVaultRenameBinding(app, store) {
139
+ const ref = app.vault.on("rename", (file, oldPath) => {
140
+ void store.rewritePaths(oldPath, file.path);
141
+ });
142
+ return () => app.vault.offref(ref);
143
+ }
@@ -0,0 +1,2 @@
1
+ export declare const BookmarksViewType = "bookmarks";
2
+ export declare const BOOKMARKS_PLUGIN_ID = "bookmarks";
@@ -0,0 +1,2 @@
1
+ export const BookmarksViewType = "bookmarks";
2
+ export const BOOKMARKS_PLUGIN_ID = "bookmarks";