@silverbulletmd/silverbullet 2.10.0 → 2.11.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.
Files changed (100) hide show
  1. package/README.md +4 -4
  2. package/client/asset_bundle/bundle.ts +0 -1
  3. package/client/config.ts +343 -0
  4. package/client/markdown_parser/constants.ts +3 -1
  5. package/client/plugos/hooks/command.ts +0 -5
  6. package/client/plugos/hooks/event.ts +0 -6
  7. package/client/plugos/hooks/mq.ts +0 -3
  8. package/client/plugos/hooks/slash_command.ts +1 -7
  9. package/client/plugos/hooks/syscall.ts +0 -4
  10. package/client/plugos/manifest_cache.ts +0 -32
  11. package/client/plugos/plug.ts +0 -3
  12. package/client/plugos/plug_compile.ts +3 -13
  13. package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
  14. package/client/plugos/syscalls/editor.ts +128 -53
  15. package/client/plugos/syscalls/event.ts +0 -2
  16. package/client/plugos/syscalls/fetch.ts +0 -4
  17. package/client/plugos/syscalls/icon.ts +46 -0
  18. package/client/plugos/syscalls/index.ts +33 -13
  19. package/client/plugos/syscalls/jsonschema.ts +0 -1
  20. package/client/plugos/syscalls/mq.ts +0 -1
  21. package/client/plugos/syscalls/navigator.ts +143 -0
  22. package/client/plugos/syscalls/search.ts +44 -0
  23. package/client/plugos/syscalls/shell.ts +0 -1
  24. package/client/plugos/syscalls/space.ts +250 -7
  25. package/client/plugos/syscalls/sync.ts +56 -4
  26. package/client/plugos/syscalls/system.ts +38 -0
  27. package/client/plugos/system.ts +7 -5
  28. package/client/plugos/worker_runtime.ts +0 -2
  29. package/client/space_lua/aggregates.ts +0 -16
  30. package/client/space_lua/ast.ts +0 -5
  31. package/client/space_lua/ast_narrow.ts +0 -5
  32. package/client/space_lua/budget.ts +113 -0
  33. package/client/space_lua/budget_ui.ts +49 -0
  34. package/client/space_lua/eval.ts +113 -88
  35. package/client/space_lua/labels.ts +0 -4
  36. package/client/space_lua/numeric.ts +0 -5
  37. package/client/space_lua/parse.ts +0 -17
  38. package/client/space_lua/quarantine.ts +127 -0
  39. package/client/space_lua/query_collection.ts +3 -47
  40. package/client/space_lua/query_env.ts +0 -2
  41. package/client/space_lua/render_lua_markdown.ts +2 -7
  42. package/client/space_lua/render_widget.ts +18 -1
  43. package/client/space_lua/runtime.ts +50 -70
  44. package/client/space_lua/stdlib/format.ts +0 -12
  45. package/client/space_lua/stdlib/js.ts +0 -2
  46. package/client/space_lua/stdlib/load.ts +0 -1
  47. package/client/space_lua/stdlib/math.ts +0 -7
  48. package/client/space_lua/stdlib/net.ts +0 -4
  49. package/client/space_lua/stdlib/os.ts +0 -10
  50. package/client/space_lua/stdlib/pattern.ts +10 -15
  51. package/client/space_lua/stdlib/space_lua.ts +1 -2
  52. package/client/space_lua/stdlib/string_pack.ts +0 -3
  53. package/client/space_lua/stdlib/table.ts +0 -5
  54. package/client/space_lua/stdlib.ts +18 -31
  55. package/client/space_lua/tonumber.ts +0 -9
  56. package/dist/plug-compile.js +2 -3
  57. package/package.json +13 -3
  58. package/plug-api/lib/async.ts +1 -6
  59. package/plug-api/lib/collation.ts +22 -0
  60. package/plug-api/lib/crypto.ts +16 -15
  61. package/plug-api/lib/dates.ts +33 -0
  62. package/plug-api/lib/fuzzy.ts +282 -0
  63. package/plug-api/lib/json.ts +0 -6
  64. package/plug-api/lib/limited_map.ts +0 -2
  65. package/plug-api/lib/link_write.ts +27 -0
  66. package/plug-api/lib/ref.ts +28 -5
  67. package/plug-api/lib/resolve.ts +0 -1
  68. package/plug-api/lib/resolve_path.ts +290 -0
  69. package/plug-api/lib/shortcut.ts +15 -0
  70. package/plug-api/lib/tags.ts +0 -3
  71. package/plug-api/lib/transclusion.ts +28 -1
  72. package/plug-api/lib/tree.ts +0 -5
  73. package/plug-api/lib/yaml.ts +3 -42
  74. package/plug-api/syscall.ts +1 -5
  75. package/plug-api/syscalls/config.ts +2 -2
  76. package/plug-api/syscalls/editor.ts +59 -213
  77. package/plug-api/syscalls/icon.ts +18 -0
  78. package/plug-api/syscalls/index.ts +22 -4
  79. package/plug-api/syscalls/search.ts +11 -0
  80. package/plug-api/syscalls/space.ts +122 -5
  81. package/plug-api/syscalls/sync.ts +23 -2
  82. package/plug-api/syscalls/system.ts +46 -0
  83. package/plug-api/syscalls.ts +2 -0
  84. package/plug-api/system_mock.ts +13 -0
  85. package/plug-api/types/client.ts +4 -3
  86. package/plug-api/types/datastore.ts +11 -2
  87. package/plug-api/types/index.ts +10 -0
  88. package/plug-api/types/profile.ts +13 -0
  89. package/plug-api/types/revisions.ts +67 -0
  90. package/plug-api/ui/description.ts +64 -0
  91. package/plug-api/ui/hover.ts +69 -0
  92. package/plug-api/ui/index.ts +58 -2
  93. package/plug-api/ui/scroll.ts +30 -0
  94. package/plug-api/ui/tree_model.ts +263 -0
  95. package/plug-api/ui/tree_types.ts +66 -0
  96. package/plug-api/ui/use_fit_collapse.ts +67 -0
  97. package/plugs/builtin_plugs.ts +0 -1
  98. package/plugs/index/types.ts +9 -0
  99. package/plug-api/lib/memory_cache.ts +0 -21
  100. /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
