@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.
- package/README.md +4 -4
- 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 +0 -4
- 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/editor.ts +128 -53
- package/client/plugos/syscalls/event.ts +0 -2
- package/client/plugos/syscalls/fetch.ts +0 -4
- package/client/plugos/syscalls/icon.ts +46 -0
- package/client/plugos/syscalls/index.ts +33 -13
- package/client/plugos/syscalls/jsonschema.ts +0 -1
- package/client/plugos/syscalls/mq.ts +0 -1
- package/client/plugos/syscalls/navigator.ts +143 -0
- package/client/plugos/syscalls/search.ts +44 -0
- package/client/plugos/syscalls/shell.ts +0 -1
- package/client/plugos/syscalls/space.ts +250 -7
- package/client/plugos/syscalls/sync.ts +56 -4
- package/client/plugos/syscalls/system.ts +38 -0
- package/client/plugos/system.ts +7 -5
- package/client/plugos/worker_runtime.ts +0 -2
- package/client/space_lua/aggregates.ts +0 -16
- package/client/space_lua/ast.ts +0 -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 +113 -88
- package/client/space_lua/labels.ts +0 -4
- package/client/space_lua/numeric.ts +0 -5
- package/client/space_lua/parse.ts +0 -17
- package/client/space_lua/quarantine.ts +127 -0
- package/client/space_lua/query_collection.ts +3 -47
- 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 +18 -1
- package/client/space_lua/runtime.ts +50 -70
- package/client/space_lua/stdlib/format.ts +0 -12
- package/client/space_lua/stdlib/js.ts +0 -2
- package/client/space_lua/stdlib/load.ts +0 -1
- package/client/space_lua/stdlib/math.ts +0 -7
- package/client/space_lua/stdlib/net.ts +0 -4
- package/client/space_lua/stdlib/os.ts +0 -10
- package/client/space_lua/stdlib/pattern.ts +10 -15
- package/client/space_lua/stdlib/space_lua.ts +1 -2
- package/client/space_lua/stdlib/string_pack.ts +0 -3
- package/client/space_lua/stdlib/table.ts +0 -5
- package/client/space_lua/stdlib.ts +18 -31
- package/client/space_lua/tonumber.ts +0 -9
- package/dist/plug-compile.js +2 -3
- package/package.json +13 -3
- 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/lib/ref.ts +28 -5
- package/plug-api/lib/resolve.ts +0 -1
- package/plug-api/lib/resolve_path.ts +290 -0
- package/plug-api/lib/shortcut.ts +15 -0
- 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 +2 -2
- package/plug-api/syscalls/editor.ts +59 -213
- package/plug-api/syscalls/icon.ts +18 -0
- package/plug-api/syscalls/index.ts +22 -4
- package/plug-api/syscalls/search.ts +11 -0
- package/plug-api/syscalls/space.ts +122 -5
- package/plug-api/syscalls/sync.ts +23 -2
- package/plug-api/syscalls/system.ts +46 -0
- package/plug-api/syscalls.ts +2 -0
- package/plug-api/system_mock.ts +13 -0
- package/plug-api/types/client.ts +4 -3
- package/plug-api/types/datastore.ts +11 -2
- package/plug-api/types/index.ts +10 -0
- package/plug-api/types/profile.ts +13 -0
- package/plug-api/types/revisions.ts +67 -0
- package/plug-api/ui/description.ts +64 -0
- package/plug-api/ui/hover.ts +69 -0
- package/plug-api/ui/index.ts +58 -2
- package/plug-api/ui/scroll.ts +30 -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/plug-api/lib/memory_cache.ts +0 -21
- /package/plug-api/{ui → lib}/panel_styles.ts +0 -0
package/plug-api/lib/dates.ts
CHANGED
|
@@ -2,6 +2,39 @@ export function niceDate(d: Date): string {
|
|
|
2
2
|
return localDateString(d).split("T")[0];
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
/** Largest-unit-first, so the cascade below divides down to the right one. */
|
|
6
|
+
const RELATIVE_DIVISIONS: {
|
|
7
|
+
amount: number;
|
|
8
|
+
unit: Intl.RelativeTimeFormatUnit;
|
|
9
|
+
}[] = [
|
|
10
|
+
{ amount: 60, unit: "second" },
|
|
11
|
+
{ amount: 60, unit: "minute" },
|
|
12
|
+
{ amount: 24, unit: "hour" },
|
|
13
|
+
{ amount: 7, unit: "day" },
|
|
14
|
+
{ amount: 4.34524, unit: "week" },
|
|
15
|
+
{ amount: 12, unit: "month" },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* "10 minutes ago", "yesterday", "3 months ago". `numeric: "auto"` is what
|
|
20
|
+
* turns the -1 cases into words rather than "1 day ago".
|
|
21
|
+
*/
|
|
22
|
+
export function relativeTime(
|
|
23
|
+
timestamp: number,
|
|
24
|
+
locale?: string,
|
|
25
|
+
now: number = Date.now(),
|
|
26
|
+
): string {
|
|
27
|
+
const formatter = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
28
|
+
let duration = (timestamp - now) / 1000;
|
|
29
|
+
for (const { amount, unit } of RELATIVE_DIVISIONS) {
|
|
30
|
+
if (Math.abs(duration) < amount) {
|
|
31
|
+
return formatter.format(Math.round(duration), unit);
|
|
32
|
+
}
|
|
33
|
+
duration /= amount;
|
|
34
|
+
}
|
|
35
|
+
return formatter.format(Math.round(duration), "year");
|
|
36
|
+
}
|
|
37
|
+
|
|
5
38
|
export function localDateString(d: Date): string {
|
|
6
39
|
return (
|
|
7
40
|
d.getFullYear() +
|
|
@@ -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
|
+
}
|
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_/:-]*))?$/;
|
|
@@ -237,9 +252,17 @@ export function coerceAndValidateRef(ref: Ref | string): Ref {
|
|
|
237
252
|
}
|
|
238
253
|
|
|
239
254
|
/**
|
|
240
|
-
*
|
|
241
|
-
*
|
|
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.
|
|
242
261
|
*/
|
|
262
|
+
export function encodeLinkText(ref: Ref): string {
|
|
263
|
+
return (ref.meta ? "^" : "") + encodeRef(ref);
|
|
264
|
+
}
|
|
265
|
+
|
|
243
266
|
export function encodeRef(ref: Ref): string {
|
|
244
267
|
let stringRef: string = ref.path;
|
|
245
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
|
|