@silverbulletmd/silverbullet 2.9.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.
- package/README.md +38 -25
- package/client/asset_bundle/bundle.ts +0 -1
- package/client/config.ts +343 -0
- package/client/markdown_parser/constants.ts +3 -1
- package/client/plugos/hooks/command.ts +0 -5
- package/client/plugos/hooks/event.ts +0 -6
- package/client/plugos/hooks/mq.ts +0 -3
- package/client/plugos/hooks/slash_command.ts +1 -7
- package/client/plugos/hooks/syscall.ts +24 -14
- package/client/plugos/manifest_cache.ts +0 -32
- package/client/plugos/plug.ts +0 -3
- package/client/plugos/plug_compile.ts +3 -13
- package/client/plugos/sandboxes/worker_sandbox.ts +19 -9
- package/client/plugos/syscalls/asset.ts +61 -23
- package/client/plugos/syscalls/clientStore.ts +32 -6
- package/client/plugos/syscalls/client_code_widget.ts +9 -4
- package/client/plugos/syscalls/code_widget.ts +26 -11
- package/client/plugos/syscalls/config.ts +146 -30
- package/client/plugos/syscalls/datastore.ts +146 -40
- package/client/plugos/syscalls/editor.ts +1518 -722
- package/client/plugos/syscalls/event.ts +44 -13
- package/client/plugos/syscalls/fetch.ts +120 -83
- package/client/plugos/syscalls/icon.ts +46 -0
- package/client/plugos/syscalls/index.ts +267 -121
- package/client/plugos/syscalls/jsonschema.ts +107 -9
- package/client/plugos/syscalls/language.ts +38 -12
- package/client/plugos/syscalls/markdown.ts +166 -50
- package/client/plugos/syscalls/mq.ts +92 -20
- package/client/plugos/syscalls/navigator.ts +143 -0
- package/client/plugos/syscalls/schema_introspection.ts +33 -0
- package/client/plugos/syscalls/search.ts +44 -0
- package/client/plugos/syscalls/service_registry.ts +74 -20
- package/client/plugos/syscalls/shell.ts +51 -21
- package/client/plugos/syscalls/space.ts +412 -113
- package/client/plugos/syscalls/sync.ts +103 -20
- package/client/plugos/syscalls/system.ts +274 -139
- package/client/plugos/system.ts +28 -10
- package/client/plugos/worker_runtime.ts +0 -2
- package/client/space_lua/aggregates.ts +0 -16
- package/client/space_lua/api_documentation.ts +107 -0
- package/client/space_lua/ast.ts +13 -5
- package/client/space_lua/ast_narrow.ts +0 -5
- package/client/space_lua/budget.ts +113 -0
- package/client/space_lua/budget_ui.ts +49 -0
- package/client/space_lua/eval.ts +125 -92
- package/client/space_lua/labels.ts +0 -4
- package/client/space_lua/numeric.ts +0 -5
- package/client/space_lua/parse.ts +243 -125
- package/client/space_lua/pretty_print.ts +128 -54
- package/client/space_lua/quarantine.ts +127 -0
- package/client/space_lua/query_collection.ts +7 -51
- package/client/space_lua/query_env.ts +0 -2
- package/client/space_lua/render_lua_markdown.ts +2 -7
- package/client/space_lua/render_widget.ts +173 -0
- package/client/space_lua/runtime.ts +133 -92
- package/client/space_lua/stdlib/crypto.ts +8 -3
- package/client/space_lua/stdlib/encoding.ts +27 -9
- package/client/space_lua/stdlib/format.ts +0 -12
- package/client/space_lua/stdlib/js.ts +136 -23
- package/client/space_lua/stdlib/load.ts +24 -16
- package/client/space_lua/stdlib/math.ts +331 -133
- package/client/space_lua/stdlib/net.ts +57 -13
- package/client/space_lua/stdlib/os.ts +111 -54
- package/client/space_lua/stdlib/pattern.ts +10 -15
- package/client/space_lua/stdlib/space_lua.ts +344 -21
- package/client/space_lua/stdlib/string.ts +282 -104
- package/client/space_lua/stdlib/string_pack.ts +58 -22
- package/client/space_lua/stdlib/table.ts +195 -45
- package/client/space_lua/stdlib.ts +354 -125
- package/client/space_lua/syscalls.ts +270 -0
- package/client/space_lua/tonumber.ts +0 -9
- package/dist/plug-compile.js +2 -3
- package/package.json +15 -5
- package/plug-api/lib/async.ts +1 -6
- package/plug-api/lib/collation.ts +22 -0
- package/plug-api/lib/crypto.ts +16 -15
- package/plug-api/lib/dates.ts +33 -0
- package/plug-api/lib/fuzzy.ts +282 -0
- package/plug-api/lib/json.ts +0 -6
- package/plug-api/lib/limited_map.ts +0 -2
- package/plug-api/lib/link_write.ts +27 -0
- package/plug-api/{ui → lib}/panel_styles.ts +4 -3
- package/plug-api/lib/ref.ts +124 -5
- package/plug-api/lib/resolve.ts +0 -1
- package/plug-api/lib/resolve_path.ts +290 -0
- package/plug-api/lib/shortcut.ts +17 -1
- package/plug-api/lib/tags.ts +0 -3
- package/plug-api/lib/transclusion.ts +28 -1
- package/plug-api/lib/tree.ts +0 -5
- package/plug-api/lib/yaml.ts +3 -42
- package/plug-api/syscall.ts +1 -5
- package/plug-api/syscalls/config.ts +7 -5
- package/plug-api/syscalls/editor.ts +70 -208
- package/plug-api/syscalls/icon.ts +18 -0
- package/plug-api/syscalls/index.ts +39 -5
- package/plug-api/syscalls/jsonschema.ts +9 -0
- package/plug-api/syscalls/lua.ts +7 -0
- package/plug-api/syscalls/search.ts +11 -0
- package/plug-api/syscalls/shell.ts +0 -1
- package/plug-api/syscalls/space.ts +122 -5
- package/plug-api/syscalls/sync.ts +23 -2
- package/plug-api/syscalls/system.ts +53 -0
- package/plug-api/syscalls.ts +2 -0
- package/plug-api/system_mock.ts +15 -2
- package/plug-api/types/client.ts +4 -3
- package/plug-api/types/datastore.ts +11 -2
- package/plug-api/types/index.ts +62 -1
- package/plug-api/types/manifest.ts +22 -4
- package/plug-api/types/profile.ts +13 -0
- package/plug-api/types/revisions.ts +67 -0
- package/plug-api/ui/cx.ts +1 -3
- package/plug-api/ui/description.ts +64 -0
- package/plug-api/ui/hover.ts +69 -0
- package/plug-api/ui/index.ts +61 -2
- package/plug-api/ui/scroll.ts +30 -0
- package/plug-api/ui/slugify.ts +44 -0
- package/plug-api/ui/tree_model.ts +263 -0
- package/plug-api/ui/tree_types.ts +66 -0
- package/plug-api/ui/use_fit_collapse.ts +67 -0
- package/plugs/builtin_plugs.ts +0 -1
- package/plugs/index/types.ts +9 -0
- package/client/plugos/syscalls/lua.ts +0 -81
- package/plug-api/lib/memory_cache.ts +0 -21
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
function normalize(s: string): string {
|
|
2
|
+
return s
|
|
3
|
+
.normalize("NFD")
|
|
4
|
+
.replace(/\p{Diacritic}/gu, "")
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const WORD_BOUNDARY_CHARS = new Set(["/", "-", "_", " ", ".", ":"]);
|
|
9
|
+
|
|
10
|
+
function isWordBoundaryAt(
|
|
11
|
+
original: string,
|
|
12
|
+
lowered: string,
|
|
13
|
+
i: number,
|
|
14
|
+
): boolean {
|
|
15
|
+
if (i === 0) return true;
|
|
16
|
+
const prev = lowered[i - 1];
|
|
17
|
+
if (WORD_BOUNDARY_CHARS.has(prev)) return true;
|
|
18
|
+
// CamelCase boundary: previous original char is lowercase, current is uppercase
|
|
19
|
+
const origPrev = original[i - 1];
|
|
20
|
+
const origCur = original[i];
|
|
21
|
+
if (
|
|
22
|
+
origPrev &&
|
|
23
|
+
origCur &&
|
|
24
|
+
origPrev === origPrev.toLowerCase() &&
|
|
25
|
+
origPrev !== origPrev.toUpperCase() &&
|
|
26
|
+
origCur === origCur.toUpperCase() &&
|
|
27
|
+
origCur !== origCur.toLowerCase()
|
|
28
|
+
) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function bestSubstringEditDistance(token: string, candidate: string): number {
|
|
35
|
+
const tl = token.length;
|
|
36
|
+
const cl = candidate.length;
|
|
37
|
+
let prevPrev = new Array(cl + 1).fill(0);
|
|
38
|
+
let prev = new Array(cl + 1).fill(0);
|
|
39
|
+
let curr = new Array(cl + 1);
|
|
40
|
+
|
|
41
|
+
for (let i = 1; i <= tl; i++) {
|
|
42
|
+
curr[0] = i;
|
|
43
|
+
for (let j = 1; j <= cl; j++) {
|
|
44
|
+
const cost = token[i - 1] === candidate[j - 1] ? 0 : 1;
|
|
45
|
+
let v = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
46
|
+
if (
|
|
47
|
+
i > 1 &&
|
|
48
|
+
j > 1 &&
|
|
49
|
+
token[i - 1] === candidate[j - 2] &&
|
|
50
|
+
token[i - 2] === candidate[j - 1]
|
|
51
|
+
) {
|
|
52
|
+
v = Math.min(v, prevPrev[j - 2] + 1);
|
|
53
|
+
}
|
|
54
|
+
curr[j] = v;
|
|
55
|
+
}
|
|
56
|
+
const tmp = prevPrev;
|
|
57
|
+
prevPrev = prev;
|
|
58
|
+
prev = curr;
|
|
59
|
+
curr = tmp;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let best = prev[0];
|
|
63
|
+
for (let j = 1; j <= cl; j++) {
|
|
64
|
+
if (prev[j] < best) best = prev[j];
|
|
65
|
+
}
|
|
66
|
+
return best;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function typoScore(token: string, candidate: string): number {
|
|
70
|
+
if (token.length < 4) return 0;
|
|
71
|
+
const max = Math.min(2, Math.floor(token.length / 4));
|
|
72
|
+
if (max < 1) return 0;
|
|
73
|
+
|
|
74
|
+
const bestDist = bestSubstringEditDistance(token, candidate);
|
|
75
|
+
if (bestDist > max) return 0;
|
|
76
|
+
return 0.25 - 0.05 * bestDist; // 0.25 / 0.20 / 0.15
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function subsequenceScore(
|
|
80
|
+
token: string,
|
|
81
|
+
candidate: string,
|
|
82
|
+
candidateOriginal: string,
|
|
83
|
+
): number {
|
|
84
|
+
// Greedy left-to-right match; track contiguous runs and word-boundary hits.
|
|
85
|
+
let ti = 0;
|
|
86
|
+
let firstMatch = -1;
|
|
87
|
+
let lastMatch = -1;
|
|
88
|
+
let contiguousRunSum = 0;
|
|
89
|
+
let currentRun = 0;
|
|
90
|
+
let boundaryHits = 0;
|
|
91
|
+
let prevMatchCi = -2;
|
|
92
|
+
|
|
93
|
+
for (let ci = 0; ci < candidate.length && ti < token.length; ci++) {
|
|
94
|
+
if (candidate[ci] === token[ti]) {
|
|
95
|
+
if (firstMatch < 0) firstMatch = ci;
|
|
96
|
+
lastMatch = ci;
|
|
97
|
+
if (isWordBoundaryAt(candidateOriginal, candidate, ci)) boundaryHits++;
|
|
98
|
+
if (ci === prevMatchCi + 1) {
|
|
99
|
+
currentRun++;
|
|
100
|
+
} else {
|
|
101
|
+
contiguousRunSum += currentRun * currentRun;
|
|
102
|
+
currentRun = 1;
|
|
103
|
+
}
|
|
104
|
+
prevMatchCi = ci;
|
|
105
|
+
ti++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
contiguousRunSum += currentRun * currentRun;
|
|
109
|
+
|
|
110
|
+
if (ti < token.length) return 0; // not a subsequence at all
|
|
111
|
+
|
|
112
|
+
// Normalize: token length over span (penalizes wide spans)
|
|
113
|
+
const span = lastMatch - firstMatch + 1;
|
|
114
|
+
const density = token.length / span; // in (0, 1]
|
|
115
|
+
const boundaryBonus = boundaryHits / token.length; // in [0, 1]
|
|
116
|
+
const contiguityBonus = contiguousRunSum / (token.length * token.length); // in (0, 1]
|
|
117
|
+
|
|
118
|
+
// Weighted combination scaled into [0.30, 0.65]
|
|
119
|
+
const raw = 0.5 * density + 0.3 * contiguityBonus + 0.2 * boundaryBonus;
|
|
120
|
+
return 0.3 + raw * 0.35;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function scoreToken(token: string, candidate: string): number {
|
|
124
|
+
if (!token) return 0;
|
|
125
|
+
const t = normalize(token);
|
|
126
|
+
const cOrig = candidate;
|
|
127
|
+
const c = normalize(candidate);
|
|
128
|
+
|
|
129
|
+
// Tier 1: exact equality
|
|
130
|
+
if (c === t) return 1.0;
|
|
131
|
+
|
|
132
|
+
// Tier 2: prefix
|
|
133
|
+
if (c.startsWith(t)) {
|
|
134
|
+
return isWordBoundaryAt(cOrig, c, 0) ? 0.95 : 0.9;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Tier 3: substring. Search for the best occurrence — a word-boundary
|
|
138
|
+
// match wins even if it appears later than a non-boundary match.
|
|
139
|
+
{
|
|
140
|
+
let bestIdx = -1;
|
|
141
|
+
let bestBoundary = false;
|
|
142
|
+
let idx = c.indexOf(t);
|
|
143
|
+
while (idx >= 0) {
|
|
144
|
+
const boundary = isWordBoundaryAt(cOrig, c, idx);
|
|
145
|
+
if (bestIdx < 0 || (boundary && !bestBoundary)) {
|
|
146
|
+
bestIdx = idx;
|
|
147
|
+
bestBoundary = boundary;
|
|
148
|
+
if (boundary) break; // can't get better
|
|
149
|
+
}
|
|
150
|
+
idx = c.indexOf(t, idx + 1);
|
|
151
|
+
}
|
|
152
|
+
if (bestIdx >= 0) {
|
|
153
|
+
// For very short tokens (<=2 chars), only count substring matches at
|
|
154
|
+
// word boundaries — otherwise short tokens generate too much noise.
|
|
155
|
+
if (t.length > 2 || bestBoundary) {
|
|
156
|
+
return bestBoundary ? 0.8 : 0.75;
|
|
157
|
+
}
|
|
158
|
+
// fall through for short non-boundary substring matches
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Tier 4: subsequence (short tokens skip this tier; substring at boundary
|
|
163
|
+
// is the only way for them to match)
|
|
164
|
+
if (t.length > 2) {
|
|
165
|
+
const subseq = subsequenceScore(t, c, cOrig);
|
|
166
|
+
if (subseq > 0) return subseq;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Tier 5: bounded typo
|
|
170
|
+
const typo = typoScore(t, c);
|
|
171
|
+
if (typo > 0) return typo;
|
|
172
|
+
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type RankField = { weight: number; segments?: boolean };
|
|
177
|
+
export type RankOptions<T> = {
|
|
178
|
+
fields?: Record<string, number | RankField>;
|
|
179
|
+
orderId?: (obj: T) => number;
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const DEFAULT_FIELDS: Record<string, number | RankField> = {
|
|
183
|
+
name: { weight: 1.0, segments: true },
|
|
184
|
+
displayName: 0.9,
|
|
185
|
+
aliases: 0.85,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Whether `phrase` equals one of `obj`'s ranked fields verbatim (normalized).
|
|
190
|
+
* Per-token scoring (below) never penalizes a candidate for carrying *extra*
|
|
191
|
+
* words the phrase didn't ask for -- "Foo" and "Foo Bar" score identically
|
|
192
|
+
* against the phrase "Foo", since each of the phrase's tokens matches
|
|
193
|
+
* somewhere in either candidate just as well. That leaves same-score ties
|
|
194
|
+
* exactly where a user's full, exact phrase should still win outright, which
|
|
195
|
+
* is what this tie-break (see `rank`'s sort) is for.
|
|
196
|
+
*/
|
|
197
|
+
function hasExactFieldMatch<T extends Record<string, any>>(
|
|
198
|
+
obj: T,
|
|
199
|
+
normalizedPhrase: string,
|
|
200
|
+
fields: Record<string, number | RankField>,
|
|
201
|
+
): boolean {
|
|
202
|
+
for (const fieldName of Object.keys(fields)) {
|
|
203
|
+
const raw = obj[fieldName];
|
|
204
|
+
const values: unknown[] = Array.isArray(raw)
|
|
205
|
+
? raw
|
|
206
|
+
: raw != null
|
|
207
|
+
? [raw]
|
|
208
|
+
: [];
|
|
209
|
+
for (const value of values) {
|
|
210
|
+
if (normalize(String(value)) === normalizedPhrase) return true;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function rank<T extends Record<string, any>>(
|
|
217
|
+
objects: T[],
|
|
218
|
+
phrase: string,
|
|
219
|
+
options: RankOptions<T> = {},
|
|
220
|
+
): (T & { score: number })[] {
|
|
221
|
+
const fields = options.fields ?? DEFAULT_FIELDS;
|
|
222
|
+
const orderId = options.orderId ?? (() => 0);
|
|
223
|
+
const normalizedPhrase = normalize(phrase);
|
|
224
|
+
const tokens = normalizedPhrase.split(/\s+/).filter((t) => t.length > 0);
|
|
225
|
+
if (tokens.length === 0) {
|
|
226
|
+
return [...objects]
|
|
227
|
+
.sort(
|
|
228
|
+
(a, b) =>
|
|
229
|
+
orderId(a) - orderId(b) ||
|
|
230
|
+
String(a.name ?? "").localeCompare(String(b.name ?? "")),
|
|
231
|
+
)
|
|
232
|
+
.map((o) => ({ ...o, score: 1 }));
|
|
233
|
+
}
|
|
234
|
+
const results: (T & { score: number })[] = [];
|
|
235
|
+
for (const obj of objects) {
|
|
236
|
+
let product = 1;
|
|
237
|
+
let excluded = false;
|
|
238
|
+
for (const token of tokens) {
|
|
239
|
+
let best = 0;
|
|
240
|
+
for (const [fieldName, fieldSpec] of Object.entries(fields)) {
|
|
241
|
+
const spec: RankField =
|
|
242
|
+
typeof fieldSpec === "number" ? { weight: fieldSpec } : fieldSpec;
|
|
243
|
+
const raw = obj[fieldName];
|
|
244
|
+
const values: string[] = Array.isArray(raw)
|
|
245
|
+
? raw.map(String)
|
|
246
|
+
: raw != null
|
|
247
|
+
? [String(raw)]
|
|
248
|
+
: [];
|
|
249
|
+
for (const value of values) {
|
|
250
|
+
if (spec.segments && value.includes("/")) {
|
|
251
|
+
const last = value.slice(value.lastIndexOf("/") + 1);
|
|
252
|
+
best = Math.max(
|
|
253
|
+
best,
|
|
254
|
+
scoreToken(token, last) * spec.weight,
|
|
255
|
+
scoreToken(token, value) * spec.weight * 0.85,
|
|
256
|
+
);
|
|
257
|
+
} else {
|
|
258
|
+
best = Math.max(best, scoreToken(token, value) * spec.weight);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (best === 0) {
|
|
263
|
+
excluded = true;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
product *= best;
|
|
267
|
+
}
|
|
268
|
+
if (!excluded) {
|
|
269
|
+
results.push({ ...obj, score: product ** (1 / tokens.length) });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return results.sort((a, b) => {
|
|
273
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
274
|
+
const aExact = hasExactFieldMatch(a, normalizedPhrase, fields);
|
|
275
|
+
const bExact = hasExactFieldMatch(b, normalizedPhrase, fields);
|
|
276
|
+
if (aExact !== bExact) return aExact ? -1 : 1;
|
|
277
|
+
return (
|
|
278
|
+
orderId(a) - orderId(b) ||
|
|
279
|
+
String(a.name ?? "").localeCompare(String(b.name ?? ""))
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
}
|
package/plug-api/lib/json.ts
CHANGED
|
@@ -50,7 +50,6 @@ export function deepEqual(a: any, b: any): boolean {
|
|
|
50
50
|
* @param d the date to convert
|
|
51
51
|
*/
|
|
52
52
|
export function cleanStringDate(d: Date): string {
|
|
53
|
-
// If no significant time, return a date string only
|
|
54
53
|
if (
|
|
55
54
|
d.getUTCHours() === 0 &&
|
|
56
55
|
d.getUTCMinutes() === 0 &&
|
|
@@ -85,7 +84,6 @@ export function cleanupJSON(a: any): any {
|
|
|
85
84
|
if (Array.isArray(a)) {
|
|
86
85
|
return a.map(cleanupJSON);
|
|
87
86
|
}
|
|
88
|
-
// If a is a date, convert to a string
|
|
89
87
|
if (a instanceof Date) {
|
|
90
88
|
return cleanStringDate(a);
|
|
91
89
|
}
|
|
@@ -106,17 +104,14 @@ export function cleanupJSON(a: any): any {
|
|
|
106
104
|
}
|
|
107
105
|
|
|
108
106
|
export function deepClone<T>(obj: T, ignoreKeys: string[] = []): T {
|
|
109
|
-
// Handle null, undefined, or primitive types (string, number, boolean, symbol, bigint)
|
|
110
107
|
if (obj === null || typeof obj !== "object") {
|
|
111
108
|
return obj;
|
|
112
109
|
}
|
|
113
110
|
|
|
114
|
-
// Handle Date
|
|
115
111
|
if (obj instanceof Date) {
|
|
116
112
|
return new Date(obj.getTime()) as any;
|
|
117
113
|
}
|
|
118
114
|
|
|
119
|
-
// Handle Array
|
|
120
115
|
if (Array.isArray(obj)) {
|
|
121
116
|
const arrClone: any[] = [];
|
|
122
117
|
for (let i = 0; i < obj.length; i++) {
|
|
@@ -125,7 +120,6 @@ export function deepClone<T>(obj: T, ignoreKeys: string[] = []): T {
|
|
|
125
120
|
return arrClone as any;
|
|
126
121
|
}
|
|
127
122
|
|
|
128
|
-
// Handle Object
|
|
129
123
|
if (obj instanceof Object) {
|
|
130
124
|
const objClone: { [key: string]: any } = {};
|
|
131
125
|
for (const key in obj) {
|
|
@@ -31,7 +31,6 @@ export class LimitedMap<V> {
|
|
|
31
31
|
}, ttl);
|
|
32
32
|
}
|
|
33
33
|
if (this.map.size >= this.maxSize) {
|
|
34
|
-
// Remove the oldest key before adding a new one
|
|
35
34
|
const oldestKey = this.getOldestKey();
|
|
36
35
|
this.map.delete(oldestKey!);
|
|
37
36
|
}
|
|
@@ -41,7 +40,6 @@ export class LimitedMap<V> {
|
|
|
41
40
|
get(key: string): V | undefined {
|
|
42
41
|
const entry = this.map.get(key);
|
|
43
42
|
if (entry) {
|
|
44
|
-
// Update the last accessed timestamp
|
|
45
43
|
entry.la = Date.now();
|
|
46
44
|
return entry.value;
|
|
47
45
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { config } from "../syscalls.ts";
|
|
2
|
+
import { getNameFromPath, type Path } from "./ref.ts";
|
|
3
|
+
import {
|
|
4
|
+
type LinkWriteFormat,
|
|
5
|
+
type PathIndex,
|
|
6
|
+
writeLinkPath,
|
|
7
|
+
} from "./resolve_path.ts";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The space's configured link write format — the one place its config key and
|
|
11
|
+
* default live. Plug-side only (reads config through a syscall).
|
|
12
|
+
*/
|
|
13
|
+
export function linkWriteFormat(): Promise<LinkWriteFormat> {
|
|
14
|
+
return config.get<LinkWriteFormat>("linkWriteFormat", "full-path");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Renders a page path as the link text SilverBullet writes for it under the
|
|
19
|
+
* given format.
|
|
20
|
+
*/
|
|
21
|
+
export function writtenLinkText(
|
|
22
|
+
path: Path,
|
|
23
|
+
format: LinkWriteFormat,
|
|
24
|
+
index: PathIndex,
|
|
25
|
+
): string {
|
|
26
|
+
return getNameFromPath(writeLinkPath(path, format, index));
|
|
27
|
+
}
|
|
@@ -15,9 +15,10 @@ export type PanelStylesOptions = {
|
|
|
15
15
|
* Order: components first (base), then space styles (so user theming wins),
|
|
16
16
|
* leaving any plug-specific CSS you append afterwards highest-precedence.
|
|
17
17
|
*/
|
|
18
|
-
export async function panelStyles(
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export async function panelStyles({
|
|
19
|
+
components = true,
|
|
20
|
+
spaceStyles = true,
|
|
21
|
+
}: PanelStylesOptions = {}): Promise<string> {
|
|
21
22
|
let out = "";
|
|
22
23
|
if (components) {
|
|
23
24
|
out += `<link rel="stylesheet" href=".client/components.css">`;
|
package/plug-api/lib/ref.ts
CHANGED
|
@@ -62,13 +62,29 @@ function normalizePath(path: string): Path {
|
|
|
62
62
|
path = path.slice(1);
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
if (
|
|
65
|
+
if (endsInExtension(path) || path === "") {
|
|
66
66
|
return path as Path;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
return `${path}.md`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
function endsInExtension(path: string): boolean {
|
|
73
|
+
const dot = path.lastIndexOf(".");
|
|
74
|
+
if (dot < 1 || dot === path.length - 1) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
for (let i = dot + 1; i < path.length; i++) {
|
|
78
|
+
const c = path.charCodeAt(i);
|
|
79
|
+
const alphanumeric =
|
|
80
|
+
(c >= 48 && c <= 57) || (c >= 65 && c <= 90) || (c >= 97 && c <= 122);
|
|
81
|
+
if (!alphanumeric) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
/**
|
|
73
89
|
* Determines wether a name conforms to all the requirments.
|
|
74
90
|
*/
|
|
@@ -93,8 +109,7 @@ export function isValidPath(path: string): path is Path {
|
|
|
93
109
|
}
|
|
94
110
|
|
|
95
111
|
/**
|
|
96
|
-
*
|
|
97
|
-
* TO THE INNER WORKINGS OF SILVERBULLET AND CHANGES COULD INTRODUCE MAJOR BUGS
|
|
112
|
+
* Shared reference grammar for parsing and validation; changes affect both.
|
|
98
113
|
*/
|
|
99
114
|
const refRegex =
|
|
100
115
|
/^(?<meta>\^)?(?<path>(?!.*\.[a-zA-Z0-9]+\.md$)(?!\/?(\.|\^))(?!.*(?:\/|^)\.{1,2}(?:\/|$)|.*\/{2})(?!.*(?:\]\]|\[\[))[^@#|<>$]*)(@(?<pos>\d+)|@[Ll](?<line>\d+)(?:[Cc](?<col>\d+))?|#\s*(?<header>.*)|\$(?<anchor>[A-Za-z_][A-Za-z0-9_/:-]*))?$/;
|
|
@@ -141,9 +156,113 @@ export function parseToRef(stringRef: string): Ref | null {
|
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
159
|
+
* Coerces a ref-or-string (including the legacy `{ page, pos, header }` shape)
|
|
160
|
+
* into a {@link Ref} and validates its structure, throwing on malformed input.
|
|
161
|
+
* Used wherever an external caller (e.g. a syscall) hands in a ref that may be a
|
|
162
|
+
* string or a legacy object.
|
|
146
163
|
*/
|
|
164
|
+
export function coerceAndValidateRef(ref: Ref | string): Ref {
|
|
165
|
+
if (typeof ref === "string") {
|
|
166
|
+
const parsedRef = parseToRef(ref);
|
|
167
|
+
if (!parsedRef) {
|
|
168
|
+
throw new Error("Unable to parse string as ref");
|
|
169
|
+
}
|
|
170
|
+
ref = parsedRef;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
// @ts-expect-error: Legacy support
|
|
175
|
+
ref.page !== undefined
|
|
176
|
+
) {
|
|
177
|
+
console.warn(
|
|
178
|
+
"You are using legacy navigation syntax (`{ page, pos, header }`), this will be phased out in the future",
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
const legacyRef = ref as unknown as {
|
|
182
|
+
kind: "page" | "document";
|
|
183
|
+
page: string;
|
|
184
|
+
pos?: number | { line: number; column: number };
|
|
185
|
+
header?: string;
|
|
186
|
+
meta?: boolean;
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
legacyRef.kind ??= "page";
|
|
190
|
+
|
|
191
|
+
let details: Ref["details"];
|
|
192
|
+
|
|
193
|
+
if (typeof legacyRef.pos === "number") {
|
|
194
|
+
details = { type: "position", pos: legacyRef.pos };
|
|
195
|
+
} else if (legacyRef.pos) {
|
|
196
|
+
details = {
|
|
197
|
+
type: "linecolumn",
|
|
198
|
+
line: legacyRef.pos.line,
|
|
199
|
+
column: legacyRef.pos.column,
|
|
200
|
+
};
|
|
201
|
+
} else if (legacyRef.header) {
|
|
202
|
+
details = { type: "header", header: legacyRef.header };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
ref = {
|
|
206
|
+
path: (legacyRef.kind === "page"
|
|
207
|
+
? `${legacyRef.page}.md`
|
|
208
|
+
: legacyRef.page) as Path,
|
|
209
|
+
details,
|
|
210
|
+
meta: legacyRef.meta,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!isValidPath(ref.path) && ref.path !== "") {
|
|
215
|
+
throw new Error("Path passed in ref is invalid");
|
|
216
|
+
} else if (typeof ref.meta !== "boolean" && ref.meta !== undefined) {
|
|
217
|
+
throw new Error("ref.meta has to be of type `boolean`");
|
|
218
|
+
} else if (ref.details !== undefined && typeof ref.details !== "object") {
|
|
219
|
+
throw new Error("ref.details has to be of type `object` or `undefined`");
|
|
220
|
+
} else if (
|
|
221
|
+
ref.details &&
|
|
222
|
+
!["position", "linecolumn", "header", "anchor"].includes(ref.details.type)
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
"ref.details.type has to be 'position', 'linecolumn', 'header' or 'anchor'",
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (ref.details?.type === "position" && typeof ref.details.pos !== "number") {
|
|
230
|
+
throw new Error("ref.details.pos has to be of type `number`");
|
|
231
|
+
} else if (
|
|
232
|
+
ref.details?.type === "header" &&
|
|
233
|
+
typeof ref.details.header !== "string"
|
|
234
|
+
) {
|
|
235
|
+
throw new Error("ref.details.header has to be of type `string`");
|
|
236
|
+
} else if (
|
|
237
|
+
ref.details?.type === "linecolumn" &&
|
|
238
|
+
typeof ref.details.line !== "number" &&
|
|
239
|
+
typeof ref.details.column !== "number"
|
|
240
|
+
) {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"ref.details.line and ref.details.column has to be of type `number`",
|
|
243
|
+
);
|
|
244
|
+
} else if (
|
|
245
|
+
ref.details?.type === "anchor" &&
|
|
246
|
+
typeof ref.details.name !== "string"
|
|
247
|
+
) {
|
|
248
|
+
throw new Error("ref.details.name has to be of type `string`");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return ref;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Renders a ref as wiki link *text*, preserving the `^` meta prefix.
|
|
256
|
+
*
|
|
257
|
+
* {@link encodeRef} deliberately drops it — it also builds page URLs, where a
|
|
258
|
+
* caret would address a page that does not exist — so anything rewriting a link
|
|
259
|
+
* in a document must use this instead, or `[[^Library/Std]]` silently loses its
|
|
260
|
+
* caret.
|
|
261
|
+
*/
|
|
262
|
+
export function encodeLinkText(ref: Ref): string {
|
|
263
|
+
return (ref.meta ? "^" : "") + encodeRef(ref);
|
|
264
|
+
}
|
|
265
|
+
|
|
147
266
|
export function encodeRef(ref: Ref): string {
|
|
148
267
|
let stringRef: string = ref.path;
|
|
149
268
|
|
package/plug-api/lib/resolve.ts
CHANGED
|
@@ -66,7 +66,6 @@ export function resolveMarkdownLink(
|
|
|
66
66
|
* Turns an absolute path into a relative path, relative to some base directory. USE WITH CAUTION, definitely buggy
|
|
67
67
|
*/
|
|
68
68
|
export function absoluteToRelativePath(base: string, absolute: string): string {
|
|
69
|
-
// Remove leading /
|
|
70
69
|
base = base.startsWith("/") ? base.slice(1) : base;
|
|
71
70
|
absolute = absolute.startsWith("/") ? absolute.slice(1) : absolute;
|
|
72
71
|
|