@@ -0,0 +1,30 @@
1
+ /**
2
+ * `block: "nearest"` scrolling, confined to one container.
3
+ *
4
+ * scrollIntoView can cross a same-origin iframe boundary and scroll the host
5
+ * editor. Restricting the calculation to one container prevents that.
6
+ */
7
+ export function revealInContainer(el: Element, container: Element) {
8
+ const c = container.getBoundingClientRect();
9
+ const e = el.getBoundingClientRect();
10
+ if (e.top < c.top) {
11
+ container.scrollTop += e.top - c.top;
12
+ } else if (e.bottom > c.bottom) {
13
+ // An element taller than the container aligns to its top rather than
14
+ // scrolling past it, same as `block: "nearest"`.
15
+ container.scrollTop += Math.min(e.bottom - c.bottom, e.top - c.top);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * `revealInContainer`, with the container found by walking up from `el` via
21
+ * `closest(selector)` -- for a caller that doesn't already hold a reference
22
+ * to the scroll container.
23
+ */
24
+ export function revealInClosest(
25
+ el: Element | null | undefined,
26
+ selector: string,
27
+ ) {
28
+ const container = el?.closest(selector);
29
+ if (el && container) revealInContainer(el, container);
30
+ }
@@ -0,0 +1,263 @@
1
+ import type { Row } from "./tree_types.ts";
2
+
3
+ export type TreeNode = {
4
+ path: string;
5
+ segment: string;
6
+ row?: Row;
7
+ children: TreeNode[];
8
+ isFolder: boolean;
9
+ };
10
+
11
+ export type VisibleRow = { node: TreeNode; depth: number };
12
+
13
+ /** A folder reads only its own row's priority: an implicit folder, or one
14
+ * whose page carries no priority, stays where the alphabet put it however its
15
+ * children are decorated. */
16
+ function priorityOf(node: TreeNode): number {
17
+ const priority = node.row?.priority;
18
+ return typeof priority === "number" && Number.isFinite(priority)
19
+ ? priority
20
+ : 0;
21
+ }
22
+
23
+ export function buildTree(
24
+ rows: Row[],
25
+ separator: string,
26
+ foldersFirst: boolean,
27
+ ): TreeNode {
28
+ const root: TreeNode = {
29
+ path: "",
30
+ segment: "",
31
+ children: [],
32
+ isFolder: true,
33
+ };
34
+ const byPath = new Map<string, TreeNode>([["", root]]);
35
+ const ensure = (path: string): TreeNode => {
36
+ let node = byPath.get(path);
37
+ if (node) return node;
38
+ const idx = path.lastIndexOf(separator);
39
+ const parent = ensure(idx === -1 ? "" : path.slice(0, idx));
40
+ node = {
41
+ path,
42
+ segment: idx === -1 ? path : path.slice(idx + separator.length),
43
+ children: [],
44
+ isFolder: false,
45
+ };
46
+ byPath.set(path, node);
47
+ parent.children.push(node);
48
+ parent.isFolder = true;
49
+ return node;
50
+ };
51
+ for (const row of rows) {
52
+ const path = String(row.obj.name ?? row.primary);
53
+ ensure(path).row = row;
54
+ }
55
+ // Stable, so everything a row says nothing about keeps the order the source
56
+ // put it in -- which is what makes an undecorated tree alphabetical.
57
+ const sortLevel = (n: TreeNode) => {
58
+ n.children.sort((a, b) => {
59
+ if (foldersFirst && a.isFolder !== b.isFolder) return a.isFolder ? -1 : 1;
60
+ return priorityOf(b) - priorityOf(a);
61
+ });
62
+ n.children.forEach(sortLevel);
63
+ };
64
+ sortLevel(root);
65
+ return root;
66
+ }
67
+
68
+ export function flattenVisible(
69
+ root: TreeNode,
70
+ expanded: Set<string>,
71
+ ): VisibleRow[] {
72
+ const out: VisibleRow[] = [];
73
+ const walk = (node: TreeNode, depth: number) => {
74
+ for (const child of node.children) {
75
+ out.push({ node: child, depth });
76
+ if (child.isFolder && expanded.has(child.path)) walk(child, depth + 1);
77
+ }
78
+ };
79
+ walk(root, 0);
80
+ return out;
81
+ }
82
+
83
+ export function pruneTree(
84
+ root: TreeNode,
85
+ scores: Map<string, number>,
86
+ ): TreeNode {
87
+ // Post-order: each node's best score is computed once, from its
88
+ // already-scored children, rather than re-walked on every sort comparison.
89
+ const bestScores = new Map<TreeNode, number>();
90
+ const prune = (n: TreeNode): TreeNode | null => {
91
+ const children = n.children
92
+ .map(prune)
93
+ .filter((c): c is TreeNode => c !== null);
94
+ if (children.length === 0 && !scores.has(n.path)) return null;
95
+ const copy = { ...n, children };
96
+ bestScores.set(
97
+ copy,
98
+ Math.max(
99
+ scores.get(n.path) ?? -1,
100
+ ...children.map((c) => bestScores.get(c)!),
101
+ ),
102
+ );
103
+ copy.children.sort((a, b) => bestScores.get(b)! - bestScores.get(a)!);
104
+ return copy;
105
+ };
106
+ return prune(root) ?? { ...root, children: [] };
107
+ }
108
+
109
+ export function ancestorPaths(path: string, separator: string): string[] {
110
+ const parts = path.split(separator);
111
+ return parts
112
+ .slice(0, -1)
113
+ .map((_, i) => parts.slice(0, i + 1).join(separator));
114
+ }
115
+
116
+ /**
117
+ * `prev` with `paths` expanded. Under `expandAll` expanding is a *removal*:
118
+ * the set holds what the user collapsed, and everything not in it is already
119
+ * open. Toggling needs no such branch -- flipping membership means "collapse"
120
+ * under one reading and "expand" under the other, which is the same gesture.
121
+ */
122
+ export function withExpanded(
123
+ prev: Set<string>,
124
+ paths: string[],
125
+ expandAll: boolean,
126
+ ): Set<string> {
127
+ const next = new Set(prev);
128
+ for (const path of paths) {
129
+ if (expandAll) next.delete(path);
130
+ else next.add(path);
131
+ }
132
+ return next;
133
+ }
134
+
135
+ export function allFolderPaths(node: TreeNode): Set<string> {
136
+ const out = new Set<string>();
137
+ const walk = (n: TreeNode) => {
138
+ for (const child of n.children) {
139
+ if (child.isFolder) {
140
+ out.add(child.path);
141
+ walk(child);
142
+ }
143
+ }
144
+ };
145
+ walk(node);
146
+ return out;
147
+ }
148
+
149
+ /**
150
+ * The object a node hands to a caller. A copy, deliberately: `isFolder` is a
151
+ * UI-injected hint, and stamping it onto the row's own object would leave it
152
+ * on the caller's cached row forever. Same shape `planMove` produces, so a
153
+ * folder reaches downstream code looking the same either way.
154
+ */
155
+ export function nodeObject(node: TreeNode): Record<string, any> {
156
+ const obj: Record<string, any> = node.row
157
+ ? { ...node.row.obj }
158
+ : { name: node.path };
159
+ if (node.isFolder) obj.isFolder = true;
160
+ return obj;
161
+ }
162
+
163
+ /** Every node under `root`, in tree order (the root itself excluded). */
164
+ export function allNodes(root: TreeNode): TreeNode[] {
165
+ const out: TreeNode[] = [];
166
+ const walk = (n: TreeNode) => {
167
+ for (const child of n.children) {
168
+ out.push(child);
169
+ walk(child);
170
+ }
171
+ };
172
+ walk(root);
173
+ return out;
174
+ }
175
+
176
+ export function findNode(root: TreeNode, path: string): TreeNode | undefined {
177
+ if (path === root.path) return root;
178
+ for (const child of root.children) {
179
+ if (path === child.path) return child;
180
+ if (path.startsWith(child.path)) {
181
+ const found = findNode(child, path);
182
+ if (found) return found;
183
+ }
184
+ }
185
+ return undefined;
186
+ }
187
+
188
+ export type MovePlan =
189
+ | { kind: "none" }
190
+ | { kind: "collision"; newName: string }
191
+ | { kind: "move"; obj: Record<string, any>; newName: string };
192
+
193
+ /**
194
+ * What dropping `draggedPath` on `targetFolder` (`""` = root) amounts to: the
195
+ * new name, the object to hand the mover, or a reason not to move at all.
196
+ * Kept here, away from the DOM, because every interesting case is a
197
+ * path/tree question -- and the drop handler must not guess at any of them.
198
+ */
199
+ export function planMove(
200
+ root: TreeNode,
201
+ draggedPath: string,
202
+ targetFolder: string,
203
+ separator: string,
204
+ ): MovePlan {
205
+ if (!draggedPath || draggedPath === targetFolder) return { kind: "none" };
206
+ // Into its own subtree: the prefix rename would chase itself.
207
+ if (targetFolder.startsWith(draggedPath + separator)) return { kind: "none" };
208
+ const idx = draggedPath.lastIndexOf(separator);
209
+ const segment =
210
+ idx === -1 ? draggedPath : draggedPath.slice(idx + separator.length);
211
+ const newName =
212
+ targetFolder === "" ? segment : targetFolder + separator + segment;
213
+ if (newName === draggedPath) return { kind: "none" };
214
+ const node = findNode(root, draggedPath);
215
+ if (!node) return { kind: "none" };
216
+ // Any node, not just row-bearing ones: dropping onto a name that already
217
+ // exists as a folder would merge two subtrees, silently overwriting every
218
+ // page whose name appears in both.
219
+ if (findNode(root, newName)) return { kind: "collision", newName };
220
+ return { kind: "move", obj: nodeObject(node), newName };
221
+ }
222
+
223
+ export type TreeDisplay = {
224
+ tree: TreeNode;
225
+ visible: VisibleRow[];
226
+ effectiveExpanded: Set<string>;
227
+ };
228
+
229
+ /**
230
+ * Combines build + (optional) prune + auto-expand-while-filtering + flatten
231
+ * into the single derivation both a tree renderer and its host's keyboard
232
+ * math need, so they never compute two different visible-row lists.
233
+ *
234
+ * `effectiveExpanded` is the one answer to "is this folder open", whichever
235
+ * reading `state` has: everything downstream -- the chevrons, `ArrowLeft`,
236
+ * spring-loading -- asks it and never the raw set.
237
+ *
238
+ * @param state the persisted set, and how to read it: the folders the user
239
+ * opened, or (under `expandAll`) the ones they closed.
240
+ */
241
+ export function computeTreeDisplay(
242
+ rows: Row[],
243
+ separator: string,
244
+ foldersFirst: boolean,
245
+ state: { expanded: Set<string>; expandAll: boolean },
246
+ scores?: Map<string, number>,
247
+ ): TreeDisplay {
248
+ const built = buildTree(rows, separator, foldersFirst);
249
+ const tree = scores ? pruneTree(built, scores) : built;
250
+ // Filtering force-expands the pruned tree either way, so the two readings
251
+ // converge there and neither set is consulted -- no double bookkeeping.
252
+ let effectiveExpanded: Set<string>;
253
+ if (scores) {
254
+ effectiveExpanded = allFolderPaths(tree);
255
+ } else if (state.expandAll) {
256
+ effectiveExpanded = allFolderPaths(tree);
257
+ for (const path of state.expanded) effectiveExpanded.delete(path);
258
+ } else {
259
+ effectiveExpanded = state.expanded;
260
+ }
261
+ const visible = flattenVisible(tree, effectiveExpanded);
262
+ return { tree, visible, effectiveExpanded };
263
+ }
@@ -0,0 +1,66 @@
1
+ import type { Description } from "./description.ts";
2
+
3
+ export type Decoration = {
4
+ text?: string;
5
+ icon?: string;
6
+ cssClass?: string;
7
+ position?: "left" | "right";
8
+ /** Native tooltip, for a chip whose text is deliberately imprecise. */
9
+ title?: string;
10
+ };
11
+
12
+ export type Row = {
13
+ obj: Record<string, any>;
14
+ primary: string;
15
+ /**
16
+ * What a *tree* row shows in place of its last path segment. For a view
17
+ * whose hierarchy is synthesized rather than read off a name, the path is
18
+ * what nests the row and this is what it reads as.
19
+ */
20
+ label?: string;
21
+ description?: Description;
22
+ decorations?: Decoration[];
23
+ /** Extra CSS classes for the row element itself. */
24
+ cssClass?: string;
25
+ /** Tree mode: floats the row above its lower-priority siblings (default 0).
26
+ * Only ever reorders one level; a folder takes its own row's priority. */
27
+ priority?: number;
28
+ };
29
+
30
+ /**
31
+ * A row action, minus whatever decides what it does and whether it applies:
32
+ * the renderer only ever knows what to draw and which index to hand back.
33
+ */
34
+ export type ActionMeta = {
35
+ /**
36
+ * A Feather name (bare or `feather:`-prefixed), or literal SVG markup (a
37
+ * string starting with `<svg` after trimming leading whitespace) --
38
+ * resolved to a node by the consuming plug.
39
+ */
40
+ icon?: string;
41
+ label: string;
42
+ /** Whether the action has a `when` predicate, i.e. needs a mask to show. */
43
+ hasWhen: boolean;
44
+ /** `"rw"` hides the action while the client is in read-only mode. */
45
+ requireMode?: "rw";
46
+ };
47
+
48
+ /**
49
+ * What a consumer has to say about one rendered thing: which actions apply to
50
+ * it, and which icon it draws with. Meant to be computed for a whole batch at
51
+ * once, never per hover, per keystroke, or per selection change.
52
+ */
53
+ export type RowState = {
54
+ /** Per-action `when` result, parallel to the action list. */
55
+ actions?: boolean[];
56
+ /** The row's icon, as a template node to clone. */
57
+ icon?: Element;
58
+ };
59
+
60
+ /** Row state by rendered thing. Exactly one half is populated, per mode. */
61
+ export type RowStates = {
62
+ /** Tree mode, keyed by node path -- covers folders and page/folder duals. */
63
+ byPath?: Map<string, RowState>;
64
+ /** List mode, keyed by the row itself. */
65
+ byRow?: WeakMap<Row, RowState>;
66
+ };
@@ -0,0 +1,67 @@
1
+ import { useLayoutEffect, useRef, useState } from "preact/hooks";
2
+
3
+ export type FitLevel = "full" | "icons";
4
+
5
+ /**
6
+ * Measures whether a row of items (buttons in `barRef`, inside `wrapRef`)
7
+ * fits at full width, collapsing to `"icons"` when it doesn't.
8
+ *
9
+ * The fit is measured rather than guessed at a breakpoint: how wide the bar
10
+ * wants to be depends on its items' own labels, which no `@container`
11
+ * condition can express. The measurement happens on layout, only when the
12
+ * wrapper actually resizes or `signature` changes -- never per frame.
13
+ *
14
+ * `signature` should change whenever the set of items (or which of them carry
15
+ * icons) changes, so a fresh full-width measurement is taken rather than
16
+ * reusing one from a different set of items.
17
+ */
18
+ export function useFitCollapse(
19
+ wrapRef: { current: HTMLElement | null },
20
+ barRef: { current: HTMLElement | null },
21
+ signature: string,
22
+ ): FitLevel {
23
+ const [fit, setFit] = useState<FitLevel>("full");
24
+ // The full-width rendering's own width, recorded while it's on screen: a
25
+ // collapsed bar can no longer be asked what the fuller one wanted, and this
26
+ // is what lets a widened wrapper snap straight back to "full" instead of
27
+ // staying collapsed until something else forces a re-measure.
28
+ const fullWidth = useRef<number | undefined>(undefined);
29
+
30
+ useLayoutEffect(() => {
31
+ fullWidth.current = undefined;
32
+ setFit("full");
33
+ }, [signature]);
34
+
35
+ useLayoutEffect(() => {
36
+ const bar = barRef.current;
37
+ const wrap = wrapRef.current;
38
+ if (!bar || !wrap) return;
39
+ const measure = () => {
40
+ // Summed rather than read off the bar: it may wrap as a last resort,
41
+ // and a wrapped bar reports the width it settled for, never the width
42
+ // it wanted.
43
+ const style = getComputedStyle(bar);
44
+ const items = [...bar.children] as HTMLElement[];
45
+ const gaps =
46
+ (parseFloat(style.columnGap) || 0) * Math.max(0, items.length - 1);
47
+ const padding =
48
+ (parseFloat(style.paddingLeft) || 0) +
49
+ (parseFloat(style.paddingRight) || 0);
50
+ const wanted =
51
+ items.reduce((sum, s) => sum + s.getBoundingClientRect().width, 0) +
52
+ gaps +
53
+ padding;
54
+ if (fit === "full") fullWidth.current = wanted;
55
+
56
+ const available = wrap.clientWidth;
57
+ const full = fullWidth.current;
58
+ setFit(full !== undefined && full > available ? "icons" : "full");
59
+ };
60
+ measure();
61
+ const observer = new ResizeObserver(measure);
62
+ observer.observe(wrap);
63
+ return () => observer.disconnect();
64
+ }, [fit, signature]);
65
+
66
+ return fit;
67
+ }
@@ -1,6 +1,5 @@
1
1
  // TODO: Figure out how to keep this up-to-date automatically
2
2
  export const builtinPlugNames = [
3
- "core",
4
3
  "editor",
5
4
  "index",
6
5
  "sync",
@@ -0,0 +1,9 @@
1
+ export type AnchorHit = {
2
+ page: string;
3
+ hostTag: string;
4
+ };
5
+
6
+ export type ResolveAnchorResult =
7
+ | { ok: true; page: string; hostTag: string; range: [number, number] }
8
+ | { ok: false; reason: "missing" }
9
+ | { ok: false; reason: "duplicate"; hits: AnchorHit[] };
@@ -1,21 +0,0 @@
1
- import { LimitedMap } from "./limited_map.ts";
2
-
3
- const cache = new LimitedMap<any>(50);
4
-
5
- export async function ttlCache<K, V>(
6
- key: K,
7
- fn: (key: K) => Promise<V>,
8
- ttlSecs?: number,
9
- ): Promise<V> {
10
- if (!ttlSecs) {
11
- return fn(key);
12
- }
13
- const serializedKey = JSON.stringify(key);
14
- const cached = cache.get(serializedKey);
15
- if (cached) {
16
- return cached;
17
- }
18
- const result = await fn(key);
19
- cache.set(serializedKey, result, ttlSecs * 1000);
20
- return result;
21
- }
File without changes