@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.
- package/CHANGELOG.md +6 -0
- package/LICENSE.md +8 -0
- package/README.md +20 -0
- package/dist/activate-bookmark.d.ts +11 -0
- package/dist/activate-bookmark.js +113 -0
- package/dist/add-bookmark-dialog.svelte +83 -0
- package/dist/add-bookmark-dialog.svelte.d.ts +15 -0
- package/dist/bookmarks-filter.d.ts +2 -0
- package/dist/bookmarks-filter.js +19 -0
- package/dist/bookmarks-panel.svelte +654 -0
- package/dist/bookmarks-panel.svelte.d.ts +13 -0
- package/dist/bookmarks-plugin.d.ts +15 -0
- package/dist/bookmarks-plugin.js +170 -0
- package/dist/bookmarks-schema.d.ts +75 -0
- package/dist/bookmarks-schema.js +258 -0
- package/dist/bookmarks-store.d.ts +44 -0
- package/dist/bookmarks-store.js +143 -0
- package/dist/bookmarks-view-type.d.ts +2 -0
- package/dist/bookmarks-view-type.js +2 -0
- package/dist/bookmarks-view.d.ts +21 -0
- package/dist/bookmarks-view.js +43 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/mount-dialog.d.ts +8 -0
- package/dist/mount-dialog.js +16 -0
- package/dist/pick-bookmark-group.svelte +76 -0
- package/dist/pick-bookmark-group.svelte.d.ts +16 -0
- package/dist/public-components.d.ts +1 -0
- package/dist/public-components.js +1 -0
- package/dist/styles.css +1 -0
- package/manifest.json +26 -0
- package/package.json +72 -0
- package/styles.css +1 -0
package/CHANGELOG.md
ADDED
package/LICENSE.md
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# License
|
|
2
|
+
|
|
3
|
+
Copyright (c) LapisMD contributors.
|
|
4
|
+
|
|
5
|
+
This software is licensed under the GNU Affero General Public License, version
|
|
6
|
+
3 or (at your option) any later version (`AGPL-3.0-or-later`). The complete
|
|
7
|
+
license text is available from the
|
|
8
|
+
[GNU Project](https://www.gnu.org/licenses/agpl-3.0.html).
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Bookmarks
|
|
2
|
+
|
|
3
|
+
Bookmark files, folders, searches, and groups
|
|
4
|
+
|
|
5
|
+
## Install for static composition
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
pnpm add @lapis-notes/bookmarks
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Register the exported plugin class in the application's `PluginProfile`. Import
|
|
12
|
+
`@lapis-notes/bookmarks/styles.css?inline` and pass the CSS through the static
|
|
13
|
+
registration so the host owns its lifecycle.
|
|
14
|
+
|
|
15
|
+
The runtime plugin ID is `bookmarks`. A matching signed
|
|
16
|
+
`bookmarks-0.1.0.lapis-plugin` archive is attached to the
|
|
17
|
+
package-scoped GitHub release for manual or registry installation.
|
|
18
|
+
|
|
19
|
+
See the [repository README](https://github.com/lapismd/lapis-plugins#readme) for
|
|
20
|
+
development, validation, and release-gate details.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type App, type TFile } from "@lapis-notes/api";
|
|
2
|
+
import { type BookmarkItem } from "./bookmarks-schema";
|
|
3
|
+
export declare function isAllowedBookmarkUrl(url: string): boolean;
|
|
4
|
+
export declare function activateBookmark(app: App, item: BookmarkItem, openUrl?: (url: string) => void): Promise<void>;
|
|
5
|
+
export declare function bookmarkableTarget(app: App): {
|
|
6
|
+
kind: "file";
|
|
7
|
+
file: TFile;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "search";
|
|
10
|
+
query: string;
|
|
11
|
+
} | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Notice, TextFileView, resolveSubpath, } from "@lapis-notes/api";
|
|
2
|
+
import { isFileBookmark, isFolderBookmark, isGraphBookmark, isGroupBookmark, isSearchBookmark, isUrlBookmark, } from "./bookmarks-schema";
|
|
3
|
+
const FILE_EXPLORER_REVEAL_PATH_COMMAND = "lapis-file-explorer:reveal-path";
|
|
4
|
+
const SEARCH_OPEN_COMMAND = "search:open-search";
|
|
5
|
+
const GRAPH_VIEW_TYPE = "graph";
|
|
6
|
+
export function isAllowedBookmarkUrl(url) {
|
|
7
|
+
try {
|
|
8
|
+
const parsed = new URL(url);
|
|
9
|
+
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export async function activateBookmark(app, item, openUrl = defaultOpenUrl) {
|
|
16
|
+
if (isFileBookmark(item)) {
|
|
17
|
+
await activateFileBookmark(app, item.path, item.subpath);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (isFolderBookmark(item)) {
|
|
21
|
+
if (!app.vault.getAbstractFileByPath(item.path)) {
|
|
22
|
+
new Notice(`Unable to find folder: ${item.path || "unknown"}`);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
await app.commands.executeCommand(FILE_EXPLORER_REVEAL_PATH_COMMAND, item.path);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (isSearchBookmark(item)) {
|
|
29
|
+
await app.commands.executeCommand(SEARCH_OPEN_COMMAND, item.query);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
if (isUrlBookmark(item)) {
|
|
33
|
+
if (!isAllowedBookmarkUrl(item.url)) {
|
|
34
|
+
new Notice("Only http and https bookmark URLs can be opened.");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
openUrl(item.url);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (isGraphBookmark(item)) {
|
|
41
|
+
await activateGraphBookmark(app);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (isGroupBookmark(item))
|
|
45
|
+
return;
|
|
46
|
+
new Notice(`Unsupported bookmark type: ${item.type}`);
|
|
47
|
+
}
|
|
48
|
+
async function activateFileBookmark(app, path, subpath) {
|
|
49
|
+
const file = app.vault.getFileByPath(path);
|
|
50
|
+
if (!file) {
|
|
51
|
+
new Notice(`Unable to find file: ${path || "unknown"}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
await app.openFile(file);
|
|
55
|
+
if (!subpath)
|
|
56
|
+
return;
|
|
57
|
+
const cache = app.metadataCache.getFileCache(file);
|
|
58
|
+
if (!cache)
|
|
59
|
+
return;
|
|
60
|
+
const resolved = resolveSubpath(cache, subpath);
|
|
61
|
+
if (!resolved)
|
|
62
|
+
return;
|
|
63
|
+
const view = app.workspace.activeLeaf?.view;
|
|
64
|
+
if (!(view instanceof TextFileView))
|
|
65
|
+
return;
|
|
66
|
+
view.editor.setCursor({
|
|
67
|
+
line: resolved.start.line,
|
|
68
|
+
ch: resolved.start.col,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
async function activateGraphBookmark(app) {
|
|
72
|
+
const existing = app.workspace.getLeavesOfType(GRAPH_VIEW_TYPE)[0];
|
|
73
|
+
if (existing) {
|
|
74
|
+
app.workspace.activateLeaf(existing, {
|
|
75
|
+
focusRootHost: false,
|
|
76
|
+
source: "api",
|
|
77
|
+
operation: "open-graph-bookmark",
|
|
78
|
+
});
|
|
79
|
+
await app.workspace.revealLeaf(existing);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
const leaf = app.workspace.getLeaf();
|
|
83
|
+
await leaf.setViewState({ type: GRAPH_VIEW_TYPE, state: {} });
|
|
84
|
+
if (leaf.view.getViewType() === GRAPH_VIEW_TYPE) {
|
|
85
|
+
app.workspace.activateLeaf(leaf, {
|
|
86
|
+
focusRootHost: false,
|
|
87
|
+
source: "api",
|
|
88
|
+
operation: "open-graph-bookmark",
|
|
89
|
+
});
|
|
90
|
+
await app.workspace.revealLeaf(leaf);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
leaf.detach();
|
|
94
|
+
new Notice("Graph is not available.");
|
|
95
|
+
}
|
|
96
|
+
function defaultOpenUrl(url) {
|
|
97
|
+
if (typeof window === "undefined")
|
|
98
|
+
return;
|
|
99
|
+
window.open(url, "_blank", "noopener,noreferrer");
|
|
100
|
+
}
|
|
101
|
+
export function bookmarkableTarget(app) {
|
|
102
|
+
const file = app.workspace.getActiveFile();
|
|
103
|
+
if (file)
|
|
104
|
+
return { kind: "file", file };
|
|
105
|
+
const leaf = app.workspace.activeLeaf;
|
|
106
|
+
if (leaf?.view.getViewType() === "search") {
|
|
107
|
+
const query = typeof leaf.view.getState().query === "string"
|
|
108
|
+
? String(leaf.view.getState().query)
|
|
109
|
+
: "";
|
|
110
|
+
return { kind: "search", query };
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "@lapismd/design-core/shadcn/button";
|
|
3
|
+
import * as Dialog from "@lapismd/design-core/shadcn/dialog";
|
|
4
|
+
import { Input } from "@lapismd/design-core/shadcn/input";
|
|
5
|
+
|
|
6
|
+
let {
|
|
7
|
+
open = true,
|
|
8
|
+
targetLabel,
|
|
9
|
+
targetValue,
|
|
10
|
+
titlePlaceholder,
|
|
11
|
+
targetEditable = false,
|
|
12
|
+
portalProps,
|
|
13
|
+
onCancel,
|
|
14
|
+
onConfirm,
|
|
15
|
+
}: {
|
|
16
|
+
open?: boolean;
|
|
17
|
+
targetLabel: string;
|
|
18
|
+
targetValue: string;
|
|
19
|
+
titlePlaceholder: string;
|
|
20
|
+
targetEditable?: boolean;
|
|
21
|
+
portalProps: { to: HTMLElement };
|
|
22
|
+
onCancel: () => void;
|
|
23
|
+
onConfirm: (title: string, targetValue: string) => void;
|
|
24
|
+
} = $props();
|
|
25
|
+
|
|
26
|
+
let title = $state("");
|
|
27
|
+
let editedTarget = $state("");
|
|
28
|
+
|
|
29
|
+
function submit(): void {
|
|
30
|
+
const nextTarget = (targetEditable ? editedTarget : targetValue).trim();
|
|
31
|
+
if (targetEditable && !nextTarget) return;
|
|
32
|
+
onConfirm(title.trim(), nextTarget);
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<Dialog.Root
|
|
37
|
+
{open}
|
|
38
|
+
onOpenChange={(next) => {
|
|
39
|
+
if (!next) onCancel();
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Dialog.Content {portalProps} class="bookmarks-add-dialog">
|
|
43
|
+
<Dialog.Header>
|
|
44
|
+
<Dialog.Title>Add bookmark</Dialog.Title>
|
|
45
|
+
<Dialog.Description>
|
|
46
|
+
Optional title defaults to the {targetLabel.toLowerCase()}.
|
|
47
|
+
</Dialog.Description>
|
|
48
|
+
</Dialog.Header>
|
|
49
|
+
<div class="bookmarks-add-dialog__fields">
|
|
50
|
+
<label class="bookmarks-add-dialog__field">
|
|
51
|
+
<span>{targetLabel}</span>
|
|
52
|
+
{#if targetEditable}
|
|
53
|
+
<Input bind:value={editedTarget} placeholder={titlePlaceholder} />
|
|
54
|
+
{:else}
|
|
55
|
+
<Input value={targetValue} readonly />
|
|
56
|
+
{/if}
|
|
57
|
+
</label>
|
|
58
|
+
<label class="bookmarks-add-dialog__field">
|
|
59
|
+
<span>Title</span>
|
|
60
|
+
<Input bind:value={title} placeholder={titlePlaceholder} />
|
|
61
|
+
</label>
|
|
62
|
+
</div>
|
|
63
|
+
<Dialog.Footer>
|
|
64
|
+
<Button variant="outline" onclick={onCancel}>Cancel</Button>
|
|
65
|
+
<Button onclick={submit}>Continue</Button>
|
|
66
|
+
</Dialog.Footer>
|
|
67
|
+
</Dialog.Content>
|
|
68
|
+
</Dialog.Root>
|
|
69
|
+
|
|
70
|
+
<style>
|
|
71
|
+
.bookmarks-add-dialog__fields {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-direction: column;
|
|
74
|
+
gap: 0.75rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.bookmarks-add-dialog__field {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: 0.35rem;
|
|
81
|
+
font-size: 0.75rem;
|
|
82
|
+
}
|
|
83
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
open?: boolean;
|
|
3
|
+
targetLabel: string;
|
|
4
|
+
targetValue: string;
|
|
5
|
+
titlePlaceholder: string;
|
|
6
|
+
targetEditable?: boolean;
|
|
7
|
+
portalProps: {
|
|
8
|
+
to: HTMLElement;
|
|
9
|
+
};
|
|
10
|
+
onCancel: () => void;
|
|
11
|
+
onConfirm: (title: string, targetValue: string) => void;
|
|
12
|
+
};
|
|
13
|
+
declare const AddBookmarkDialog: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
14
|
+
type AddBookmarkDialog = ReturnType<typeof AddBookmarkDialog>;
|
|
15
|
+
export default AddBookmarkDialog;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { bookmarkSearchText, isGroupBookmark, } from "./bookmarks-schema";
|
|
2
|
+
export function filterBookmarkItems(items, query) {
|
|
3
|
+
const needle = query.trim().toLocaleLowerCase();
|
|
4
|
+
if (!needle)
|
|
5
|
+
return items;
|
|
6
|
+
const next = [];
|
|
7
|
+
for (const item of items) {
|
|
8
|
+
if (isGroupBookmark(item)) {
|
|
9
|
+
const children = filterBookmarkItems(item.items, query);
|
|
10
|
+
if (children.length > 0 || bookmarkSearchText(item).includes(needle)) {
|
|
11
|
+
next.push({ ...item, items: children });
|
|
12
|
+
}
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (bookmarkSearchText(item).includes(needle))
|
|
16
|
+
next.push(item);
|
|
17
|
+
}
|
|
18
|
+
return next;
|
|
19
|
+
}
|